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
# 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%.
# Minimum swap fee as fraction (2%)
MIN_SWAP_FEE = 0.02
# Minimum swap fee as fraction (1%)
MIN_SWAP_FEE = 0.01
# 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%)
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_SHAPE = 'linear'
# Min amount allowed for Swap
MIN_SWAP_AMOUNT = 50000

View File

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

View File

@ -2,7 +2,7 @@ from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin
from django.contrib.auth.models import Group, User
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(User)
@ -53,6 +53,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"is_fiat_sent",
"created_at",
"expires_at",
"payout_tx_link",
"payout_link",
"maker_bond_link",
"taker_bond_link",
@ -63,6 +64,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"maker",
"taker",
"currency",
"payout_tx",
"payout",
"maker_bond",
"taker_bond",
@ -108,6 +110,25 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
ordering = ("-expires_at", )
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)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):

View File

@ -2,7 +2,7 @@ from datetime import timedelta
from tkinter import N
from django.utils import timezone
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.tasks import send_message
@ -495,6 +495,8 @@ class Logics:
return True, None
def compute_swap_fee_rate(balance):
shape = str(config('SWAP_FEE_SHAPE'))
if shape == "linear":
@ -508,23 +510,40 @@ class Logics:
slope = (MAX_SWAP_FEE - MIN_SWAP_FEE) / (MAX_POINT - MIN_POINT)
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
@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.
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.
'''
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()
order.payout_tx = onchain_payment
order.save()
return True, None
return True
@classmethod
def payout_amount(cls, order, user):
@ -553,10 +572,16 @@ class Logics:
if context["invoice_amount"] < MIN_SWAP_AMOUNT:
context["swap_allowed"] = False
context["swap_failure_reason"] = "Order amount is too small to be eligible for a swap"
return True, context
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["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
MEMPO = 2, "In mempool" # Tx is sent to mempool
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
concept = models.PositiveSmallIntegerField(choices=Concepts.choices,
@ -218,11 +223,12 @@ class OnchainPayment(models.Model):
null=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,
related_name="balance",
on_delete=models.SET_NULL,
default=BalanceLog.objects.create)
null=True,
default=get_balance)
swap_fee_rate = models.DecimalField(max_digits=4,
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}"
class Meta:
verbose_name = "Lightning payment"
verbose_name_plural = "Lightning payments"
verbose_name = "Onchain payment"
verbose_name_plural = "Onchain payments"
@property
def hash(self):
# Payment hash is the primary key of LNpayments
# However it is too long for the admin panel.
# We created a truncated property for display 'hash'
return truncatechars(self.payment_hash, 10)
# Display txid as 'hash' truncated
return truncatechars(self.txid, 10)
class Order(models.Model):
@ -460,14 +464,14 @@ class Order(models.Model):
blank=True,
)
payout_tx = models.OneToOneField(
OnchainPayment,
related_name="order_paid_TX",
on_delete=models.SET_NULL,
null=True,
default=None,
blank=True,
)
# payout_tx = models.OneToOneField(
# OnchainPayment,
# related_name="order_paid_TX",
# on_delete=models.SET_NULL,
# null=True,
# default=None,
# blank=True,
# )
# ratings
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)
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)
# 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'])
total = models.PositiveBigIntegerField(default=get_total)
onchain_fraction = models.DecimalField(max_digits=6, decimal_places=5, default=get_frac)
onchain_total = models.PositiveBigIntegerField(default=get_oc_total)
onchain_confirmed = models.PositiveBigIntegerField(default=get_oc_conf)
onchain_unconfirmed = models.PositiveBigIntegerField(default=get_oc_unconf)
ln_local = models.PositiveBigIntegerField(default=get_ln_local)
ln_remote = models.PositiveBigIntegerField(default=get_ln_remote)
ln_local_unsettled = models.PositiveBigIntegerField(default=get_ln_local_unsettled)
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):
pass

View File

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