mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 19:06:26 +00:00
Add keysend devfund donations functionality (#589)
This commit is contained in:
parent
b4a54f3b1a
commit
0a620901a7
10
.env-sample
10
.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
|
||||
|
@ -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",
|
||||
|
@ -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!"""
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
),
|
||||
),
|
||||
]
|
@ -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}"
|
||||
|
52
api/tasks.py
52
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():
|
||||
"""
|
||||
|
@ -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),
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user