Add onchain logics pt3

This commit is contained in:
Reckless_Satoshi 2022-06-07 15:14:56 -07:00
parent cf82a4d6ae
commit b1d68a39f7
No known key found for this signature in database
GPG Key ID: 9C4585B561315571
7 changed files with 122 additions and 42 deletions

View File

@ -102,18 +102,20 @@ REWARDS_TIMEOUT_SECONDS = 60
PAYOUT_TIMEOUT_SECONDS = 60 PAYOUT_TIMEOUT_SECONDS = 60
# REVERSE SUBMARINE SWAP PAYOUTS # REVERSE SUBMARINE SWAP PAYOUTS
# 4 parameters needed, min/max change and min/max balance points. E.g. If 25% or more of liquidity # 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)
SWAP_LAMBDA = 8.8
# LINEAR. 4 parameters needed: min/max fees 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%. # 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%) # Minimum swap fee as fraction (1%)
MIN_SWAP_FEE = 0.02 MIN_SWAP_FEE = 0.01
# Liquidity split point (LN/onchain) at which we use MIN_SWAP_FEE # Liquidity split point (LN/onchain) at which we use MIN_SWAP_FEE
MIN_SWAP_POINT = 0.25 MIN_SWAP_POINT = 0.35
# Maximum swap fee as fraction (~10%) # Maximum swap fee as fraction (~10%)
MAX_SWAP_FEE = 0.1 MAX_SWAP_FEE = 0.1
# Liquidity split point (LN/onchain) at which we use MAX_SWAP_FEE # Liquidity split point (LN/onchain) at which we use MAX_SWAP_FEE
MAX_SWAP_POINT = 0 MAX_SWAP_POINT = 0
# Shape of fee to available liquidity curve. Only 'linear' implemented.
SWAP_FEE_SHAPE = 'linear'
# Min amount allowed for Swap # Min amount allowed for Swap
MIN_SWAP_AMOUNT = 50000 MIN_SWAP_AMOUNT = 50000

View File

@ -1,10 +1,14 @@
FROM python:3.10.2-bullseye FROM python:3.10.2-bullseye
ARG DEBIAN_FRONTEND=noninteractive
RUN mkdir -p /usr/src/robosats RUN mkdir -p /usr/src/robosats
# specifying the working dir inside the container # specifying the working dir inside the container
WORKDIR /usr/src/robosats WORKDIR /usr/src/robosats
RUN apt-get update
RUN apt-get install -y postgresql-client
RUN python -m pip install --upgrade pip RUN python -m pip install --upgrade pip
COPY requirements.txt ./ COPY requirements.txt ./

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin from django_admin_relation_links import AdminChangeLinksMixin
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.contrib.auth.admin import UserAdmin from django.contrib.auth.admin import UserAdmin
from api.models import Order, LNPayment, Profile, MarketTick, Currency from api.models import OnchainPayment, Order, LNPayment, Profile, MarketTick, Currency
admin.site.unregister(Group) admin.site.unregister(Group)
admin.site.unregister(User) admin.site.unregister(User)
@ -53,6 +53,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"is_fiat_sent", "is_fiat_sent",
"created_at", "created_at",
"expires_at", "expires_at",
"payout_tx_link",
"payout_link", "payout_link",
"maker_bond_link", "maker_bond_link",
"taker_bond_link", "taker_bond_link",
@ -63,6 +64,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"maker", "maker",
"taker", "taker",
"currency", "currency",
"payout_tx",
"payout", "payout",
"maker_bond", "maker_bond",
"taker_bond", "taker_bond",
@ -108,6 +110,25 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
ordering = ("-expires_at", ) ordering = ("-expires_at", )
search_fields = ["payment_hash","num_satoshis","sender__username","receiver__username","description"] search_fields = ["payment_hash","num_satoshis","sender__username","receiver__username","description"]
@admin.register(OnchainPayment)
class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = (
"id",
"address",
"concept",
"status",
"num_satoshis",
"hash",
"swap_fee_rate",
"mining_fee_sats",
"balance_link",
)
change_links = (
"balance",
)
list_display_links = ("id","address", "concept")
list_filter = ("concept", "status")
search_fields = ["address","num_satoshis","receiver__username","txid"]
@admin.register(Profile) @admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):

