Add keysend devfund donations functionality (#589)

This commit is contained in:
Reckless_Satoshi 2023-05-16 17:12:15 +00:00 committed by GitHub
parent b4a54f3b1a
commit 0a620901a7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 221 additions and 20 deletions

View File

@ -73,6 +73,13 @@ FEE = 0.002
# Shall incentivize order making # Shall incentivize order making
MAKER_FEE_SPLIT=0.125 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 (%) # Bond size as percentage (%)
DEFAULT_BOND_SIZE = 3 DEFAULT_BOND_SIZE = 3
MIN_BOND_SIZE = 1 MIN_BOND_SIZE = 1
@ -121,6 +128,9 @@ REWARDS_TIMEOUT_SECONDS = 30
PAYOUT_TIMEOUT_SECONDS = 90 PAYOUT_TIMEOUT_SECONDS = 90
DEBUG_PERMISSIONED_PAYOUTS = False DEBUG_PERMISSIONED_PAYOUTS = False
# Allow self keysend on keysend function (set true to debug keysend functionality)
ALLOW_SELF_KEYSEND = False
# REVERSE SUBMARINE SWAP PAYOUTS # REVERSE SUBMARINE SWAP PAYOUTS
# Disable on-the-fly swaps feature # Disable on-the-fly swaps feature
DISABLE_ONCHAIN = False DISABLE_ONCHAIN = False

View File

@ -303,6 +303,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"order_taken_link", "order_taken_link",
"order_escrow_link", "order_escrow_link",
"order_paid_LN_link", "order_paid_LN_link",
"order_donated_link",
) )
list_display_links = ("hash", "concept") list_display_links = ("hash", "concept")
change_links = ( change_links = (
@ -312,6 +313,7 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"order_taken", "order_taken",
"order_escrow", "order_escrow",
"order_paid_LN", "order_paid_LN",
"order_donated",
) )
raw_id_fields = ( raw_id_fields = (
"receiver", "receiver",

View File

@ -1,6 +1,7 @@
import hashlib import hashlib
import os import os
import secrets import secrets
import struct
import time import time
from base64 import b64decode from base64 import b64decode
from datetime import datetime, timedelta from datetime import datetime, timedelta
@ -16,8 +17,10 @@ from . import lightning_pb2 as lnrpc
from . import lightning_pb2_grpc as lightningstub from . import lightning_pb2_grpc as lightningstub
from . import router_pb2 as routerrpc from . import router_pb2 as routerrpc
from . import router_pb2_grpc as routerstub 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 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) # Works with LND (c-lightning in the future for multi-vendor resilience)
@ -57,12 +60,8 @@ class LNNode:
lightningstub = lightningstub.LightningStub(channel) lightningstub = lightningstub.LightningStub(channel)
invoicesstub = invoicesstub.InvoicesStub(channel) invoicesstub = invoicesstub.InvoicesStub(channel)
routerstub = routerstub.RouterStub(channel) routerstub = routerstub.RouterStub(channel)
verrpcstub = verrpcstub.VersionerStub(channel) signerstub = signerstub.SignerStub(channel)
verstub = verstub.VersionerStub(channel)
lnrpc = lnrpc
invoicesrpc = invoicesrpc
routerrpc = routerrpc
verrpc = verrpc
payment_failure_context = { payment_failure_context = {
0: "Payment isn't failed (yet)", 0: "Payment isn't failed (yet)",
@ -77,7 +76,7 @@ class LNNode:
def get_version(cls): def get_version(cls):
try: try:
request = verrpc.VersionRequest() request = verrpc.VersionRequest()
response = cls.verrpcstub.GetVersion(request) response = cls.verstub.GetVersion(request)
return "v" + response.version return "v" + response.version
except Exception as e: except Exception as e:
print(e) print(e)
@ -466,7 +465,7 @@ class LNNode:
hash = lnpayment.payment_hash hash = lnpayment.payment_hash
request = cls.routerrpc.SendPaymentRequest( request = routerrpc.SendPaymentRequest(
payment_request=lnpayment.invoice, payment_request=lnpayment.invoice,
fee_limit_sat=fee_limit_sat, fee_limit_sat=fee_limit_sat,
timeout_seconds=timeout_seconds, timeout_seconds=timeout_seconds,
@ -616,6 +615,82 @@ class LNNode:
else: else:
print(str(e)) 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 @classmethod
def double_check_htlc_is_settled(cls, payment_hash): def double_check_htlc_is_settled(cls, payment_hash):
"""Just as it sounds. Better safe than sorry!""" """Just as it sounds. Better safe than sorry!"""

View File

@ -8,7 +8,7 @@ from django.utils import timezone
from api.lightning.node import LNNode from api.lightning.node import LNNode
from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order 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 api.utils import validate_onchain_address
from chat.models import Message from chat.models import Message
@ -1066,7 +1066,6 @@ class Logics:
@classmethod @classmethod
def gen_maker_hold_invoice(cls, order, user): def gen_maker_hold_invoice(cls, order, user):
# Do not gen and cancel if order is older than expiry time # Do not gen and cancel if order is older than expiry time
if order.expires_at < timezone.now(): if order.expires_at < timezone.now():
cls.order_expires(order) cls.order_expires(order)
@ -1570,9 +1569,10 @@ class Logics:
slashed_robot.earned_rewards += slashed_return slashed_robot.earned_rewards += slashed_return
slashed_robot.save(update_fields=["earned_rewards"]) slashed_robot.save(update_fields=["earned_rewards"])
proceeds = int(slashed_satoshis * (1 - reward_fraction)) new_proceeds = int(slashed_satoshis * (1 - reward_fraction))
order.proceeds += proceeds order.proceeds += new_proceeds
order.save(update_fields=["proceeds"]) order.save(update_fields=["proceeds"])
send_devfund_donation.delay(order.id, new_proceeds, "slashed bond")
return return
@ -1645,13 +1645,17 @@ class Logics:
""" """
if order.is_swap: if order.is_swap:
payout_sats = order.payout_tx.sent_satoshis + order.payout_tx.mining_fee payout_sats = (
order.proceeds += int(order.trade_escrow.num_satoshis - payout_sats) order.payout_tx.sent_satoshis + order.payout_tx.mining_fee_sats
)
new_proceeds = int(order.trade_escrow.num_satoshis - payout_sats)
else: else:
payout_sats = order.payout.num_satoshis + order.payout.fee 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"]) order.save(update_fields=["proceeds"])
send_devfund_donation.delay(order.id, new_proceeds, "successful order")
@classmethod @classmethod
def summarize_trade(cls, order, user): def summarize_trade(cls, order, user):

View File

@ -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,
),
),
]

View File

@ -9,6 +9,7 @@ class LNPayment(models.Model):
class Types(models.IntegerChoices): class Types(models.IntegerChoices):
NORM = 0, "Regular invoice" NORM = 0, "Regular invoice"
HOLD = 1, "hold invoice" HOLD = 1, "hold invoice"
KEYS = 2, "Keysend"
class Concepts(models.IntegerChoices): class Concepts(models.IntegerChoices):
MAKEBOND = 0, "Maker bond" MAKEBOND = 0, "Maker bond"
@ -16,6 +17,7 @@ class LNPayment(models.Model):
TRESCROW = 2, "Trade escrow" TRESCROW = 2, "Trade escrow"
PAYBUYER = 3, "Payment to buyer" PAYBUYER = 3, "Payment to buyer"
WITHREWA = 4, "Withdraw rewards" WITHREWA = 4, "Withdraw rewards"
DEVDONAT = 5, "Devfund donation"
class Status(models.IntegerChoices): class Status(models.IntegerChoices):
INVGEN = 0, "Generated" INVGEN = 0, "Generated"
@ -116,6 +118,13 @@ class LNPayment(models.Model):
null=True, null=True,
default=None, default=None,
) )
order_donated = models.ForeignKey(
"api.Order",
related_name="order_donated",
null=True,
on_delete=models.SET_NULL,
default=None,
)
def __str__(self): def __str__(self):
return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}" return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"

View File

@ -88,6 +88,58 @@ def follow_send_payment(hash):
return results 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) @shared_task(name="payments_cleansing", time_limit=600)
def payments_cleansing(): def payments_cleansing():
""" """

View File

@ -43,10 +43,6 @@ app.conf.beat_schedule = {
"task": "payments_cleansing", "task": "payments_cleansing",
"schedule": crontab(hour=0, minute=0), "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 "do-accounting": { # Does accounting for the last day
"task": "do_accounting", "task": "do_accounting",
"schedule": crontab(hour=23, minute=59), "schedule": crontab(hour=23, minute=59),

View File

@ -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 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 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 # LND Versioner proto
curl -o verrpc.proto -s https://raw.githubusercontent.com/lightningnetwork/lnd/master/lnrpc/verrpc/verrpc.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 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 # patch generated files relative imports
sed -i 's/^import .*_pb2 as/from . \0/' router_pb2.py 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/' invoices_pb2.py
sed -i 's/^import .*_pb2 as/from . \0/' verrpc_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/' 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/' lightning_pb2_grpc.py
sed -i 's/^import .*_pb2 as/from . \0/' invoices_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 sed -i 's/^import .*_pb2 as/from . \0/' verrpc_pb2_grpc.py