From 0a620901a7ff2ea6151813c211d3fd771339ea79 Mon Sep 17 00:00:00 2001 From: Reckless_Satoshi <90936742+Reckless-Satoshi@users.noreply.github.com> Date: Tue, 16 May 2023 17:12:15 +0000 Subject: [PATCH] Add keysend devfund donations functionality (#589) --- .env-sample | 10 ++ api/admin.py | 2 + api/lightning/node.py | 93 +++++++++++++++++-- api/logics.py | 18 ++-- ...onated_alter_lnpayment_concept_and_more.py | 47 ++++++++++ api/models/ln_payment.py | 9 ++ api/tasks.py | 52 +++++++++++ robosats/celery/__init__.py | 4 - scripts/generate_grpc.sh | 6 ++ 9 files changed, 221 insertions(+), 20 deletions(-) create mode 100644 api/migrations/0037_lnpayment_order_donated_alter_lnpayment_concept_and_more.py diff --git a/.env-sample b/.env-sample index 3ec2518d..71901bb6 100644 --- a/.env-sample +++ b/.env-sample @@ -73,6 +73,13 @@ FEE = 0.002 # Shall incentivize order making MAKER_FEE_SPLIT=0.125 +# Robosats Development Fund donation as fraction. 0.2 = 20% of successful orders proceeds are donated via keysend. +# Donations to the devfund are important for the sustainabilty of the project, however, these are totally optional (you +# can run a coordinator without donating devfund!). Coordinators with higher devfund donations % will be more prominently +# displayed (and have special badges), while coordinators that do not donate might eventually lose frontend/client support. +# Leaving the default value (20%) will grant the DevFund contributor badge. +DEVFUND = 0.2 + # Bond size as percentage (%) DEFAULT_BOND_SIZE = 3 MIN_BOND_SIZE = 1 @@ -121,6 +128,9 @@ REWARDS_TIMEOUT_SECONDS = 30 PAYOUT_TIMEOUT_SECONDS = 90 DEBUG_PERMISSIONED_PAYOUTS = False +# Allow self keysend on keysend function (set true to debug keysend functionality) +ALLOW_SELF_KEYSEND = False + # REVERSE SUBMARINE SWAP PAYOUTS # Disable on-the-fly swaps feature DISABLE_ONCHAIN = False diff --git a/api/admin.py b/api/admin.py index 7158b8d1..228fc4c9 100644 --- a/api/admin.py +++ b/api/admin.py @@ -303,6 +303,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): "order_taken_link", "order_escrow_link", "order_paid_LN_link", + "order_donated_link", ) list_display_links = ("hash", "concept") change_links = ( @@ -312,6 +313,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): "order_taken", "order_escrow", "order_paid_LN", + "order_donated", ) raw_id_fields = ( "receiver", diff --git a/api/lightning/node.py b/api/lightning/node.py index 05d4c544..7c8fc118 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -1,6 +1,7 @@ import hashlib import os import secrets +import struct import time from base64 import b64decode from datetime import datetime, timedelta @@ -16,8 +17,10 @@ from . import lightning_pb2 as lnrpc from . import lightning_pb2_grpc as lightningstub from . import router_pb2 as routerrpc from . import router_pb2_grpc as routerstub +from . import signer_pb2 as signerrpc +from . import signer_pb2_grpc as signerstub from . import verrpc_pb2 as verrpc -from . import verrpc_pb2_grpc as verrpcstub +from . import verrpc_pb2_grpc as verstub ####### # Works with LND (c-lightning in the future for multi-vendor resilience) @@ -57,12 +60,8 @@ class LNNode: lightningstub = lightningstub.LightningStub(channel) invoicesstub = invoicesstub.InvoicesStub(channel) routerstub = routerstub.RouterStub(channel) - verrpcstub = verrpcstub.VersionerStub(channel) - - lnrpc = lnrpc - invoicesrpc = invoicesrpc - routerrpc = routerrpc - verrpc = verrpc + signerstub = signerstub.SignerStub(channel) + verstub = verstub.VersionerStub(channel) payment_failure_context = { 0: "Payment isn't failed (yet)", @@ -77,7 +76,7 @@ class LNNode: def get_version(cls): try: request = verrpc.VersionRequest() - response = cls.verrpcstub.GetVersion(request) + response = cls.verstub.GetVersion(request) return "v" + response.version except Exception as e: print(e) @@ -466,7 +465,7 @@ class LNNode: hash = lnpayment.payment_hash - request = cls.routerrpc.SendPaymentRequest( + request = routerrpc.SendPaymentRequest( payment_request=lnpayment.invoice, fee_limit_sat=fee_limit_sat, timeout_seconds=timeout_seconds, @@ -616,6 +615,82 @@ class LNNode: else: print(str(e)) + @classmethod + def send_keysend( + cls, target_pubkey, message, num_satoshis, routing_budget_sats, timeout, sign + ): + # Thank you @cryptosharks131 / lndg for the inspiration + # Source https://github.com/cryptosharks131/lndg/blob/master/keysend.py + + from api.models import LNPayment + + ALLOW_SELF_KEYSEND = config("ALLOW_SELF_KEYSEND", cast=bool, default=False) + keysend_payment = {} + keysend_payment["created_at"] = timezone.now() + keysend_payment["expires_at"] = timezone.now() + try: + secret = secrets.token_bytes(32) + hashed_secret = hashlib.sha256(secret).hexdigest() + custom_records = [ + (5482373484, secret), + ] + keysend_payment["preimage"] = secret.hex() + keysend_payment["payment_hash"] = hashed_secret + + msg = str(message) + + if len(msg) > 0: + custom_records.append( + (34349334, bytes.fromhex(msg.encode("utf-8").hex())) + ) + if sign: + self_pubkey = cls.lightningstub.GetInfo( + lnrpc.GetInfoRequest() + ).identity_pubkey + timestamp = struct.pack(">i", int(time.time())) + signature = cls.signerstub.SignMessage( + signerrpc.SignMessageReq( + msg=( + bytes.fromhex(self_pubkey) + + bytes.fromhex(target_pubkey) + + timestamp + + bytes.fromhex(msg.encode("utf-8").hex()) + ), + key_loc=signerrpc.KeyLocator(key_family=6, key_index=0), + ) + ).signature + custom_records.append((34349337, signature)) + custom_records.append((34349339, bytes.fromhex(self_pubkey))) + custom_records.append((34349343, timestamp)) + + request = routerrpc.SendPaymentRequest( + dest=bytes.fromhex(target_pubkey), + dest_custom_records=custom_records, + fee_limit_sat=routing_budget_sats, + timeout_seconds=timeout, + amt=num_satoshis, + payment_hash=bytes.fromhex(hashed_secret), + allow_self_payment=ALLOW_SELF_KEYSEND, + ) + for response in cls.routerstub.SendPaymentV2(request): + if response.status == 1: + keysend_payment["status"] = LNPayment.Status.FLIGHT + if response.status == 2: + keysend_payment["fee"] = float(response.fee_msat) / 1000 + keysend_payment["status"] = LNPayment.Status.SUCCED + if response.status == 3: + keysend_payment["status"] = LNPayment.Status.FAILRO + keysend_payment["failure_reason"] = response.failure_reason + if response.status == 0: + print("Unknown Error") + except Exception as e: + if "self-payments not allowed" in str(e): + print("Self keysend is not allowed") + else: + print("Error while sending keysend payment! Error: " + str(e)) + + return True, keysend_payment + @classmethod def double_check_htlc_is_settled(cls, payment_hash): """Just as it sounds. Better safe than sorry!""" diff --git a/api/logics.py b/api/logics.py index ddfc1fc2..0ecafe0f 100644 --- a/api/logics.py +++ b/api/logics.py @@ -8,7 +8,7 @@ from django.utils import timezone from api.lightning.node import LNNode from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order -from api.tasks import send_notification +from api.tasks import send_devfund_donation, send_notification from api.utils import validate_onchain_address from chat.models import Message @@ -1066,7 +1066,6 @@ class Logics: @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) @@ -1570,9 +1569,10 @@ class Logics: slashed_robot.earned_rewards += slashed_return slashed_robot.save(update_fields=["earned_rewards"]) - proceeds = int(slashed_satoshis * (1 - reward_fraction)) - order.proceeds += proceeds + new_proceeds = int(slashed_satoshis * (1 - reward_fraction)) + order.proceeds += new_proceeds order.save(update_fields=["proceeds"]) + send_devfund_donation.delay(order.id, new_proceeds, "slashed bond") return @@ -1645,13 +1645,17 @@ class Logics: """ if order.is_swap: - payout_sats = order.payout_tx.sent_satoshis + order.payout_tx.mining_fee - order.proceeds += int(order.trade_escrow.num_satoshis - payout_sats) + payout_sats = ( + order.payout_tx.sent_satoshis + order.payout_tx.mining_fee_sats + ) + new_proceeds = int(order.trade_escrow.num_satoshis - payout_sats) else: payout_sats = order.payout.num_satoshis + order.payout.fee - order.proceeds += int(order.trade_escrow.num_satoshis - payout_sats) + new_proceeds = int(order.trade_escrow.num_satoshis - payout_sats) + order.proceeds += new_proceeds order.save(update_fields=["proceeds"]) + send_devfund_donation.delay(order.id, new_proceeds, "successful order") @classmethod def summarize_trade(cls, order, user): diff --git a/api/migrations/0037_lnpayment_order_donated_alter_lnpayment_concept_and_more.py b/api/migrations/0037_lnpayment_order_donated_alter_lnpayment_concept_and_more.py new file mode 100644 index 00000000..6abc9811 --- /dev/null +++ b/api/migrations/0037_lnpayment_order_donated_alter_lnpayment_concept_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.2.1 on 2023-05-16 17:08 + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("api", "0001_squashed_0036_remove_order_maker_last_seen_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="lnpayment", + name="order_donated", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="order_donated", + to="api.order", + ), + ), + migrations.AlterField( + model_name="lnpayment", + name="concept", + field=models.PositiveSmallIntegerField( + choices=[ + (0, "Maker bond"), + (1, "Taker bond"), + (2, "Trade escrow"), + (3, "Payment to buyer"), + (4, "Withdraw rewards"), + (5, "Devfund donation"), + ], + default=0, + ), + ), + migrations.AlterField( + model_name="lnpayment", + name="type", + field=models.PositiveSmallIntegerField( + choices=[(0, "Regular invoice"), (1, "hold invoice"), (2, "Keysend")], + default=1, + ), + ), + ] diff --git a/api/models/ln_payment.py b/api/models/ln_payment.py index 4d13feb6..77ea112a 100644 --- a/api/models/ln_payment.py +++ b/api/models/ln_payment.py @@ -9,6 +9,7 @@ class LNPayment(models.Model): class Types(models.IntegerChoices): NORM = 0, "Regular invoice" HOLD = 1, "hold invoice" + KEYS = 2, "Keysend" class Concepts(models.IntegerChoices): MAKEBOND = 0, "Maker bond" @@ -16,6 +17,7 @@ class LNPayment(models.Model): TRESCROW = 2, "Trade escrow" PAYBUYER = 3, "Payment to buyer" WITHREWA = 4, "Withdraw rewards" + DEVDONAT = 5, "Devfund donation" class Status(models.IntegerChoices): INVGEN = 0, "Generated" @@ -116,6 +118,13 @@ class LNPayment(models.Model): null=True, default=None, ) + order_donated = models.ForeignKey( + "api.Order", + related_name="order_donated", + null=True, + on_delete=models.SET_NULL, + default=None, + ) def __str__(self): return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}" diff --git a/api/tasks.py b/api/tasks.py index 1e5ce899..f03977a7 100644 --- a/api/tasks.py +++ b/api/tasks.py @@ -88,6 +88,58 @@ def follow_send_payment(hash): return results +@shared_task(name="send_devfund_donation", time_limit=300, soft_time_limit=295) +def send_devfund_donation(order_id, proceeds, reason): + """Sends a fraction of order.proceeds via keysend as + donation to the RoboSats Open Source project devfund. + """ + from decouple import config + from django.contrib.auth.models import User + + from api.lightning.node import LNNode + from api.models import LNPayment, Order + + if config("NETWORK", cast=str) == "testnet": + target_pubkey = ( + "03ecb271b3e2e36f2b91c92c65bab665e5165f8cdfdada1b5f46cfdd3248c87fd6" + ) + else: + target_pubkey = ( + "0282eb467bc073833a039940392592bf10cf338a830ba4e392c1667d7697654c7e" + ) + + order = Order.objects.get(id=order_id) + coordinator_alias = config("COORDINATOR_ALIAS", cast=str, default="NoAlias") + donation_fraction = max(0.05, config("DEVFUND", cast=float, default=0.2)) + message = f"Devfund donation; {coordinator_alias}; {order}; {donation_fraction}; {reason};" + num_satoshis = int(proceeds * donation_fraction) + routing_budget_sats = int(max(5, num_satoshis * 0.000_1)) + timeout = 280 + sign = False + + valid, keysend_payment = LNNode.send_keysend( + target_pubkey, message, num_satoshis, routing_budget_sats, timeout, sign + ) + if not valid: + return False + + LNPayment.objects.create( + concept=LNPayment.Concepts.DEVDONAT, + type=LNPayment.Types.KEYS, + sender=User.objects.get( + username=config("ESCROW_USERNAME", cast=str, default="admin") + ), + invoice=f"Target pubkey: {target_pubkey}; At: {keysend_payment['created_at']}", + routing_budget_sats=routing_budget_sats, + description=message, + num_satoshis=num_satoshis, + order_donated=order, + **keysend_payment, + ) + + return True + + @shared_task(name="payments_cleansing", time_limit=600) def payments_cleansing(): """ diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py index 794ff97c..c1a27eb6 100644 --- a/robosats/celery/__init__.py +++ b/robosats/celery/__init__.py @@ -43,10 +43,6 @@ app.conf.beat_schedule = { "task": "payments_cleansing", "schedule": crontab(hour=0, minute=0), }, - "give-rewards": { # Referral rewards go from 'pending' to 'earned' at midnight - "task": "give_rewards", - "schedule": crontab(hour=0, minute=0), - }, "do-accounting": { # Does accounting for the last day "task": "do_accounting", "schedule": crontab(hour=23, minute=59), diff --git a/scripts/generate_grpc.sh b/scripts/generate_grpc.sh index ff403ad2..ea0cac2f 100755 --- a/scripts/generate_grpc.sh +++ b/scripts/generate_grpc.sh @@ -16,6 +16,10 @@ python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_pyt curl -o router.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/routerrpc/router.proto python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. router.proto +# LND Signer proto +curl -o signer.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/signrpc/signer.proto +python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. signer.proto + # LND Versioner proto curl -o verrpc.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/verrpc/verrpc.proto python3 -m grpc_tools.protoc --proto_path=googleapis:. --python_out=. --grpc_python_out=. verrpc.proto @@ -25,9 +29,11 @@ rm -r googleapis # patch generated files relative imports sed -i 's/^import .*_pb2 as/from . \0/' router_pb2.py +sed -i 's/^import .*_pb2 as/from . \0/' signer_pb2.py sed -i 's/^import .*_pb2 as/from . \0/' invoices_pb2.py sed -i 's/^import .*_pb2 as/from . \0/' verrpc_pb2.py sed -i 's/^import .*_pb2 as/from . \0/' router_pb2_grpc.py +sed -i 's/^import .*_pb2 as/from . \0/' signer_pb2_grpc.py sed -i 's/^import .*_pb2 as/from . \0/' lightning_pb2_grpc.py sed -i 's/^import .*_pb2 as/from . \0/' invoices_pb2_grpc.py sed -i 's/^import .*_pb2 as/from . \0/' verrpc_pb2_grpc.py