View File

@ -2,7 +2,7 @@ from datetime import timedelta
from tkinter import N from tkinter import N
from django.utils import timezone from django.utils import timezone
from api.lightning.node import LNNode from api.lightning.node import LNNode
from django.db.models import Q from django.db.models import Q, Sum
from api.models import OnchainPayment, Order, LNPayment, MarketTick, User, Currency from api.models import OnchainPayment, Order, LNPayment, MarketTick, User, Currency
from api.tasks import send_message from api.tasks import send_message
@ -495,6 +495,8 @@ class Logics:
return True, None return True, None
def compute_swap_fee_rate(balance): def compute_swap_fee_rate(balance):
shape = str(config('SWAP_FEE_SHAPE')) shape = str(config('SWAP_FEE_SHAPE'))
if shape == "linear": if shape == "linear":
@ -508,23 +510,40 @@ class Logics:
slope = (MAX_SWAP_FEE - MIN_SWAP_FEE) / (MAX_POINT - MIN_POINT) slope = (MAX_SWAP_FEE - MIN_SWAP_FEE) / (MAX_POINT - MIN_POINT)
swap_fee_rate = slope * (balance.onchain_fraction - MAX_POINT) + MAX_SWAP_FEE swap_fee_rate = slope * (balance.onchain_fraction - MAX_POINT) + MAX_SWAP_FEE
elif shape == "exponential":
MIN_SWAP_FEE = float(config('MIN_SWAP_FEE'))
MAX_SWAP_FEE = float(config('MAX_SWAP_FEE'))
SWAP_LAMBDA = float(config('SWAP_LAMBDA'))
swap_fee_rate = MIN_SWAP_FEE + (MAX_SWAP_FEE - MIN_SWAP_FEE) * math.exp(-SWAP_LAMBDA * balance.onchain_fraction)
return swap_fee_rate return swap_fee_rate
@classmethod @classmethod
def create_onchain_payment(cls, order, estimate_sats): def create_onchain_payment(cls, order, preliminary_amount):
''' '''
Creates an empty OnchainPayment for order.payout_tx. Creates an empty OnchainPayment for order.payout_tx.
It sets the fees to be applied to this order if onchain Swap is used. It sets the fees to be applied to this order if onchain Swap is used.
If the user submits a LN invoice instead. The returned OnchainPayment goes unused. If the user submits a LN invoice instead. The returned OnchainPayment goes unused.
''' '''
onchain_payment = OnchainPayment.objects.create() onchain_payment = OnchainPayment.objects.create()
onchain_payment.suggested_mining_fee_rate = LNNode.estimate_fee(amount_sats=estimate_sats)
onchain_payment.swap_fee_rate = cls.compute_swap_fee_rate(onchain_payment.balance) # Compute a safer available onchain liquidity: (confirmed_utxos - reserve - pending_outgoing_txs))
# Accounts for already committed outgoing TX for previous users.
confirmed = onchain_payment.balance.onchain_confirmed
reserve = 0.01 * onchain_payment.balance.total # We assume a reserve of 1%
pending_txs = OnchainPayment.objects.filter(status=OnchainPayment.Status.VALID).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
available_onchain = confirmed - reserve - pending_txs
if preliminary_amount > available_onchain: # Not enough onchain balance to commit for this swap.
return False
onchain_payment.suggested_mining_fee_rate = LNNode.estimate_fee(amount_sats=preliminary_amount)
onchain_payment.swap_fee_rate = cls.compute_swap_fee_rate(onchain_payment.preliminary_amount)
onchain_payment.save() onchain_payment.save()
order.payout_tx = onchain_payment order.payout_tx = onchain_payment
order.save() order.save()
return True, None return True
@classmethod @classmethod
def payout_amount(cls, order, user): def payout_amount(cls, order, user):
@ -553,10 +572,16 @@ class Logics:
if context["invoice_amount"] < MIN_SWAP_AMOUNT: if context["invoice_amount"] < MIN_SWAP_AMOUNT:
context["swap_allowed"] = False context["swap_allowed"] = False
context["swap_failure_reason"] = "Order amount is too small to be eligible for a swap"
return True, context return True, context
if order.payout_tx == None: if order.payout_tx == None:
cls.create_onchain_payment(order, estimate_sats=context["invoice_amount"]) # Creates the OnchainPayment object and checks node balance
valid, _ = cls.create_onchain_payment(order, preliminary_amount=context["invoice_amount"])
if not valid:
context["swap_allowed"] = False
context["swap_failure_reason"] = "Not enough onchain liquidity available to offer swaps"
return True, context
context["swap_allowed"] = True context["swap_allowed"] = True
context["suggested_mining_fee_rate"] = order.payout_tx.suggested_mining_fee_rate context["suggested_mining_fee_rate"] = order.payout_tx.suggested_mining_fee_rate

