from datetime import timedelta from django.utils import timezone from api.lightning.node import LNNode from api.models import Order, LNPayment, MarketTick, User, Currency from decouple import config from api.tasks import follow_send_payment import math import ast FEE = float(config('FEE')) BOND_SIZE = float(config('BOND_SIZE')) ESCROW_USERNAME = config('ESCROW_USERNAME') PENALTY_TIMEOUT = int(config('PENALTY_TIMEOUT')) MIN_TRADE = int(config('MIN_TRADE')) MAX_TRADE = int(config('MAX_TRADE')) EXP_MAKER_BOND_INVOICE = int(config('EXP_MAKER_BOND_INVOICE')) EXP_TAKER_BOND_INVOICE = int(config('EXP_TAKER_BOND_INVOICE')) BOND_EXPIRY = int(config('BOND_EXPIRY')) ESCROW_EXPIRY = int(config('ESCROW_EXPIRY')) PUBLIC_ORDER_DURATION = int(config('PUBLIC_ORDER_DURATION')) INVOICE_AND_ESCROW_DURATION = int(config('INVOICE_AND_ESCROW_DURATION')) FIAT_EXCHANGE_DURATION = int(config('FIAT_EXCHANGE_DURATION')) class Logics(): def validate_already_maker_or_taker(user): '''Checks if the user is already partipant of an order''' queryset = Order.objects.filter(maker=user) if queryset.exists(): return False, {'bad_request':'You are already maker of an order'} queryset = Order.objects.filter(taker=user) if queryset.exists(): return False, {'bad_request':'You are already taker of an order'} return True, None def validate_order_size(order): '''Checks if order is withing limits at t0''' if order.t0_satoshis > MAX_TRADE: return False, {'bad_request': 'Your order is too big. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now. But limit is '+'{:,}'.format(MAX_TRADE)+ ' Sats'} if order.t0_satoshis < MIN_TRADE: return False, {'bad_request': 'Your order is too small. It is worth '+'{:,}'.format(order.t0_satoshis)+' Sats now. But limit is '+'{:,}'.format(MIN_TRADE)+ ' Sats'} return True, None @classmethod def take(cls, order, user): is_penalized, time_out = cls.is_penalized(user) if is_penalized: return False, {'bad_request',f'You need to wait {time_out} seconds to take an order'} else: order.taker = user order.status = Order.Status.TAK order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK]) order.save() return True, None def is_buyer(order, user): is_maker = order.maker == user is_taker = order.taker == user return (is_maker and order.type == Order.Types.BUY) or (is_taker and order.type == Order.Types.SELL) def is_seller(order, user): is_maker = order.maker == user is_taker = order.taker == user return (is_maker and order.type == Order.Types.SELL) or (is_taker and order.type == Order.Types.BUY) def satoshis_now(order): ''' checks trade amount in sats ''' if order.is_explicit: satoshis_now = order.satoshis else: exchange_rate = float(order.currency.exchange_rate) premium_rate = exchange_rate * (1+float(order.premium)/100) satoshis_now = (float(order.amount) / premium_rate) * 100*1000*1000 return int(satoshis_now) def price_and_premium_now(order): ''' computes order premium live ''' exchange_rate = float(order.currency.exchange_rate) if not order.is_explicit: premium = order.premium price = exchange_rate * (1+float(premium)/100) else: order_rate = float(order.amount) / (float(order.satoshis) / 100000000) premium = order_rate / exchange_rate - 1 premium = int(premium*10000)/100 # 2 decimals left price = order_rate significant_digits = 5 price = round(price, significant_digits - int(math.floor(math.log10(abs(price)))) - 1) return price, premium @classmethod def order_expires(cls, order): ''' General cases when time runs out.''' # Do not change order status if an order in any with # any of these status is sent to expire here do_nothing = [Order.Status.DEL, Order.Status.UCA, Order.Status.EXP, Order.Status.FSE, Order.Status.DIS, Order.Status.CCA, Order.Status.PAY, Order.Status.SUC, Order.Status.FAI, Order.Status.MLD, Order.Status.TLD] if order.status in do_nothing: return False elif order.status == Order.Status.WFB: order.status = Order.Status.EXP cls.cancel_bond(order.maker_bond) order.maker = None order.taker = None order.save() return True elif order.status == Order.Status.PUB: cls.return_bond(order.maker_bond) order.status = Order.Status.EXP order.maker = None order.taker = None order.save() return True elif order.status == Order.Status.TAK: cls.cancel_bond(order.taker_bond) cls.kick_taker(order) return True elif order.status == Order.Status.WF2: '''Weird case where an order expires and both participants did not proceed with the contract. Likely the site was down or there was a bug. Still bonds must be charged to avoid service DDOS. ''' cls.settle_bond(order.maker_bond) cls.settle_bond(order.taker_bond) cls.cancel_escrow(order) order.status = Order.Status.EXP order.maker = None order.taker = None order.save() return True elif order.status == Order.Status.WFE: maker_is_seller = cls.is_seller(order, order.maker) # If maker is seller, settle the bond and order goes to expired if maker_is_seller: cls.settle_bond(order.maker_bond) cls.return_bond(order.taker_bond) cls.cancel_escrow(order) order.status = Order.Status.EXP order.maker = None order.taker = None order.save() return True # If maker is buyer, settle the taker's bond order goes back to public else: cls.settle_bond(order.taker_bond) cls.cancel_escrow(order) order.status = Order.Status.PUB order.taker = 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.save() return True elif order.status == Order.Status.WFI: # The trade could happen without a buyer invoice. However, this user # is likely AFK; will probably desert the contract as well. 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: cls.settle_bond(order.maker_bond) cls.return_bond(order.taker_bond) cls.return_escrow(order) order.status = Order.Status.EXP order.maker = None order.taker = None order.save() return True # If maker is seller settle the taker's bond, order goes back to public else: cls.settle_bond(order.taker_bond) cls.return_escrow(order) order.status = Order.Status.PUB order.taker = 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.save() return True elif order.status == Order.Status.CHA: # Another weird case. The time to confirm 'fiat sent' expired. Yet no dispute # was opened. Hint: a seller-scammer could persuade a buyer to not click "fiat # sent", we assume this is a dispute case by default. cls.open_dispute(order) return True def kick_taker(order): ''' The taker did not lock the taker_bond. Now he has to go''' # Add a time out to the taker profile = order.taker.profile profile.penalty_expiration = timezone.now() + timedelta(seconds=PENALTY_TIMEOUT) profile.save() # Make order public again order.status = Order.Status.PUB order.taker = None order.taker_bond = None order.expires_at = order.created_at + timedelta(seconds=Order.t_to_expire[Order.Status.PUB]) order.save() return True @classmethod def open_dispute(cls, order, user=None): # 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: cls.settle_escrow(order) order.is_disputed = True order.status = Order.Status.DIS order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.DIS]) order.save() # User could be None if a dispute is open automatically due to weird expiration. if not user == None: profile = user.profile profile.num_disputes = profile.num_disputes + 1 profile.orders_disputes_started = list(profile.orders_disputes_started).append(str(order.id)) profile.save() return True, None def dispute_statement(order, user, statement): ''' Updates the dispute statements in DB''' if not order.status == Order.Status.DIS: return False, {'bad_request':'Only orders in dispute accept a dispute statements'} if len(statement) > 5000: return False, {'bad_statement':'The statement is longer than 5000 characters'} if order.maker == user: order.maker_statement = statement else: order.taker_statement = statement # If both statements are in, move status to wait for dispute resolution if order.maker_statement != None and order.taker_statement != None: order.status = Order.Status.WFR order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.WFR]) order.save() return True, None @classmethod def buyer_invoice_amount(cls, order, user): ''' Computes buyer invoice amount. Uses order.last_satoshis, that is the final trade amount set at Taker Bond time''' if cls.is_buyer(order, user): invoice_amount = int(order.last_satoshis * (1-FEE)) # Trading FEE is charged here. return True, {'invoice_amount': invoice_amount} @classmethod def update_invoice(cls, order, user, invoice): # only the buyer can post a buyer invoice if not cls.is_buyer(order, user): return False, {'bad_request':'Only the buyer of this order can provide a buyer invoice.'} if not order.taker_bond: return False, {'bad_request':'Wait for your order to be taken.'} if not (order.taker_bond.status == order.maker_bond.status == LNPayment.Status.LOCKED): return False, {'bad_request':'You cannot a invoice while bonds are not posted.'} num_satoshis = cls.buyer_invoice_amount(order, user)[1]['invoice_amount'] buyer_invoice = LNNode.validate_ln_invoice(invoice, num_satoshis) if not buyer_invoice['valid']: return False, buyer_invoice['context'] order.buyer_invoice, _ = LNPayment.objects.update_or_create( concept = LNPayment.Concepts.PAYBUYER, type = LNPayment.Types.NORM, sender = User.objects.get(username=ESCROW_USERNAME), receiver= user, # if there is a LNPayment matching these above, it updates that one with defaults below. defaults={ 'invoice' : invoice, 'status' : LNPayment.Status.VALIDI, 'num_satoshis' : num_satoshis, 'description' : buyer_invoice['description'], 'payment_hash' : buyer_invoice['payment_hash'], 'created_at' : buyer_invoice['created_at'], 'expires_at' : buyer_invoice['expires_at']} ) # If the order status is 'Waiting for invoice'. Move forward to 'chat' if order.status == Order.Status.WFI: order.status = Order.Status.CHA order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA]) # If the order status is 'Waiting for both'. Move forward to 'waiting for escrow' if order.status == Order.Status.WF2: # If the escrow is lock move to Chat. if order.trade_escrow.status == LNPayment.Status.LOCKED: order.status = Order.Status.CHA order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA]) else: order.status = Order.Status.WFE order.save() return True, None def add_profile_rating(profile, rating): ''' 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 latest_ratings = profile.latest_ratings if latest_ratings == None: profile.latest_ratings = [rating] profile.avg_rating = rating else: latest_ratings = ast.literal_eval(latest_ratings) latest_ratings.append(rating) profile.latest_ratings = 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() def is_penalized(user): ''' Checks if a user that is not participant of orders has a limit on taking or making a order''' if user.profile.penalty_expiration: if user.profile.penalty_expiration > timezone.now(): time_out = (user.profile.penalty_expiration - timezone.now()).seconds return True, time_out return False, None @classmethod def cancel_order(cls, order, user, state=None): # 1) When maker cancels before bond '''The order never shows up on the book and order status becomes "cancelled". That's it.''' if order.status == Order.Status.WFB and order.maker == user: order.maker = None order.status = Order.Status.UCA order.save() return True, None # 2) When maker cancels after bond '''The order dissapears from book and goes to cancelled. Maker is charged the bond to prevent DDOS on the LN node and order book. TODO Only charge a small part of the bond (requires maker submitting an invoice)''' elif order.status == Order.Status.PUB and order.maker == user: #Settle the maker bond (Maker loses the bond for cancelling public order) if cls.settle_bond(order.maker_bond): order.maker = None order.status = Order.Status.UCA order.save() return True, None # 3) When taker cancels before bond ''' The order goes back to the book as public. LNPayment "order.taker_bond" is deleted() ''' elif order.status == Order.Status.TAK and order.taker == user: # adds a timeout penalty cls.cancel_bond(order.taker_bond) cls.kick_taker(order) return True, None # 4) When taker or maker cancel after bond (before escrow) '''The order goes into cancelled status if maker cancels. The order goes into the public book if taker cancels. In both cases there is a small fee.''' # 4.a) When maker cancel after bond (before escrow) '''The order into cancelled status if maker cancels.''' elif order.status > Order.Status.PUB and order.status < Order.Status.CHA and order.maker == user: #Settle the maker bond (Maker loses the bond for canceling an ongoing trade) valid = cls.settle_bond(order.maker_bond) if valid: order.maker = None order.status = Order.Status.UCA order.save() return True, None # 4.b) When taker cancel after bond (before escrow) '''The order into cancelled status if maker cancels.''' elif order.status > Order.Status.TAK and order.status < Order.Status.CHA and order.taker == user: # Settle the maker bond (Maker loses the bond for canceling an ongoing trade) valid = cls.settle_bond(order.taker_bond) if valid: order.taker = None order.status = Order.Status.PUB # order.taker_bond = None # TODO fix this, it overrides the information about the settled taker bond. Might make admin tasks hard. order.save() return True, None # 5) When trade collateral has been posted (after escrow) '''Always goes to cancelled status. Collaboration is needed. When a user asks for cancel, 'order.is_pending_cancel' goes True. When the second user asks for cancel. Order is totally cancelled. Has a small cost for both parties to prevent node DDOS.''' else: 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 def is_maker_bond_locked(cls, order): if LNNode.validate_hold_invoice_locked(order.maker_bond.payment_hash): order.maker_bond.status = LNPayment.Status.LOCKED order.maker_bond.save() cls.publish_order(order) return True return False @classmethod def gen_maker_hold_invoice(cls, order, user): # Do not gen and cancel if order is older than expiry time if order.expires_at < timezone.now(): cls.order_expires(order) return False, {'bad_request':'Invoice expired. You did not confirm publishing the order in time. Make a new order.'} # Return the previous invoice if there was one and is still unpaid if order.maker_bond: if cls.is_maker_bond_locked(order): return False, None elif order.maker_bond.status == LNPayment.Status.INVGEN: return True, {'bond_invoice':order.maker_bond.invoice,'bond_satoshis':order.maker_bond.num_satoshis} # If there was no maker_bond object yet, generates one order.last_satoshis = cls.satoshis_now(order) bond_satoshis = int(order.last_satoshis * BOND_SIZE) description = f"RoboSats - Publishing '{str(order)}' - This is a maker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel." # Gen hold Invoice hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) order.maker_bond = LNPayment.objects.create( concept = LNPayment.Concepts.MAKEBOND, type = LNPayment.Types.HOLD, sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), invoice = hold_payment['invoice'], preimage = hold_payment['preimage'], status = LNPayment.Status.INVGEN, num_satoshis = bond_satoshis, description = description, payment_hash = hold_payment['payment_hash'], created_at = hold_payment['created_at'], expires_at = hold_payment['expires_at']) order.save() return True, {'bond_invoice':hold_payment['invoice'], 'bond_satoshis':bond_satoshis} @classmethod def finalize_contract(cls, order): ''' When the taker locks the taker_bond the contract is final ''' # 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) order.last_satoshis = cls.satoshis_now(order) order.taker_bond.status = LNPayment.Status.LOCKED order.taker_bond.save() # Both users profiles are added one more contract // Unsafe can add more than once. order.maker.profile.total_contracts += 1 order.taker.profile.total_contracts += 1 order.maker.profile.save() order.taker.profile.save() # Log a market tick MarketTick.log_a_tick(order) # With the bond confirmation the order is extended 'public_order_duration' hours order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.WF2]) order.status = Order.Status.WF2 order.save() 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 @classmethod def gen_taker_hold_invoice(cls, order, user): # Do not gen and kick out the taker if order is older than expiry time if order.expires_at < timezone.now(): cls.cancel_bond(order.taker_bond) cls.kick_taker(order) return False, {'bad_request':'Invoice expired. You did not confirm taking the order in time.'} # Do not gen if a taker invoice exist. Do not return if it is already locked. Return the old one if still waiting. if order.taker_bond: # Check if status is INVGEN and still not expired if cls.is_taker_bond_locked(order): return False, None elif order.taker_bond.status == LNPayment.Status.INVGEN: return True, {'bond_invoice':order.taker_bond.invoice,'bond_satoshis':order.taker_bond.num_satoshis} # If there was no taker_bond object yet, generates one order.last_satoshis = cls.satoshis_now(order) bond_satoshis = int(order.last_satoshis * BOND_SIZE) pos_text = 'Buying' if cls.is_buyer(order, user) else 'Selling' description = (f"RoboSats - Taking 'Order {order.id}' {pos_text} BTC for {str(float(order.amount)) + Currency.currency_dict[str(order.currency.currency)]}" + " - This is a taker bond, it will freeze in your wallet temporarily and automatically return. It will be charged if you cheat or cancel.") # Gen hold Invoice hold_payment = LNNode.gen_hold_invoice(bond_satoshis, description, BOND_EXPIRY*3600) order.taker_bond = LNPayment.objects.create( concept = LNPayment.Concepts.TAKEBOND, type = LNPayment.Types.HOLD, sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), invoice = hold_payment['invoice'], preimage = hold_payment['preimage'], status = LNPayment.Status.INVGEN, num_satoshis = bond_satoshis, description = description, payment_hash = hold_payment['payment_hash'], created_at = hold_payment['created_at'], expires_at = hold_payment['expires_at']) order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.TAK]) order.save() return True, {'bond_invoice': hold_payment['invoice'], 'bond_satoshis': bond_satoshis} @classmethod def is_trade_escrow_locked(cls, order): if LNNode.validate_hold_invoice_locked(order.trade_escrow.payment_hash): order.trade_escrow.status = LNPayment.Status.LOCKED order.trade_escrow.save() # If status is 'Waiting for both' move to Waiting for invoice if order.status == Order.Status.WF2: order.status = Order.Status.WFI # If status is 'Waiting for invoice' move to Chat elif order.status == Order.Status.WFE: order.status = Order.Status.CHA order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.CHA]) order.save() return True return False @classmethod def gen_escrow_hold_invoice(cls, order, user): # Do not generate if escrow deposit time has expired if order.expires_at < timezone.now(): cls.cancel_order(order,user) return False, {'bad_request':'Invoice expired. You did not send the escrow in time.'} # Do not gen if an escrow invoice exist. Do not return if it is already locked. Return the old one if still waiting. if order.trade_escrow: # Check if status is INVGEN and still not expired if cls.is_trade_escrow_locked(order): return False, None elif order.trade_escrow.status == LNPayment.Status.INVGEN: return True, {'escrow_invoice':order.trade_escrow.invoice, 'escrow_satoshis':order.trade_escrow.num_satoshis} # If there was no taker_bond object yet, generates one escrow_satoshis = order.last_satoshis # Amount was fixed when taker bond was locked description = f"RoboSats - Escrow amount for '{str(order)}' - The escrow will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment." # Gen hold Invoice hold_payment = LNNode.gen_hold_invoice(escrow_satoshis, description, ESCROW_EXPIRY*3600) order.trade_escrow = LNPayment.objects.create( concept = LNPayment.Concepts.TRESCROW, type = LNPayment.Types.HOLD, sender = user, receiver = User.objects.get(username=ESCROW_USERNAME), invoice = hold_payment['invoice'], preimage = hold_payment['preimage'], status = LNPayment.Status.INVGEN, num_satoshis = escrow_satoshis, description = description, payment_hash = hold_payment['payment_hash'], created_at = hold_payment['created_at'], expires_at = hold_payment['expires_at']) order.save() return True, {'escrow_invoice':hold_payment['invoice'],'escrow_satoshis': escrow_satoshis} def settle_escrow(order): ''' Settles the trade escrow hold invoice''' # TODO ERROR HANDLING if LNNode.settle_hold_invoice(order.trade_escrow.preimage): order.trade_escrow.status = LNPayment.Status.SETLED order.trade_escrow.save() return True def settle_bond(bond): ''' Settles the bond hold invoice''' # TODO ERROR HANDLING if LNNode.settle_hold_invoice(bond.preimage): bond.status = LNPayment.Status.SETLED bond.save() return True def return_escrow(order): '''returns the trade escrow''' if LNNode.cancel_return_hold_invoice(order.trade_escrow.payment_hash): order.trade_escrow.status = LNPayment.Status.RETNED 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): '''returns a bond''' if bond == None: return try: LNNode.cancel_return_hold_invoice(bond.payment_hash) bond.status = LNPayment.Status.RETNED return True except Exception as e: if 'invoice already settled' in str(e): bond.status = LNPayment.Status.SETLED return True else: raise e def cancel_bond(bond): '''cancel a bond''' # Same as return bond, but used when the invoice was never LOCKED if bond == None: return True try: LNNode.cancel_return_hold_invoice(bond.payment_hash) bond.status = LNPayment.Status.CANCEL return True except Exception as e: if 'invoice already settled' in str(e): bond.status = LNPayment.Status.SETLED return True else: raise e def pay_buyer_invoice(order): ''' Pay buyer invoice''' suceeded, context = follow_send_payment(order.buyer_invoice) return suceeded, context @classmethod def confirm_fiat(cls, order, user): ''' If Order is in the CHAT states: If user is buyer: mark FIAT SENT and settle escrow! If User is the seller and FIAT is SENT: Pay buyer invoice!''' if order.status == Order.Status.CHA or order.status == Order.Status.FSE: # TODO Alternatively, if all collateral is locked? test out # If buyer, settle escrow and mark fiat sent if cls.is_buyer(order, user): if cls.settle_escrow(order): ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!! order.trade_escrow.status = LNPayment.Status.SETLED order.status = Order.Status.FSE order.is_fiat_sent = True # If seller and fiat sent, pay buyer invoice elif cls.is_seller(order, user): if not order.is_fiat_sent: return False, {'bad_request':'You cannot confirm to have received the fiat before it is confirmed to be sent by the buyer.'} # Make sure the trade escrow is at least as big as the buyer invoice if order.trade_escrow.num_satoshis <= order.buyer_invoice.num_satoshis: return False, {'bad_request':'Woah, something broke badly. Report in the public channels, or open a Github Issue.'} # Double check the escrow is settled. if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): is_payed, context = cls.pay_buyer_invoice(order) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!! if is_payed: order.status = Order.Status.SUC order.buyer_invoice.status = LNPayment.Status.SUCCED order.expires_at = timezone.now() + timedelta(seconds=Order.t_to_expire[Order.Status.SUC]) order.save() # RETURN THE BONDS cls.return_bond(order.taker_bond) cls.return_bond(order.maker_bond) else: # error handling here pass else: return False, {'bad_request':'You cannot confirm the fiat payment at this stage'} order.save() return True, None @classmethod def rate_counterparty(cls, order, user, rating): # If the trade is finished if order.status > Order.Status.PAY: # if maker, rates taker if order.maker == user and order.maker_rated == False: cls.add_profile_rating(order.taker.profile, rating) order.maker_rated = True order.save() # if taker, rates maker if order.taker == user and order.taker_rated == False: cls.add_profile_rating(order.maker.profile, rating) order.taker_rated = True order.save() else: return False, {'bad_request':'You cannot rate your counterparty yet.'} return True, None