diff --git a/.env-sample b/.env-sample index c989392e..0a37b7c7 100644 --- a/.env-sample +++ b/.env-sample @@ -93,7 +93,7 @@ FIAT_EXCHANGE_DURATION = 24 # ROUTING # Proportional routing fee limit (fraction of total payout: % / 100) -PROPORTIONAL_ROUTING_FEE_LIMIT = 0.0002 +PROPORTIONAL_ROUTING_FEE_LIMIT = 0.001 # Base flat limit fee for routing in Sats (used only when proportional is lower than this) MIN_FLAT_ROUTING_FEE_LIMIT = 10 MIN_FLAT_ROUTING_FEE_LIMIT_REWARD = 2 @@ -101,6 +101,22 @@ MIN_FLAT_ROUTING_FEE_LIMIT_REWARD = 2 REWARDS_TIMEOUT_SECONDS = 60 PAYOUT_TIMEOUT_SECONDS = 60 +# REVERSE SUBMARINE SWAP PAYOUTS +# 4 parameters needed, min/max change and min/max balance points. E.g. If 25% or more of liquidity +# is onchain the fee for swap is 2% (minimum), if it is 12% fee is 6%, and for 0% fee is 10%. +# Minimum swap fee as fraction (2%) +MIN_SWAP_FEE = 0.02 +# Liquidity split point (LN/onchain) at which we use MIN_SWAP_FEE +MIN_SWAP_POINT = 0.25 +# Maximum swap fee as fraction (~10%) +MAX_SWAP_FEE = 0.1 +# Liquidity split point (LN/onchain) at which we use MAX_SWAP_FEE +MAX_SWAP_POINT = 0 +# Shape of fee to available liquidity curve. Only 'linear' implemented. +SWAP_FEE_ = 'linear' +# Min amount allowed for Swap +MIN_SWAP_AMOUNT = 800000 + # Reward tip. Reward for every finished trade in the referral program (Satoshis) REWARD_TIP = 100 # Fraction rewarded to user from the slashed bond of a counterpart. diff --git a/api/lightning/node.py b/api/lightning/node.py index ed79116c..8944746a 100644 --- a/api/lightning/node.py +++ b/api/lightning/node.py @@ -1,4 +1,4 @@ -import grpc, os, hashlib, secrets +import grpc, os, hashlib, secrets, ring 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 @@ -8,7 +8,6 @@ from base64 import b64decode from datetime import timedelta, datetime from django.utils import timezone - from api.models import LNPayment ####### @@ -67,6 +66,52 @@ class LNNode: MACAROON.hex())]) return response + @classmethod + 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) + request = lnrpc.EstimateFeeRequest(AddrToAmount={'bc1q3cpp7ww92n6zp04hv40kd3eyy5avgughx6xqnx':amount_sats}, + target_conf=target_conf, + min_confs=min_confs, + spend_unconfirmed=False) + + response = cls.lightningstub.EstimateFee(request, + metadata=[("macaroon", + MACAROON.hex())]) + + return {'mining_fee_sats': response.fee_sat, 'mining_fee_rate': response.sat_per_vbyte} + + wallet_balance_cache = {} + @ring.dict(wallet_balance_cache, expire=10) # keeps in cache for 10 seconds + @classmethod + def wallet_balance(cls): + """Returns onchain balance""" + request = lnrpc.WalletBalanceRequest() + response = cls.lightningstub.WalletBalance(request, + metadata=[("macaroon", + MACAROON.hex())]) + print(response) + return {'total_balance': response.total_balance, + 'confirmed_balance': response.confirmed_balance, + 'unconfirmed_balance': response.unconfirmed_balance} + + channel_balance_cache = {} + @ring.dict(channel_balance_cache, expire=10) # keeps in cache for 10 seconds + @classmethod + def channel_balance(cls): + """Returns channels balance""" + request = lnrpc.ChannelBalanceRequest() + response = cls.lightningstub.ChannelBalance(request, + metadata=[("macaroon", + MACAROON.hex())]) + + print(response) + return {'local_balance': response.local_balance.sat, + 'remote_balance': response.remote_balance.sat, + 'unsettled_local_balance': response.unsettled_local_balance.sat, + 'unsettled_remote_balance': response.unsettled_remote_balance.sat} + @classmethod def cancel_return_hold_invoice(cls, payment_hash): """Cancels or returns a hold invoice""" diff --git a/control/admin.py b/control/admin.py index 9e5a49af..1287b114 100755 --- a/control/admin.py +++ b/control/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from control.models import AccountingDay, AccountingMonth, Dispute +from control.models import AccountingDay, AccountingMonth, BalanceLog from import_export.admin import ImportExportModelAdmin # Register your models here. @@ -50,4 +50,35 @@ class AccountingMonthAdmin(ImportExportModelAdmin): "rewards_claimed", ) change_links = ["month"] - search_fields = ["month"] \ No newline at end of file + search_fields = ["month"] + + +@admin.register(BalanceLog) +class BalanceLogAdmin(ImportExportModelAdmin): + + list_display = ( + "time", + "total", + "onchain_fraction", + "onchain_total", + "onchain_confirmed", + "onchain_unconfirmed", + "ln_local", + "ln_remote", + "ln_local_unsettled", + "ln_remote_unsettled", + ) + readonly_fields = [ + "time", + "total", + "onchain_fraction", + "onchain_total", + "onchain_confirmed", + "onchain_unconfirmed", + "ln_local", + "ln_remote", + "ln_local_unsettled", + "ln_remote_unsettled", + ] + change_links = ["time"] + search_fields = ["time"] \ No newline at end of file diff --git a/control/models.py b/control/models.py index 82dd2678..fe1b40e8 100755 --- a/control/models.py +++ b/control/models.py @@ -1,6 +1,8 @@ from django.db import models from django.utils import timezone +from api.lightning.node import LNNode + class AccountingDay(models.Model): day = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False) @@ -36,8 +38,6 @@ class AccountingDay(models.Model): # Rewards claimed on day rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) - - class AccountingMonth(models.Model): month = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False) @@ -73,5 +73,19 @@ class AccountingMonth(models.Model): # Rewards claimed on day rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) +class BalanceLog(models.Model): + time = models.DateTimeField(primary_key=True, default=timezone.now) + + # Every field is denominated in Sats + total = models.PositiveBigIntegerField(default=lambda : LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance']) + onchain_fraction = models.DecimalField(max_digits=5, decimal_places=5, default=lambda : (LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance']) / LNNode.wallet_balance()['total_balance']) + onchain_total = models.PositiveBigIntegerField(default=lambda : LNNode.wallet_balance()['total_balance']) + onchain_confirmed = models.PositiveBigIntegerField(default=lambda : LNNode.wallet_balance()['confirmed_balance']) + onchain_unconfirmed = models.PositiveBigIntegerField(default=lambda : LNNode.wallet_balance()['unconfirmed_balance']) + ln_local = models.PositiveBigIntegerField(default=lambda : LNNode.channel_balance()['local_balance']) + ln_remote = models.PositiveBigIntegerField(default=lambda : LNNode.channel_balance()['remote_balance']) + ln_local_unsettled = models.PositiveBigIntegerField(default=lambda : LNNode.channel_balance()['unsettled_local_balance']) + ln_remote_unsettled = models.PositiveBigIntegerField(default=lambda : LNNode.channel_balance()['unsettled_remote_balance']) + class Dispute(models.Model): pass \ No newline at end of file diff --git a/control/tasks.py b/control/tasks.py index 616ae1ff..333362cb 100644 --- a/control/tasks.py +++ b/control/tasks.py @@ -1,10 +1,4 @@ from celery import shared_task -from api.models import Order, LNPayment, Profile, MarketTick -from control.models import AccountingDay, AccountingMonth -from django.utils import timezone -from datetime import timedelta -from django.db.models import Sum -from decouple import config @shared_task(name="do_accounting") def do_accounting(): @@ -12,6 +6,13 @@ def do_accounting(): Does all accounting from the beginning of time ''' + from api.models import Order, LNPayment, Profile, MarketTick + from control.models import AccountingDay + from django.utils import timezone + from datetime import timedelta + from django.db.models import Sum + from decouple import config + all_payments = LNPayment.objects.all() all_ticks = MarketTick.objects.all() today = timezone.now().date() @@ -109,4 +110,15 @@ def do_accounting(): result[str(day)]={'contracted':contracted,'inflow':inflow,'outflow':outflow} day = day + timedelta(days=1) - return result \ No newline at end of file + return result + +@shared_task(name="compute_node_balance", ignore_result=True) +def compute_node_balance(): + ''' + Queries LND for channel and wallet balance + ''' + from control.models import BalanceLog + + BalanceLog.objects.create() + + return \ No newline at end of file diff --git a/robosats/celery/__init__.py b/robosats/celery/__init__.py index 12b2a2fc..0e8f5425 100644 --- a/robosats/celery/__init__.py +++ b/robosats/celery/__init__.py @@ -55,6 +55,10 @@ app.conf.beat_schedule = { "task": "cache_external_market_prices", "schedule": timedelta(seconds=60), }, + "compute-node-balance": { # Logs LND channel and wallet balance + "task":"compute_node_balance", + "schedule": timedelta(minutes=15), + } } app.conf.timezone = "UTC"