View File

@ -176,6 +176,11 @@ class OnchainPayment(models.Model):
VALID = 1, "Valid" # Valid onchain address submitted VALID = 1, "Valid" # Valid onchain address submitted
MEMPO = 2, "In mempool" # Tx is sent to mempool MEMPO = 2, "In mempool" # Tx is sent to mempool
CONFI = 3, "Confirmed" # Tx is confirme +2 blocks CONFI = 3, "Confirmed" # Tx is confirme +2 blocks
CANCE = 4, "Cancelled" # Cancelled tx
def get_balance():
balance = BalanceLog.objects.create()
return balance.time
# payment use details # payment use details
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, concept = models.PositiveSmallIntegerField(choices=Concepts.choices,
@ -218,11 +223,12 @@ class OnchainPayment(models.Model):
null=False, null=False,
blank=False) blank=False)
# platform onchain/channels balance at creattion, swap fee rate as percent of total volume # platform onchain/channels balance at creation, swap fee rate as percent of total volume
balance = models.ForeignKey(BalanceLog, balance = models.ForeignKey(BalanceLog,
related_name="balance", related_name="balance",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
default=BalanceLog.objects.create) null=True,
default=get_balance)
swap_fee_rate = models.DecimalField(max_digits=4, swap_fee_rate = models.DecimalField(max_digits=4,
decimal_places=2, decimal_places=2,
@ -248,15 +254,13 @@ class OnchainPayment(models.Model):
return f"TX-{txname}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}" return f"TX-{txname}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
class Meta: class Meta:
verbose_name = "Lightning payment" verbose_name = "Onchain payment"
verbose_name_plural = "Lightning payments" verbose_name_plural = "Onchain payments"
@property @property
def hash(self): def hash(self):
# Payment hash is the primary key of LNpayments # Display txid as 'hash' truncated
# However it is too long for the admin panel. return truncatechars(self.txid, 10)
# We created a truncated property for display 'hash'
return truncatechars(self.payment_hash, 10)
class Order(models.Model): class Order(models.Model):
@ -460,14 +464,14 @@ class Order(models.Model):
blank=True, blank=True,
) )
payout_tx = models.OneToOneField( # payout_tx = models.OneToOneField(
OnchainPayment, # OnchainPayment,
related_name="order_paid_TX", # related_name="order_paid_TX",
on_delete=models.SET_NULL, # on_delete=models.SET_NULL,
null=True, # null=True,
default=None, # default=None,
blank=True, # blank=True,
) # )
# ratings # ratings
maker_rated = models.BooleanField(default=False, null=False) maker_rated = models.BooleanField(default=False, null=False)

View File

