From efed6b3c0a06ea5396f279653075a09c1a5ad0f4 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi Date: Thu, 16 Jun 2022 08:31:30 -0700 Subject: [PATCH] Pay buyer onchain-tx --- .env-sample | 4 +- api/lightning/node.py | 27 +++++++++++++- api/logics.py | 57 ++++++++++++++++++++--------- api/models.py | 2 +- api/views.py | 15 +++++++- frontend/src/components/TradeBox.js | 25 +++++++++++-- 6 files changed, 104 insertions(+), 26 deletions(-) diff --git a/.env-sample b/.env-sample index ee4b7a66..70a6db73 100644 --- a/.env-sample +++ b/.env-sample @@ -99,9 +99,11 @@ MIN_FLAT_ROUTING_FEE_LIMIT = 10 MIN_FLAT_ROUTING_FEE_LIMIT_REWARD = 2 # Routing timeouts REWARDS_TIMEOUT_SECONDS = 60 -PAYOUT_TIMEOUT_SECONDS = 60 +PAYOUT_TIMEOUT_SECONDS = 90 # REVERSE SUBMARINE SWAP PAYOUTS +# Disable on-the-fly swaps feature +DISABLE_ONCHAIN = False # Shape of fee to available liquidity curve. Either "linear" or "exponential" SWAP_FEE_SHAPE = 'exponential' # EXPONENTIAL. fee (%) = MIN_SWAP_FEE + (MAX_SWAP_FEE - MIN_SWAP_FEE) * e ^ (-LAMBDA * onchain_liquidity_fraction) diff --git a/api/lightning/node.py b/api/lightning/node.py index ecc555a0..96ad7e69 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -1,4 +1,6 @@ import grpc, os, hashlib, secrets, ring + +from robosats.api.models import OnchainPayment from . import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub from . import router_pb2 as routerrpc, router_pb2_grpc as routerstub @@ -70,7 +72,7 @@ class LNNode: def estimate_fee(cls, amount_sats, target_conf=2, min_confs=1): """Returns estimated fee for onchain payouts""" - # We assume segwit. Use robosats donation address (shortcut so there is no need to have user input) + # We assume segwit. Use robosats donation address as shortcut so there is no need of user inputs request = lnrpc.EstimateFeeRequest(AddrToAmount={'bc1q3cpp7ww92n6zp04hv40kd3eyy5avgughx6xqnx':amount_sats}, target_conf=target_conf, min_confs=min_confs, @@ -112,6 +114,29 @@ class LNNode: 'unsettled_local_balance': response.unsettled_local_balance.sat, 'unsettled_remote_balance': response.unsettled_remote_balance.sat} + @classmethod + def pay_onchain(cls, onchainpayment): + """Send onchain transaction for buyer payouts""" + + if bool(config("DISABLE_ONCHAIN")): + return False + + request = lnrpc.SendCoinsRequest(addr=onchainpayment.address, + amount=int(onchainpayment.sent_satoshis), + sat_per_vbyte=int(onchainpayment.mining_fee_rate), + label=str("Payout order #" + str(onchainpayment.order_paid_TX.id)), + spend_unconfirmed=True) + response = cls.lightningstub.SendCoins(request, + metadata=[("macaroon", + MACAROON.hex())]) + + print(response) + onchainpayment.txid = response.txid + onchainpayment.status = OnchainPayment.Status.MEMPO + onchainpayment.save() + + return True + @classmethod def cancel_return_hold_invoice(cls, payment_hash): """Cancels or returns a hold invoice""" diff --git a/api/logics.py b/api/logics.py index 9896cfa1..6bd29e8b 100644 --- a/api/logics.py +++ b/api/logics.py @@ -589,12 +589,17 @@ class Logics: context["swap_failure_reason"] = "Order amount is too small to be eligible for a swap" return True, context + if not bool(config("DISABLE_ONCHAIN")): + context["swap_allowed"] = False + context["swap_failure_reason"] = "On-the-fly submarine swaps are dissabled" + return True, context + if order.payout_tx == None: # Creates the OnchainPayment object and checks node balance valid = cls.create_onchain_payment(order, user, preliminary_amount=context["invoice_amount"]) if not valid: context["swap_allowed"] = False - context["swap_failure_reason"] = "Not enough onchain liquidity available to offer swaps" + context["swap_failure_reason"] = "Not enough onchain liquidity available to offer a SWAP" return True, context context["swap_allowed"] = True @@ -1246,7 +1251,6 @@ class Logics: 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() @@ -1254,7 +1258,6 @@ class Logics: 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() @@ -1310,6 +1313,30 @@ class Logics: else: raise e + @classmethod + def pay_buyer(cls, order): + '''Pays buyer invoice or onchain address''' + + # Pay to buyer invoice + if not order.is_swap: + ##### Background process "follow_invoices" will try to pay this invoice until success + order.status = Order.Status.PAY + order.payout.status = LNPayment.Status.FLIGHT + order.payout.save() + order.save() + send_message.delay(order.id,'trade_successful') + return True + + # Pay onchain to address + else: + valid = LNNode.pay_onchain(order.payout_tx) + if valid: + order.status = Order.Status.SUC + order.save() + send_message.delay(order.id,'trade_successful') + return True + return False + @classmethod def confirm_fiat(cls, order, user): """If Order is in the CHAT states: @@ -1318,7 +1345,7 @@ class Logics: 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): @@ -1334,30 +1361,24 @@ class Logics: } # Make sure the trade escrow is at least as big as the buyer invoice - if order.trade_escrow.num_satoshis <= order.payout.num_satoshis: + num_satoshis = order.payout_tx.num_satoshis if order.is_swap else order.payout.num_satoshis + if order.trade_escrow.num_satoshis <= num_satoshis: return False, { "bad_request": "Woah, something broke badly. Report in the public channels, or open a Github Issue." } - - if cls.settle_escrow( - order - ): ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!! + + # !!! KEY LINE - SETTLES THE TRADE ESCROW !!! + if cls.settle_escrow(order): order.trade_escrow.status = LNPayment.Status.SETLED # Double check the escrow is settled. - if LNNode.double_check_htlc_is_settled( - order.trade_escrow.payment_hash): - # RETURN THE BONDS // Probably best also do it even if payment failed + if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash): + # RETURN THE BONDS cls.return_bond(order.taker_bond) cls.return_bond(order.maker_bond) ##### !!! KEY LINE - PAYS THE BUYER INVOICE !!! - ##### Background process "follow_invoices" will try to pay this invoice until success - order.status = Order.Status.PAY - order.payout.status = LNPayment.Status.FLIGHT - order.payout.save() - order.save() - send_message.delay(order.id,'trade_successful') + cls.pay_buyer(order) # Add referral rewards (safe) try: diff --git a/api/models.py b/api/models.py index f7d02e2d..c1fde799 100644 --- a/api/models.py +++ b/api/models.py @@ -220,7 +220,7 @@ class OnchainPayment(models.Model): null=False, blank=False) mining_fee_rate = models.DecimalField(max_digits=6, - decimal_places=3, + decimal_places=3, default=1.05, null=False, blank=False) diff --git a/api/views.py b/api/views.py index 78ba2049..1365e60e 100644 --- a/api/views.py +++ b/api/views.py @@ -12,7 +12,7 @@ from django.views.decorators.csrf import csrf_exempt from django.contrib.auth.models import User from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer, UserGenSerializer -from api.models import LNPayment, MarketTick, Order, Currency, Profile +from api.models import LNPayment, MarketTick, OnchainPayment, Order, Currency, Profile from control.models import AccountingDay from api.logics import Logics from api.messages import Telegram @@ -399,6 +399,19 @@ class OrderView(viewsets.ViewSet): if order.status == Order.Status.EXP: data["expiry_reason"] = order.expiry_reason data["expiry_message"] = Order.ExpiryReasons(order.expiry_reason).label + + # If status is 'Succes' add final stats and txid if it is a swap + if order.status == Order.Status.SUC: + # TODO: add summary of order for buyer/sellers: sats in/out, fee paid, total time? etc + # If buyer and is a swap, add TXID + if Logics.is_buyer(order,request.user): + if order.is_swap: + data["num_satoshis"] = order.payout_tx.num_satoshis + data["sent_satoshis"] = order.payout_tx.sent_satoshis + if order.payout_tx.status in [OnchainPayment.Status.MEMPO, OnchainPayment.Status.CONFI]: + data["txid"] = order.payout_tx.txid + + return Response(data, status.HTTP_200_OK) diff --git a/frontend/src/components/TradeBox.js b/frontend/src/components/TradeBox.js index 68aa7bc5..6b6781be 100644 --- a/frontend/src/components/TradeBox.js +++ b/frontend/src/components/TradeBox.js @@ -1228,19 +1228,36 @@ handleRatingRobosatsChange=(e)=>{ {this.state.rating_platform==5 ? -

{t("Thank you! RoboSats loves you too ❤️")}

-

{t("RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!")}

+ {t("Thank you! RoboSats loves you too ❤️")} +
+ + {t("RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!")}
: null} {this.state.rating_platform!=5 & this.state.rating_platform!=null ? -

{t("Thank you for using Robosats!")}

-

Let us know how the platform could improve (Telegram / Github)

+ {t("Thank you for using Robosats!")} +
+ + Let us know how the platform could improve (Telegram / Github)
: null} + + {/* SHOW TXID IF USER RECEIVES ONCHAIN */} + {this.props.data.txid ? + + + {t("Your TXID:")} + + + {this.props.data.txid} + + + : null} +