mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-31 02:21:35 +00:00
Add background order updates. Add confirm boxes for Dispute and Fiat Received
This commit is contained in:
parent
0db73c7c82
commit
28d18a4842
@ -1,11 +1,14 @@
|
|||||||
from datetime import time, timedelta
|
from datetime import timedelta
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from .lightning.node import LNNode
|
from api.lightning.node import LNNode
|
||||||
|
|
||||||
from .models import Order, LNPayment, MarketTick, User, Currency
|
from api.models import Order, LNPayment, MarketTick, User, Currency
|
||||||
from decouple import config
|
from decouple import config
|
||||||
|
|
||||||
|
from api.tasks import follow_send_payment
|
||||||
|
|
||||||
import math
|
import math
|
||||||
|
import ast
|
||||||
|
|
||||||
FEE = float(config('FEE'))
|
FEE = float(config('FEE'))
|
||||||
BOND_SIZE = float(config('BOND_SIZE'))
|
BOND_SIZE = float(config('BOND_SIZE'))
|
||||||
@ -140,6 +143,7 @@ class Logics():
|
|||||||
|
|
||||||
cls.settle_bond(order.maker_bond)
|
cls.settle_bond(order.maker_bond)
|
||||||
cls.settle_bond(order.taker_bond)
|
cls.settle_bond(order.taker_bond)
|
||||||
|
cls.cancel_escrow(order)
|
||||||
order.status = Order.Status.EXP
|
order.status = Order.Status.EXP
|
||||||
order.maker = None
|
order.maker = None
|
||||||
order.taker = None
|
order.taker = None
|
||||||
@ -152,6 +156,7 @@ class Logics():
|
|||||||
if maker_is_seller:
|
if maker_is_seller:
|
||||||
cls.settle_bond(order.maker_bond)
|
cls.settle_bond(order.maker_bond)
|
||||||
cls.return_bond(order.taker_bond)
|
cls.return_bond(order.taker_bond)
|
||||||
|
cls.cancel_escrow(order)
|
||||||
order.status = Order.Status.EXP
|
order.status = Order.Status.EXP
|
||||||
order.maker = None
|
order.maker = None
|
||||||
order.taker = None
|
order.taker = None
|
||||||
@ -161,22 +166,25 @@ class Logics():
|
|||||||
# If maker is buyer, settle the taker's bond order goes back to public
|
# If maker is buyer, settle the taker's bond order goes back to public
|
||||||
else:
|
else:
|
||||||
cls.settle_bond(order.taker_bond)
|
cls.settle_bond(order.taker_bond)
|
||||||
|
cls.cancel_escrow(order)
|
||||||
order.status = Order.Status.PUB
|
order.status = Order.Status.PUB
|
||||||
order.taker = None
|
order.taker = None
|
||||||
order.taker_bond = None
|
order.taker_bond = None
|
||||||
|
order.trade_escrow = None
|
||||||
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
|
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
|
||||||
order.save()
|
order.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif order.status == Order.Status.WFI:
|
elif order.status == Order.Status.WFI:
|
||||||
# The trade could happen without a buyer invoice. However, this user
|
# The trade could happen without a buyer invoice. However, this user
|
||||||
# is likely AFK since he did not submit an invoice; will probably
|
# is likely AFK; will probably desert the contract as well.
|
||||||
# desert the contract as well.
|
|
||||||
maker_is_buyer = cls.is_buyer(order, order.maker)
|
maker_is_buyer = cls.is_buyer(order, order.maker)
|
||||||
# If maker is buyer, settle the bond and order goes to expired
|
# If maker is buyer, settle the bond and order goes to expired
|
||||||
if maker_is_buyer:
|
if maker_is_buyer:
|
||||||
cls.settle_bond(order.maker_bond)
|
cls.settle_bond(order.maker_bond)
|
||||||
cls.return_bond(order.taker_bond)
|
cls.return_bond(order.taker_bond)
|
||||||
|
cls.return_escrow(order)
|
||||||
order.status = Order.Status.EXP
|
order.status = Order.Status.EXP
|
||||||
order.maker = None
|
order.maker = None
|
||||||
order.taker = None
|
order.taker = None
|
||||||
@ -186,17 +194,19 @@ class Logics():
|
|||||||
# If maker is seller settle the taker's bond, order goes back to public
|
# If maker is seller settle the taker's bond, order goes back to public
|
||||||
else:
|
else:
|
||||||
cls.settle_bond(order.taker_bond)
|
cls.settle_bond(order.taker_bond)
|
||||||
|
cls.return_escrow(order)
|
||||||
order.status = Order.Status.PUB
|
order.status = Order.Status.PUB
|
||||||
order.taker = None
|
order.taker = None
|
||||||
order.taker_bond = None
|
order.taker_bond = None
|
||||||
|
order.trade_escrow = None
|
||||||
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
|
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
|
||||||
order.save()
|
order.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
elif order.status == Order.Status.CHA:
|
elif order.status == Order.Status.CHA:
|
||||||
# Another weird case. The time to confirm 'fiat sent' expired. Yet no dispute
|
# Another weird case. The time to confirm 'fiat sent' expired. Yet no dispute
|
||||||
# was opened. A seller-scammer could persuade a buyer to not click "fiat sent"
|
# was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat
|
||||||
# as of now, we assume this is a dispute case by default.
|
# sent", we assume this is a dispute case by default.
|
||||||
cls.open_dispute(order)
|
cls.open_dispute(order)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -219,12 +229,14 @@ class Logics():
|
|||||||
def open_dispute(cls, order, user=None):
|
def open_dispute(cls, order, user=None):
|
||||||
|
|
||||||
# Always settle the escrow during a dispute (same as with 'Fiat Sent')
|
# Always settle the escrow during a dispute (same as with 'Fiat Sent')
|
||||||
|
# Dispute winner will have to submit a new invoice.
|
||||||
|
|
||||||
if not order.trade_escrow.status == LNPayment.Status.SETLED:
|
if not order.trade_escrow.status == LNPayment.Status.SETLED:
|
||||||
cls.settle_escrow(order)
|
cls.settle_escrow(order)
|
||||||
|
|
||||||
order.is_disputed = True
|
order.is_disputed = True
|
||||||
order.status = Order.Status.DIS
|
order.status = Order.Status.DIS
|
||||||
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.DIS])
|
order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.DIS])
|
||||||
order.save()
|
order.save()
|
||||||
|
|
||||||
# User could be None if a dispute is open automatically due to weird expiration.
|
# User could be None if a dispute is open automatically due to weird expiration.
|
||||||
@ -235,6 +247,7 @@ class Logics():
|
|||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
def dispute_statement(order, user, statement):
|
def dispute_statement(order, user, statement):
|
||||||
''' Updates the dispute statements in DB'''
|
''' Updates the dispute statements in DB'''
|
||||||
if not order.status == Order.Status.DIS:
|
if not order.status == Order.Status.DIS:
|
||||||
@ -319,16 +332,18 @@ class Logics():
|
|||||||
def add_profile_rating(profile, rating):
|
def add_profile_rating(profile, rating):
|
||||||
''' adds a new rating to a user profile'''
|
''' adds a new rating to a user profile'''
|
||||||
|
|
||||||
|
# TODO Unsafe, does not update ratings, it adds more ratings everytime a new rating is clicked.
|
||||||
profile.total_ratings = profile.total_ratings + 1
|
profile.total_ratings = profile.total_ratings + 1
|
||||||
latest_ratings = profile.latest_ratings
|
latest_ratings = profile.latest_ratings
|
||||||
if len(latest_ratings) <= 1:
|
if latest_ratings == None:
|
||||||
profile.latest_ratings = [rating]
|
profile.latest_ratings = [rating]
|
||||||
profile.avg_rating = rating
|
profile.avg_rating = rating
|
||||||
|
|
||||||
else:
|
else:
|
||||||
latest_ratings = list(latest_ratings).append(rating)
|
latest_ratings = ast.literal_eval(latest_ratings)
|
||||||
|
latest_ratings.append(rating)
|
||||||
profile.latest_ratings = latest_ratings
|
profile.latest_ratings = latest_ratings
|
||||||
profile.avg_rating = sum(latest_ratings) / len(latest_ratings)
|
profile.avg_rating = sum(list(map(int, latest_ratings))) / len(latest_ratings) # Just an average, but it is a list of strings. Has to be converted to int.
|
||||||
|
|
||||||
profile.save()
|
profile.save()
|
||||||
|
|
||||||
@ -413,15 +428,20 @@ class Logics():
|
|||||||
else:
|
else:
|
||||||
return False, {'bad_request':'You cannot cancel this order'}
|
return False, {'bad_request':'You cannot cancel this order'}
|
||||||
|
|
||||||
|
def publish_order(order):
|
||||||
|
if order.status == Order.Status.WFB:
|
||||||
|
order.status = Order.Status.PUB
|
||||||
|
# With the bond confirmation the order is extended 'public_order_duration' hours
|
||||||
|
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
|
||||||
|
order.save()
|
||||||
|
return
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_maker_bond_locked(cls, order):
|
def is_maker_bond_locked(cls, order):
|
||||||
if LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
|
if LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash):
|
||||||
order.maker_bond.status = LNPayment.Status.LOCKED
|
order.maker_bond.status = LNPayment.Status.LOCKED
|
||||||
order.maker_bond.save()
|
order.maker_bond.save()
|
||||||
order.status = Order.Status.PUB
|
cls.publish_order(order)
|
||||||
# With the bond confirmation the order is extended 'public_order_duration' hours
|
|
||||||
order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB])
|
|
||||||
order.save()
|
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -467,13 +487,12 @@ class Logics():
|
|||||||
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
|
return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis}
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def is_taker_bond_locked(cls, order):
|
def finalize_contract(cls, order):
|
||||||
if order.taker_bond.status == LNPayment.Status.LOCKED:
|
''' When the taker locks the taker_bond
|
||||||
return True
|
the contract is final '''
|
||||||
|
|
||||||
if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
|
|
||||||
# THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
|
# THE TRADE AMOUNT IS FINAL WITH THE CONFIRMATION OF THE TAKER BOND!
|
||||||
# (This is the last update to "last_satoshis", it becomes the escrow amount next!)
|
# (This is the last update to "last_satoshis", it becomes the escrow amount next)
|
||||||
order.last_satoshis = cls.satoshis_now(order)
|
order.last_satoshis = cls.satoshis_now(order)
|
||||||
order.taker_bond.status = LNPayment.Status.LOCKED
|
order.taker_bond.status = LNPayment.Status.LOCKED
|
||||||
order.taker_bond.save()
|
order.taker_bond.save()
|
||||||
@ -492,6 +511,14 @@ class Logics():
|
|||||||
order.status = Order.Status.WF2
|
order.status = Order.Status.WF2
|
||||||
order.save()
|
order.save()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_taker_bond_locked(cls, order):
|
||||||
|
if order.taker_bond.status == LNPayment.Status.LOCKED:
|
||||||
|
return True
|
||||||
|
if LNNode.validate_hold_invoice_locked(order.taker_bond.payment_hash):
|
||||||
|
cls.finalize_contract(order)
|
||||||
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -618,11 +645,17 @@ class Logics():
|
|||||||
order.trade_escrow.status = LNPayment.Status.RETNED
|
order.trade_escrow.status = LNPayment.Status.RETNED
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def cancel_escrow(order):
|
||||||
|
'''returns the trade escrow'''
|
||||||
|
# Same as return escrow, but used when the invoice was never LOCKED
|
||||||
|
if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash):
|
||||||
|
order.trade_escrow.status = LNPayment.Status.CANCEL
|
||||||
|
return True
|
||||||
|
|
||||||
def return_bond(bond):
|
def return_bond(bond):
|
||||||
'''returns a bond'''
|
'''returns a bond'''
|
||||||
if bond == None:
|
if bond == None:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
LNNode.cancel_return_hold_invoice(bond.payment_hash)
|
LNNode.cancel_return_hold_invoice(bond.payment_hash)
|
||||||
bond.status = LNPayment.Status.RETNED
|
bond.status = LNPayment.Status.RETNED
|
||||||
@ -631,10 +664,12 @@ class Logics():
|
|||||||
if 'invoice already settled' in str(e):
|
if 'invoice already settled' in str(e):
|
||||||
bond.status = LNPayment.Status.SETLED
|
bond.status = LNPayment.Status.SETLED
|
||||||
return True
|
return True
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
def cancel_bond(bond):
|
def cancel_bond(bond):
|
||||||
'''cancel a bond'''
|
'''cancel a bond'''
|
||||||
# Same as return bond, but used when the invoice was never accepted
|
# Same as return bond, but used when the invoice was never LOCKED
|
||||||
if bond == None:
|
if bond == None:
|
||||||
return True
|
return True
|
||||||
try:
|
try:
|
||||||
@ -645,11 +680,12 @@ class Logics():
|
|||||||
if 'invoice already settled' in str(e):
|
if 'invoice already settled' in str(e):
|
||||||
bond.status = LNPayment.Status.SETLED
|
bond.status = LNPayment.Status.SETLED
|
||||||
return True
|
return True
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
def pay_buyer_invoice(order):
|
def pay_buyer_invoice(order):
|
||||||
''' Pay buyer invoice'''
|
''' Pay buyer invoice'''
|
||||||
# TODO ERROR HANDLING
|
suceeded, context = follow_send_payment(order.buyer_invoice)
|
||||||
suceeded, context = LNNode.pay_invoice(order.buyer_invoice.invoice, order.buyer_invoice.num_satoshis)
|
|
||||||
return suceeded, context
|
return suceeded, context
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -703,11 +739,15 @@ class Logics():
|
|||||||
# If the trade is finished
|
# If the trade is finished
|
||||||
if order.status > Order.Status.PAY:
|
if order.status > Order.Status.PAY:
|
||||||
# if maker, rates taker
|
# if maker, rates taker
|
||||||
if order.maker == user:
|
if order.maker == user and order.maker_rated == False:
|
||||||
cls.add_profile_rating(order.taker.profile, rating)
|
cls.add_profile_rating(order.taker.profile, rating)
|
||||||
|
order.maker_rated = True
|
||||||
|
order.save()
|
||||||
# if taker, rates maker
|
# if taker, rates maker
|
||||||
if order.taker == user:
|
if order.taker == user and order.taker_rated == False:
|
||||||
cls.add_profile_rating(order.maker.profile, rating)
|
cls.add_profile_rating(order.maker.profile, rating)
|
||||||
|
order.taker_rated = True
|
||||||
|
order.save()
|
||||||
else:
|
else:
|
||||||
return False, {'bad_request':'You cannot rate your counterparty yet.'}
|
return False, {'bad_request':'You cannot rate your counterparty yet.'}
|
||||||
|
|
||||||
|
@ -36,6 +36,7 @@ class Command(BaseCommand):
|
|||||||
context = str(order)+ " was "+ Order.Status(order.status).label
|
context = str(order)+ " was "+ Order.Status(order.status).label
|
||||||
if Logics.order_expires(order): # Order send to expire here
|
if Logics.order_expires(order): # Order send to expire here
|
||||||
debug['expired_orders'].append({idx:context})
|
debug['expired_orders'].append({idx:context})
|
||||||
|
|
||||||
self.stdout.write(str(timezone.now()))
|
if debug['num_expired_orders'] > 0:
|
||||||
self.stdout.write(str(debug))
|
self.stdout.write(str(timezone.now()))
|
||||||
|
self.stdout.write(str(debug))
|
||||||
|
@ -1,21 +1,25 @@
|
|||||||
from django.core.management.base import BaseCommand, CommandError
|
from django.core.management.base import BaseCommand, CommandError
|
||||||
|
|
||||||
from django.utils import timezone
|
|
||||||
from api.lightning.node import LNNode
|
from api.lightning.node import LNNode
|
||||||
|
from api.models import LNPayment, Order
|
||||||
|
from api.logics import Logics
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from datetime import timedelta
|
||||||
from decouple import config
|
from decouple import config
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from api.models import LNPayment
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
MACAROON = b64decode(config('LND_MACAROON_BASE64'))
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
'''
|
'''
|
||||||
Background: SubscribeInvoices stub iterator would be great to use here
|
Background: SubscribeInvoices stub iterator would be great to use here.
|
||||||
however it only sends updates when the invoice is OPEN (new) or SETTLED.
|
However, it only sends updates when the invoice is OPEN (new) or SETTLED.
|
||||||
We are very interested on the other two states (CANCELLED and ACCEPTED).
|
We are very interested on the other two states (CANCELLED and ACCEPTED).
|
||||||
Therefore, this thread (follow_invoices) will iterate over all LNpayment
|
Therefore, this thread (follow_invoices) will iterate over all LNpayment
|
||||||
objects and do InvoiceLookupV2 to update their state 'live' '''
|
objects and do InvoiceLookupV2 every X seconds to update their state 'live'
|
||||||
|
'''
|
||||||
|
|
||||||
help = 'Follows all active hold invoices'
|
help = 'Follows all active hold invoices'
|
||||||
|
|
||||||
@ -27,10 +31,10 @@ class Command(BaseCommand):
|
|||||||
until settled or canceled'''
|
until settled or canceled'''
|
||||||
|
|
||||||
lnd_state_to_lnpayment_status = {
|
lnd_state_to_lnpayment_status = {
|
||||||
0: LNPayment.Status.INVGEN,
|
0: LNPayment.Status.INVGEN, # OPEN
|
||||||
1: LNPayment.Status.SETLED,
|
1: LNPayment.Status.SETLED, # SETTLED
|
||||||
2: LNPayment.Status.CANCEL,
|
2: LNPayment.Status.CANCEL, # CANCELLED
|
||||||
3: LNPayment.Status.LOCKED
|
3: LNPayment.Status.LOCKED # ACCEPTED
|
||||||
}
|
}
|
||||||
|
|
||||||
stub = LNNode.invoicesstub
|
stub = LNNode.invoicesstub
|
||||||
@ -45,6 +49,7 @@ class Command(BaseCommand):
|
|||||||
debug = {}
|
debug = {}
|
||||||
debug['num_active_invoices'] = len(queryset)
|
debug['num_active_invoices'] = len(queryset)
|
||||||
debug['invoices'] = []
|
debug['invoices'] = []
|
||||||
|
at_least_one_changed = False
|
||||||
|
|
||||||
for idx, hold_lnpayment in enumerate(queryset):
|
for idx, hold_lnpayment in enumerate(queryset):
|
||||||
old_status = LNPayment.Status(hold_lnpayment.status).label
|
old_status = LNPayment.Status(hold_lnpayment.status).label
|
||||||
@ -56,29 +61,55 @@ class Command(BaseCommand):
|
|||||||
|
|
||||||
# If it fails at finding the invoice it has been canceled.
|
# If it fails at finding the invoice it has been canceled.
|
||||||
# On RoboSats DB we make a distinction between cancelled and returned (LND does not)
|
# On RoboSats DB we make a distinction between cancelled and returned (LND does not)
|
||||||
except:
|
except Exception as e:
|
||||||
hold_lnpayment.status = LNPayment.Status.CANCEL
|
if 'unable to locate invoice' in str(e):
|
||||||
continue
|
hold_lnpayment.status = LNPayment.Status.CANCEL
|
||||||
|
else:
|
||||||
|
self.stdout.write(str(e))
|
||||||
|
|
||||||
new_status = LNPayment.Status(hold_lnpayment.status).label
|
new_status = LNPayment.Status(hold_lnpayment.status).label
|
||||||
|
|
||||||
# Only save the hold_payments that change (otherwise this function does not scale)
|
# Only save the hold_payments that change (otherwise this function does not scale)
|
||||||
changed = not old_status==new_status
|
changed = not old_status==new_status
|
||||||
if changed:
|
if changed:
|
||||||
|
# self.handle_status_change(hold_lnpayment, old_status)
|
||||||
hold_lnpayment.save()
|
hold_lnpayment.save()
|
||||||
|
self.update_order_status(hold_lnpayment)
|
||||||
|
|
||||||
# Report for debugging
|
# Report for debugging
|
||||||
new_status = LNPayment.Status(hold_lnpayment.status).label
|
new_status = LNPayment.Status(hold_lnpayment.status).label
|
||||||
debug['invoices'].append({idx:{
|
debug['invoices'].append({idx:{
|
||||||
'payment_hash': str(hold_lnpayment.payment_hash),
|
'payment_hash': str(hold_lnpayment.payment_hash),
|
||||||
'status_changed': not old_status==new_status,
|
'old_status': old_status,
|
||||||
'old_status': old_status,
|
'new_status': new_status,
|
||||||
'new_status': new_status,
|
}})
|
||||||
}})
|
|
||||||
|
|
||||||
debug['time']=time.time()-t0
|
at_least_one_changed = at_least_one_changed or changed
|
||||||
|
|
||||||
self.stdout.write(str(timezone.now())+str(debug))
|
debug['time']=time.time()-t0
|
||||||
|
|
||||||
|
if at_least_one_changed:
|
||||||
|
self.stdout.write(str(timezone.now()))
|
||||||
|
self.stdout.write(str(debug))
|
||||||
|
|
||||||
|
|
||||||
|
def update_order_status(self, lnpayment):
|
||||||
|
''' Background process following LND hold invoices
|
||||||
|
might catch LNpayments changing status. If they do,
|
||||||
|
the order status might have to change status too.'''
|
||||||
|
|
||||||
|
# If the LNPayment goes to LOCKED (ACCEPTED)
|
||||||
|
if lnpayment.status == LNPayment.Status.LOCKED:
|
||||||
|
|
||||||
|
# It is a maker bond => Publish order.
|
||||||
|
order = lnpayment.order_made
|
||||||
|
if not order == None:
|
||||||
|
Logics.publish_order(order)
|
||||||
|
return
|
||||||
|
|
||||||
|
# It is a taker bond => close contract.
|
||||||
|
order = lnpayment.order_taken
|
||||||
|
if not order == None:
|
||||||
|
if order.status == Order.Status.TAK:
|
||||||
|
Logics.finalize_contract(order)
|
||||||
|
return
|
@ -146,16 +146,16 @@ class Order(models.Model):
|
|||||||
|
|
||||||
# LNpayments
|
# LNpayments
|
||||||
# Order collateral
|
# Order collateral
|
||||||
maker_bond = models.ForeignKey(LNPayment, related_name='maker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
maker_bond = models.OneToOneField(LNPayment, related_name='order_made', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||||
taker_bond = models.ForeignKey(LNPayment, related_name='taker_bond', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
taker_bond = models.OneToOneField(LNPayment, related_name='order_taken', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||||
trade_escrow = models.ForeignKey(LNPayment, related_name='trade_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
trade_escrow = models.OneToOneField(LNPayment, related_name='order_escrow', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||||
|
|
||||||
# buyer payment LN invoice
|
# buyer payment LN invoice
|
||||||
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
buyer_invoice = models.ForeignKey(LNPayment, related_name='buyer_invoice', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
||||||
|
|
||||||
# Unused so far. Cancel LN invoices // these are only needed to charge lower-than-bond amounts. E.g., a taken order has a small cost if cancelled, to avoid DDOSing.
|
# ratings
|
||||||
# maker_cancel = models.ForeignKey(LNPayment, related_name='maker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
maker_rated = models.BooleanField(default=False, null=False)
|
||||||
# taker_cancel = models.ForeignKey(LNPayment, related_name='taker_cancel', on_delete=models.SET_NULL, null=True, default=None, blank=True)
|
taker_rated = models.BooleanField(default=False, null=False)
|
||||||
|
|
||||||
t_to_expire = {
|
t_to_expire = {
|
||||||
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
|
0 : int(config('EXP_MAKER_BOND_INVOICE')) , # 'Waiting for maker bond'
|
||||||
@ -182,6 +182,7 @@ class Order(models.Model):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
# Make relational back to ORDER
|
# Make relational back to ORDER
|
||||||
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}')
|
return (f'Order {self.id}: {self.Types(self.type).label} BTC for {float(self.amount)} {self.currency}')
|
||||||
|
|
||||||
|
|
||||||
@receiver(pre_delete, sender=Order)
|
@receiver(pre_delete, sender=Order)
|
||||||
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
|
def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
|
||||||
|
@ -61,7 +61,7 @@ export default class OrderPage extends Component {
|
|||||||
"8": 10000, //'Waiting only for buyer invoice'
|
"8": 10000, //'Waiting only for buyer invoice'
|
||||||
"9": 10000, //'Sending fiat - In chatroom'
|
"9": 10000, //'Sending fiat - In chatroom'
|
||||||
"10": 15000, //'Fiat sent - In chatroom'
|
"10": 15000, //'Fiat sent - In chatroom'
|
||||||
"11": 300000, //'In dispute'
|
"11": 60000, //'In dispute'
|
||||||
"12": 9999999,//'Collaboratively cancelled'
|
"12": 9999999,//'Collaboratively cancelled'
|
||||||
"13": 120000, //'Sending satoshis to buyer'
|
"13": 120000, //'Sending satoshis to buyer'
|
||||||
"14": 9999999,//'Sucessful trade'
|
"14": 9999999,//'Sucessful trade'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon} from "@mui/material"
|
import { Link, Paper, Rating, Button, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
|
||||||
import QRCode from "react-qr-code";
|
import QRCode from "react-qr-code";
|
||||||
|
|
||||||
import Chat from "./Chat"
|
import Chat from "./Chat"
|
||||||
@ -37,11 +37,100 @@ export default class TradeBox extends Component {
|
|||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
this.state = {
|
this.state = {
|
||||||
|
openConfirmFiatReceived: false,
|
||||||
|
openConfirmDispute: false,
|
||||||
badInvoice: false,
|
badInvoice: false,
|
||||||
badStatement: false,
|
badStatement: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleClickOpenConfirmDispute = () => {
|
||||||
|
this.setState({openConfirmDispute: true});
|
||||||
|
};
|
||||||
|
handleClickCloseConfirmDispute = () => {
|
||||||
|
this.setState({openConfirmDispute: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClickAgreeDisputeButton=()=>{
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'action': "dispute",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => (this.props.data = data));
|
||||||
|
this.handleClickCloseConfirmDispute();
|
||||||
|
}
|
||||||
|
|
||||||
|
ConfirmDisputeDialog =() =>{
|
||||||
|
return(
|
||||||
|
<Dialog
|
||||||
|
open={this.state.openConfirmDispute}
|
||||||
|
onClose={this.handleClickCloseConfirmDispute}
|
||||||
|
aria-labelledby="open-dispute-dialog-title"
|
||||||
|
aria-describedby="open-dispute-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="open-dispute-dialog-title">
|
||||||
|
{"Do you want to open a dispute?"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
The RoboSats staff will examine the statements and evidence provided by the participants.
|
||||||
|
It is best if you provide a burner contact method on your statement for the staff to contact you.
|
||||||
|
The satoshis in the trade escrow will be sent to the dispute winner, while the dispute
|
||||||
|
loser will lose the bond.
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={this.handleClickCloseConfirmDispute} autoFocus>Disagree</Button>
|
||||||
|
<Button onClick={this.handleClickAgreeDisputeButton}> Agree </Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickOpenConfirmFiatReceived = () => {
|
||||||
|
this.setState({openConfirmFiatReceived: true});
|
||||||
|
};
|
||||||
|
handleClickCloseConfirmFiatReceived = () => {
|
||||||
|
this.setState({openConfirmFiatReceived: false});
|
||||||
|
};
|
||||||
|
|
||||||
|
handleClickTotallyConfirmFiatReceived = () =>{
|
||||||
|
this.handleClickConfirmButton();
|
||||||
|
this.handleClickCloseConfirmFiatReceived();
|
||||||
|
};
|
||||||
|
|
||||||
|
ConfirmFiatReceivedDialog =() =>{
|
||||||
|
return(
|
||||||
|
<Dialog
|
||||||
|
open={this.state.openConfirmFiatReceived}
|
||||||
|
onClose={this.handleClickCloseConfirmFiatReceived}
|
||||||
|
aria-labelledby="fiat-received-dialog-title"
|
||||||
|
aria-describedby="fiat-received-dialog-description"
|
||||||
|
>
|
||||||
|
<DialogTitle id="open-dispute-dialog-title">
|
||||||
|
{"Confirm you received " +this.props.data.currencyCode+ "?"}
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
Confirming that you received the fiat will finalize the trade. The satoshis
|
||||||
|
in the escrow will be released to the buyer. Only confirm after the {this.props.data.currencyCode+ " "}
|
||||||
|
has arrived to your account. In addition, if you have received {this.props.data.currencyCode+ " "}
|
||||||
|
and do not confirm the receipt, you risk losing your bond.
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button onClick={this.handleClickCloseConfirmFiatReceived} autoFocus>Go back</Button>
|
||||||
|
<Button onClick={this.handleClickTotallyConfirmFiatReceived}> Confirm </Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
showQRInvoice=()=>{
|
showQRInvoice=()=>{
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
@ -275,7 +364,7 @@ export default class TradeBox extends Component {
|
|||||||
/>
|
/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Button onClick={this.handleClickSubmitStatementButton} variant='contained' color='primary'>Submit</Button>
|
<Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>Submit</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{this.showBondIsLocked()}
|
{this.showBondIsLocked()}
|
||||||
@ -382,18 +471,7 @@ export default class TradeBox extends Component {
|
|||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then((data) => (this.props.data = data));
|
.then((data) => (this.props.data = data));
|
||||||
}
|
}
|
||||||
handleClickOpenDisputeButton=()=>{
|
|
||||||
const requestOptions = {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
|
||||||
body: JSON.stringify({
|
|
||||||
'action': "dispute",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
|
||||||
.then((response) => response.json())
|
|
||||||
.then((data) => (this.props.data = data));
|
|
||||||
}
|
|
||||||
handleRatingChange=(e)=>{
|
handleRatingChange=(e)=>{
|
||||||
const requestOptions = {
|
const requestOptions = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -419,11 +497,9 @@ handleRatingChange=(e)=>{
|
|||||||
}
|
}
|
||||||
|
|
||||||
showFiatReceivedButton(){
|
showFiatReceivedButton(){
|
||||||
// TODO, show alert and ask for double confirmation (Have you check you received the fiat? Confirming fiat received settles the trade.)
|
|
||||||
// Ask for double confirmation.
|
|
||||||
return(
|
return(
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Button defaultValue="confirm" variant='contained' color='secondary' onClick={this.handleClickConfirmButton}>Confirm {this.props.data.currencyCode} received</Button>
|
<Button defaultValue="confirm" variant='contained' color='secondary' onClick={this.handleClickOpenConfirmFiatReceived}>Confirm {this.props.data.currencyCode} received</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -432,7 +508,7 @@ handleRatingChange=(e)=>{
|
|||||||
// TODO, show alert about how opening a dispute might involve giving away personal data and might mean losing the bond. Ask for double confirmation.
|
// TODO, show alert about how opening a dispute might involve giving away personal data and might mean losing the bond. Ask for double confirmation.
|
||||||
return(
|
return(
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Button color="inherit" onClick={this.handleClickOpenDisputeButton}>Open Dispute</Button>
|
<Button color="inherit" onClick={this.handleClickOpenConfirmDispute}>Open Dispute</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -487,7 +563,7 @@ handleRatingChange=(e)=>{
|
|||||||
<Rating name="size-large" defaultValue={2} size="large" onChange={this.handleRatingChange} />
|
<Rating name="size-large" defaultValue={2} size="large" onChange={this.handleRatingChange} />
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Button color='primary' to='/' component={Link}>Start Again</Button>
|
<Button color='primary' href='/' component="a">Start Again</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
)
|
)
|
||||||
@ -497,6 +573,8 @@ handleRatingChange=(e)=>{
|
|||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<Grid container spacing={1} style={{ width:330}}>
|
<Grid container spacing={1} style={{ width:330}}>
|
||||||
|
<this.ConfirmDisputeDialog/>
|
||||||
|
<this.ConfirmFiatReceivedDialog/>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography component="h5" variant="h5">
|
<Typography component="h5" variant="h5">
|
||||||
Contract Box
|
Contract Box
|
||||||
|
Loading…
Reference in New Issue
Block a user