mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-20 21:21:36 +00: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
|
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
|
||||||
|
|
||||||
|
@ -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 ./
|
||||||
|
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_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):
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
@ -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
|
||||||
|
Loading…
Reference in New Issue
Block a user