mirror of
https://github.com/Reckless-Satoshi/robosats.git
synced 2024-09-11 11:55:26 +03:00
Add onchain logics pt3
This commit is contained in:
parent
cf82a4d6ae
commit
b1d68a39f7
14
.env-sample
14
.env-sample
@ -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
|
||||
|
||||
|
@ -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 ./
|
||||
|
23
api/admin.py
23
api/admin.py
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user