@ -74,18 +74,41 @@ class AccountingMonth(models.Model):
rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
class BalanceLog(models.Model): class BalanceLog(models.Model):
def get_total():
return LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance']
def get_frac():
return (LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance']) / LNNode.wallet_balance()['total_balance']
def get_oc_total():
return LNNode.wallet_balance()['total_balance']
def get_oc_conf():
return LNNode.wallet_balance()['confirmed_balance']
def get_oc_unconf():
return LNNode.wallet_balance()['unconfirmed_balance']
def get_ln_local():
return LNNode.channel_balance()['local_balance']
def get_ln_remote():
return LNNode.channel_balance()['remote_balance']
def get_ln_local_unsettled():
return LNNode.channel_balance()['unsettled_local_balance']
def get_ln_remote_unsettled():
return LNNode.channel_balance()['unsettled_remote_balance']
time = models.DateTimeField(primary_key=True, default=timezone.now) time = models.DateTimeField(primary_key=True, default=timezone.now)
# Every field is denominated in Sats # Every field is denominated in Sats
total = models.PositiveBigIntegerField(default=lambda : LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance']) total = models.PositiveBigIntegerField(default=get_total)
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_fraction = models.DecimalField(max_digits=6, decimal_places=5, default=get_frac)
onchain_total = models.PositiveBigIntegerField(default=lambda : LNNode.wallet_balance()['total_balance']) onchain_total = models.PositiveBigIntegerField(default=get_oc_total)
onchain_confirmed = models.PositiveBigIntegerField(default=lambda : LNNode.wallet_balance()['confirmed_balance']) onchain_confirmed = models.PositiveBigIntegerField(default=get_oc_conf)
onchain_unconfirmed = models.PositiveBigIntegerField(default=lambda : LNNode.wallet_balance()['unconfirmed_balance']) onchain_unconfirmed = models.PositiveBigIntegerField(default=get_oc_unconf)
ln_local = models.PositiveBigIntegerField(default=lambda : LNNode.channel_balance()['local_balance']) ln_local = models.PositiveBigIntegerField(default=get_ln_local)
ln_remote = models.PositiveBigIntegerField(default=lambda : LNNode.channel_balance()['remote_balance']) ln_remote = models.PositiveBigIntegerField(default=get_ln_remote)
ln_local_unsettled = models.PositiveBigIntegerField(default=lambda : LNNode.channel_balance()['unsettled_local_balance']) ln_local_unsettled = models.PositiveBigIntegerField(default=get_ln_local_unsettled)
ln_remote_unsettled = models.PositiveBigIntegerField(default=lambda : LNNode.channel_balance()['unsettled_remote_balance']) ln_remote_unsettled = models.PositiveBigIntegerField(default=get_ln_remote_unsettled)
def __str__(self):
return f"Balance at {self.time.strftime('%d/%m/%Y %H:%M:%S')}"
class Dispute(models.Model): class Dispute(models.Model):
pass pass

View File

@ -22,6 +22,7 @@ services:
backend: backend:
build: . build: .
image: backend
container_name: django-dev container_name: django-dev
restart: always restart: always
depends_on: depends_on:
@ -45,7 +46,7 @@ services:
- ./frontend:/usr/src/frontend - ./frontend:/usr/src/frontend
clean-orders: clean-orders:
build: . image: backend
restart: always restart: always
container_name: clord-dev container_name: clord-dev
command: python3 manage.py clean_orders command: python3 manage.py clean_orders
@ -55,7 +56,7 @@ services:
network_mode: service:tor network_mode: service:tor
follow-invoices: follow-invoices:
build: . image: backend
container_name: invo-dev container_name: invo-dev
restart: always restart: always
depends_on: depends_on:
@ -68,7 +69,7 @@ services:
network_mode: service:tor network_mode: service:tor
telegram-watcher: telegram-watcher:
build: . image: backend
container_name: tg-dev container_name: tg-dev
restart: always restart: always
command: python3 manage.py telegram_watcher command: python3 manage.py telegram_watcher
@ -78,7 +79,7 @@ services:
network_mode: service:tor network_mode: service:tor
celery: celery:
build: . image: backend
container_name: cele-dev container_name: cele-dev
restart: always restart: always
command: celery -A robosats worker --beat -l info -S django command: celery -A robosats worker --beat -l info -S django