mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 10:56:24 +00:00
Merge branch 'onchain-buyer-payouts' into main #160
This commit is contained in:
commit
164a960b62
26
.env-sample
26
.env-sample
@ -91,11 +91,35 @@ INVOICE_AND_ESCROW_DURATION = 30
|
|||||||
# Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS
|
# Time to confim chat and confirm fiat (time to Fiat Sent confirmation) HOURS
|
||||||
FIAT_EXCHANGE_DURATION = 24
|
FIAT_EXCHANGE_DURATION = 24
|
||||||
|
|
||||||
|
# ROUTING
|
||||||
# Proportional routing fee limit (fraction of total payout: % / 100)
|
# 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)
|
# 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 = 10
|
||||||
MIN_FLAT_ROUTING_FEE_LIMIT_REWARD = 2
|
MIN_FLAT_ROUTING_FEE_LIMIT_REWARD = 2
|
||||||
|
# Routing timeouts
|
||||||
|
REWARDS_TIMEOUT_SECONDS = 60
|
||||||
|
PAYOUT_TIMEOUT_SECONDS = 90
|
||||||
|
|
||||||
|
# REVERSE SUBMARINE SWAP PAYOUTS
|
||||||
|
# Disable on-the-fly swaps feature
|
||||||
|
DISABLE_ONCHAIN = False
|
||||||
|
# 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 (1%)
|
||||||
|
MIN_SWAP_FEE = 0.01
|
||||||
|
# Liquidity split point (LN/onchain) at which we use MIN_SWAP_FEE
|
||||||
|
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
|
||||||
|
# Min amount allowed for Swap
|
||||||
|
MIN_SWAP_AMOUNT = 50000
|
||||||
|
|
||||||
# Reward tip. Reward for every finished trade in the referral program (Satoshis)
|
# Reward tip. Reward for every finished trade in the referral program (Satoshis)
|
||||||
REWARD_TIP = 100
|
REWARD_TIP = 100
|
||||||
|
@ -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):
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
import grpc, os, hashlib, secrets
|
import grpc, os, hashlib, secrets, ring
|
||||||
|
|
||||||
|
|
||||||
from . import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
|
from . import lightning_pb2 as lnrpc, lightning_pb2_grpc as lightningstub
|
||||||
from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub
|
from . import invoices_pb2 as invoicesrpc, invoices_pb2_grpc as invoicesstub
|
||||||
from . import router_pb2 as routerrpc, router_pb2_grpc as routerstub
|
from . import router_pb2 as routerrpc, router_pb2_grpc as routerstub
|
||||||
@ -9,7 +11,6 @@ from base64 import b64decode
|
|||||||
from datetime import timedelta, datetime
|
from datetime import timedelta, datetime
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from api.models import LNPayment
|
|
||||||
|
|
||||||
#######
|
#######
|
||||||
# Should work with LND (c-lightning in the future if there are features that deserve the work)
|
# Should work with LND (c-lightning in the future if there are features that deserve the work)
|
||||||
@ -67,6 +68,74 @@ class LNNode:
|
|||||||
MACAROON.hex())])
|
MACAROON.hex())])
|
||||||
return response
|
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 as shortcut so there is no need of user inputs
|
||||||
|
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())])
|
||||||
|
|
||||||
|
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())])
|
||||||
|
|
||||||
|
|
||||||
|
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 pay_onchain(cls, onchainpayment):
|
||||||
|
"""Send onchain transaction for buyer payouts"""
|
||||||
|
|
||||||
|
if config("DISABLE_ONCHAIN", cast=bool):
|
||||||
|
return False
|
||||||
|
|
||||||
|
request = lnrpc.SendCoinsRequest(addr=onchainpayment.address,
|
||||||
|
amount=int(onchainpayment.sent_satoshis),
|
||||||
|
sat_per_vbyte=int(onchainpayment.mining_fee_rate),
|
||||||
|
label=str("Payout order #" + str(onchainpayment.order_paid_TX.id)),
|
||||||
|
spend_unconfirmed=True)
|
||||||
|
|
||||||
|
response = cls.lightningstub.SendCoins(request,
|
||||||
|
metadata=[("macaroon",
|
||||||
|
MACAROON.hex())])
|
||||||
|
|
||||||
|
onchainpayment.txid = response.txid
|
||||||
|
onchainpayment.save()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def cancel_return_hold_invoice(cls, payment_hash):
|
def cancel_return_hold_invoice(cls, payment_hash):
|
||||||
"""Cancels or returns a hold invoice"""
|
"""Cancels or returns a hold invoice"""
|
||||||
@ -131,28 +200,25 @@ class LNNode:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def validate_hold_invoice_locked(cls, lnpayment):
|
def validate_hold_invoice_locked(cls, lnpayment):
|
||||||
"""Checks if hold invoice is locked"""
|
"""Checks if hold invoice is locked"""
|
||||||
|
from api.models import LNPayment
|
||||||
|
|
||||||
request = invoicesrpc.LookupInvoiceMsg(
|
request = invoicesrpc.LookupInvoiceMsg(
|
||||||
payment_hash=bytes.fromhex(lnpayment.payment_hash))
|
payment_hash=bytes.fromhex(lnpayment.payment_hash))
|
||||||
response = cls.invoicesstub.LookupInvoiceV2(request,
|
response = cls.invoicesstub.LookupInvoiceV2(request,
|
||||||
metadata=[("macaroon",
|
metadata=[("macaroon",
|
||||||
MACAROON.hex())
|
MACAROON.hex())
|
||||||
])
|
])
|
||||||
print("status here")
|
|
||||||
print(response.state)
|
|
||||||
|
|
||||||
# TODO ERROR HANDLING
|
|
||||||
# Will fail if 'unable to locate invoice'. Happens if invoice expiry
|
# Will fail if 'unable to locate invoice'. Happens if invoice expiry
|
||||||
# time has passed (but these are 15% padded at the moment). Should catch it
|
# time has passed (but these are 15% padded at the moment). Should catch it
|
||||||
# and report back that the invoice has expired (better robustness)
|
# and report back that the invoice has expired (better robustness)
|
||||||
if response.state == 0: # OPEN
|
if response.state == 0: # OPEN
|
||||||
print("STATUS: OPEN")
|
|
||||||
pass
|
pass
|
||||||
if response.state == 1: # SETTLED
|
if response.state == 1: # SETTLED
|
||||||
pass
|
pass
|
||||||
if response.state == 2: # CANCELLED
|
if response.state == 2: # CANCELLED
|
||||||
pass
|
pass
|
||||||
if response.state == 3: # ACCEPTED (LOCKED)
|
if response.state == 3: # ACCEPTED (LOCKED)
|
||||||
print("STATUS: ACCEPTED")
|
|
||||||
lnpayment.expiry_height = response.htlcs[0].expiry_height
|
lnpayment.expiry_height = response.htlcs[0].expiry_height
|
||||||
lnpayment.status = LNPayment.Status.LOCKED
|
lnpayment.status = LNPayment.Status.LOCKED
|
||||||
lnpayment.save()
|
lnpayment.save()
|
||||||
@ -183,7 +249,6 @@ class LNNode:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
payreq_decoded = cls.decode_payreq(invoice)
|
payreq_decoded = cls.decode_payreq(invoice)
|
||||||
print(payreq_decoded)
|
|
||||||
except:
|
except:
|
||||||
payout["context"] = {
|
payout["context"] = {
|
||||||
"bad_invoice": "Does not look like a valid lightning invoice"
|
"bad_invoice": "Does not look like a valid lightning invoice"
|
||||||
@ -238,7 +303,7 @@ class LNNode:
|
|||||||
|
|
||||||
if payout["expires_at"] < timezone.now():
|
if payout["expires_at"] < timezone.now():
|
||||||
payout["context"] = {
|
payout["context"] = {
|
||||||
"bad_invoice": f"The invoice provided has already expired"
|
"bad_invoice": "The invoice provided has already expired"
|
||||||
}
|
}
|
||||||
return payout
|
return payout
|
||||||
|
|
||||||
@ -251,15 +316,17 @@ class LNNode:
|
|||||||
@classmethod
|
@classmethod
|
||||||
def pay_invoice(cls, lnpayment):
|
def pay_invoice(cls, lnpayment):
|
||||||
"""Sends sats. Used for rewards payouts"""
|
"""Sends sats. Used for rewards payouts"""
|
||||||
|
from api.models import LNPayment
|
||||||
|
|
||||||
fee_limit_sat = int(
|
fee_limit_sat = int(
|
||||||
max(
|
max(
|
||||||
lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
|
lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
|
||||||
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
|
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
|
||||||
)) # 200 ppm or 10 sats
|
)) # 200 ppm or 10 sats
|
||||||
|
timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS"))
|
||||||
request = routerrpc.SendPaymentRequest(payment_request=lnpayment.invoice,
|
request = routerrpc.SendPaymentRequest(payment_request=lnpayment.invoice,
|
||||||
fee_limit_sat=fee_limit_sat,
|
fee_limit_sat=fee_limit_sat,
|
||||||
timeout_seconds=30)
|
timeout_seconds=timeout_seconds)
|
||||||
|
|
||||||
for response in cls.routerstub.SendPaymentV2(request,
|
for response in cls.routerstub.SendPaymentV2(request,
|
||||||
metadata=[("macaroon",
|
metadata=[("macaroon",
|
||||||
|
246
api/logics.py
246
api/logics.py
@ -1,12 +1,14 @@
|
|||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from tkinter import N
|
from tkinter import N, ON
|
||||||
|
from tokenize import Octnumber
|
||||||
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 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
|
||||||
from decouple import config
|
from decouple import config
|
||||||
|
from api.utils import validate_onchain_address
|
||||||
|
|
||||||
import gnupg
|
import gnupg
|
||||||
|
|
||||||
@ -494,10 +496,78 @@ class Logics:
|
|||||||
order.save()
|
order.save()
|
||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
|
def compute_swap_fee_rate(balance):
|
||||||
|
|
||||||
|
|
||||||
|
shape = str(config('SWAP_FEE_SHAPE'))
|
||||||
|
|
||||||
|
if shape == "linear":
|
||||||
|
MIN_SWAP_FEE = float(config('MIN_SWAP_FEE'))
|
||||||
|
MIN_POINT = float(config('MIN_POINT'))
|
||||||
|
MAX_SWAP_FEE = float(config('MAX_SWAP_FEE'))
|
||||||
|
MAX_POINT = float(config('MAX_POINT'))
|
||||||
|
if float(balance.onchain_fraction) > MIN_POINT:
|
||||||
|
swap_fee_rate = MIN_SWAP_FEE
|
||||||
|
else:
|
||||||
|
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 * float(balance.onchain_fraction))
|
||||||
|
|
||||||
|
return swap_fee_rate * 100
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_onchain_payment(cls, order, user, 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.
|
||||||
|
'''
|
||||||
|
# Make sure no invoice payout is attached to order
|
||||||
|
order.payout = None
|
||||||
|
|
||||||
|
# Create onchain_payment
|
||||||
|
onchain_payment = OnchainPayment.objects.create(receiver=user)
|
||||||
|
|
||||||
|
# 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']
|
||||||
|
|
||||||
|
if pending_txs == None:
|
||||||
|
pending_txs = 0
|
||||||
|
|
||||||
|
available_onchain = confirmed - reserve - pending_txs
|
||||||
|
if preliminary_amount > available_onchain: # Not enough onchain balance to commit for this swap.
|
||||||
|
return False
|
||||||
|
|
||||||
|
suggested_mining_fee_rate = LNNode.estimate_fee(amount_sats=preliminary_amount)["mining_fee_rate"]
|
||||||
|
|
||||||
|
# Hardcap mining fee suggested at 50 sats/vbyte
|
||||||
|
if suggested_mining_fee_rate > 50:
|
||||||
|
suggested_mining_fee_rate = 50
|
||||||
|
|
||||||
|
onchain_payment.suggested_mining_fee_rate = max(1.05, LNNode.estimate_fee(amount_sats=preliminary_amount)["mining_fee_rate"])
|
||||||
|
onchain_payment.swap_fee_rate = cls.compute_swap_fee_rate(onchain_payment.balance)
|
||||||
|
onchain_payment.save()
|
||||||
|
|
||||||
|
order.payout_tx = onchain_payment
|
||||||
|
order.save()
|
||||||
|
return True
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def payout_amount(cls, order, user):
|
def payout_amount(cls, order, user):
|
||||||
"""Computes buyer invoice amount. Uses order.last_satoshis,
|
"""Computes buyer invoice amount. Uses order.last_satoshis,
|
||||||
that is the final trade amount set at Taker Bond time"""
|
that is the final trade amount set at Taker Bond time
|
||||||
|
Adds context for onchain swap.
|
||||||
|
"""
|
||||||
|
if not cls.is_buyer(order, user):
|
||||||
|
return False, None
|
||||||
|
|
||||||
if user == order.maker:
|
if user == order.maker:
|
||||||
fee_fraction = FEE * MAKER_FEE_SPLIT
|
fee_fraction = FEE * MAKER_FEE_SPLIT
|
||||||
@ -508,10 +578,36 @@ class Logics:
|
|||||||
|
|
||||||
reward_tip = int(config('REWARD_TIP')) if user.profile.is_referred else 0
|
reward_tip = int(config('REWARD_TIP')) if user.profile.is_referred else 0
|
||||||
|
|
||||||
if cls.is_buyer(order, user):
|
context = {}
|
||||||
invoice_amount = round(order.last_satoshis - fee_sats - reward_tip) # Trading fee to buyer is charged here.
|
# context necessary for the user to submit a LN invoice
|
||||||
|
context["invoice_amount"] = round(order.last_satoshis - fee_sats - reward_tip) # Trading fee to buyer is charged here.
|
||||||
|
|
||||||
return True, {"invoice_amount": invoice_amount}
|
# context necessary for the user to submit an onchain address
|
||||||
|
MIN_SWAP_AMOUNT = int(config("MIN_SWAP_AMOUNT"))
|
||||||
|
|
||||||
|
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 config("DISABLE_ONCHAIN", cast=bool):
|
||||||
|
context["swap_allowed"] = False
|
||||||
|
context["swap_failure_reason"] = "On-the-fly submarine swaps are dissabled"
|
||||||
|
return True, context
|
||||||
|
|
||||||
|
if order.payout_tx == None:
|
||||||
|
# Creates the OnchainPayment object and checks node balance
|
||||||
|
valid = cls.create_onchain_payment(order, user, preliminary_amount=context["invoice_amount"])
|
||||||
|
if not valid:
|
||||||
|
context["swap_allowed"] = False
|
||||||
|
context["swap_failure_reason"] = "Not enough onchain liquidity available to offer a SWAP"
|
||||||
|
return True, context
|
||||||
|
|
||||||
|
context["swap_allowed"] = True
|
||||||
|
context["suggested_mining_fee_rate"] = order.payout_tx.suggested_mining_fee_rate
|
||||||
|
context["swap_fee_rate"] = order.payout_tx.swap_fee_rate
|
||||||
|
|
||||||
|
return True, context
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def escrow_amount(cls, order, user):
|
def escrow_amount(cls, order, user):
|
||||||
@ -532,6 +628,66 @@ class Logics:
|
|||||||
|
|
||||||
return True, {"escrow_amount": escrow_amount}
|
return True, {"escrow_amount": escrow_amount}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_address(cls, order, user, address, mining_fee_rate):
|
||||||
|
|
||||||
|
# Empty address?
|
||||||
|
if not address:
|
||||||
|
return False, {
|
||||||
|
"bad_address":
|
||||||
|
"You submitted an empty invoice"
|
||||||
|
}
|
||||||
|
# only the buyer can post a buyer address
|
||||||
|
if not cls.is_buyer(order, user):
|
||||||
|
return False, {
|
||||||
|
"bad_request":
|
||||||
|
"Only the buyer of this order can provide a payout address."
|
||||||
|
}
|
||||||
|
# not the right time to submit
|
||||||
|
if (not (order.taker_bond.status == order.maker_bond.status ==
|
||||||
|
LNPayment.Status.LOCKED)
|
||||||
|
and not order.status == Order.Status.FAI):
|
||||||
|
return False, {
|
||||||
|
"bad_request":
|
||||||
|
"You cannot submit an adress are not locked."
|
||||||
|
}
|
||||||
|
# not a valid address (does not accept Taproot as of now)
|
||||||
|
valid, context = validate_onchain_address(address)
|
||||||
|
if not valid:
|
||||||
|
return False, context
|
||||||
|
|
||||||
|
if mining_fee_rate:
|
||||||
|
# not a valid mining fee
|
||||||
|
if float(mining_fee_rate) <= 1:
|
||||||
|
return False, {
|
||||||
|
"bad_address":
|
||||||
|
"The mining fee is too low."
|
||||||
|
}
|
||||||
|
elif float(mining_fee_rate) > 50:
|
||||||
|
return False, {
|
||||||
|
"bad_address":
|
||||||
|
"The mining fee is too high."
|
||||||
|
}
|
||||||
|
order.payout_tx.mining_fee_rate = float(mining_fee_rate)
|
||||||
|
# If not mining ee provider use backend's suggested fee rate
|
||||||
|
else:
|
||||||
|
order.payout_tx.mining_fee_rate = order.payout_tx.suggested_mining_fee_rate
|
||||||
|
|
||||||
|
tx = order.payout_tx
|
||||||
|
tx.address = address
|
||||||
|
tx.mining_fee_sats = int(tx.mining_fee_rate * 141)
|
||||||
|
tx.num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
|
||||||
|
tx.sent_satoshis = int(float(tx.num_satoshis) - float(tx.num_satoshis) * float(tx.swap_fee_rate)/100 - float(tx.mining_fee_sats))
|
||||||
|
tx.status = OnchainPayment.Status.VALID
|
||||||
|
tx.save()
|
||||||
|
|
||||||
|
order.is_swap = True
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
cls.move_state_updated_payout_method(order)
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def update_invoice(cls, order, user, invoice):
|
def update_invoice(cls, order, user, invoice):
|
||||||
|
|
||||||
@ -563,6 +719,11 @@ class Logics:
|
|||||||
"You cannot submit an invoice only after expiration or 3 failed attempts"
|
"You cannot submit an invoice only after expiration or 3 failed attempts"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# cancel onchain_payout if existing
|
||||||
|
if order.payout_tx:
|
||||||
|
order.payout_tx.status = OnchainPayment.Status.CANCE
|
||||||
|
order.payout_tx.save()
|
||||||
|
|
||||||
num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
|
num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
|
||||||
payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
|
payout = LNNode.validate_ln_invoice(invoice, num_satoshis)
|
||||||
|
|
||||||
@ -573,7 +734,7 @@ class Logics:
|
|||||||
concept=LNPayment.Concepts.PAYBUYER,
|
concept=LNPayment.Concepts.PAYBUYER,
|
||||||
type=LNPayment.Types.NORM,
|
type=LNPayment.Types.NORM,
|
||||||
sender=User.objects.get(username=ESCROW_USERNAME),
|
sender=User.objects.get(username=ESCROW_USERNAME),
|
||||||
order_paid=
|
order_paid_LN=
|
||||||
order, # In case this user has other payouts, update the one related to this order.
|
order, # In case this user has other payouts, update the one related to this order.
|
||||||
receiver=user,
|
receiver=user,
|
||||||
# if there is a LNPayment matching these above, it updates that one with defaults below.
|
# if there is a LNPayment matching these above, it updates that one with defaults below.
|
||||||
@ -588,6 +749,15 @@ class Logics:
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
order.is_swap = False
|
||||||
|
order.save()
|
||||||
|
|
||||||
|
cls.move_state_updated_payout_method(order)
|
||||||
|
|
||||||
|
return True, None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def move_state_updated_payout_method(cls,order):
|
||||||
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
|
# If the order status is 'Waiting for invoice'. Move forward to 'chat'
|
||||||
if order.status == Order.Status.WFI:
|
if order.status == Order.Status.WFI:
|
||||||
order.status = Order.Status.CHA
|
order.status = Order.Status.CHA
|
||||||
@ -617,10 +787,9 @@ class Logics:
|
|||||||
order.payout.status = LNPayment.Status.FLIGHT
|
order.payout.status = LNPayment.Status.FLIGHT
|
||||||
order.payout.routing_attempts = 0
|
order.payout.routing_attempts = 0
|
||||||
order.payout.save()
|
order.payout.save()
|
||||||
order.save()
|
|
||||||
|
|
||||||
order.save()
|
order.save()
|
||||||
return True, None
|
return True
|
||||||
|
|
||||||
def add_profile_rating(profile, rating):
|
def add_profile_rating(profile, rating):
|
||||||
"""adds a new rating to a user profile"""
|
"""adds a new rating to a user profile"""
|
||||||
@ -1087,7 +1256,6 @@ class Logics:
|
|||||||
|
|
||||||
def settle_escrow(order):
|
def settle_escrow(order):
|
||||||
"""Settles the trade escrow hold invoice"""
|
"""Settles the trade escrow hold invoice"""
|
||||||
# TODO ERROR HANDLING
|
|
||||||
if LNNode.settle_hold_invoice(order.trade_escrow.preimage):
|
if LNNode.settle_hold_invoice(order.trade_escrow.preimage):
|
||||||
order.trade_escrow.status = LNPayment.Status.SETLED
|
order.trade_escrow.status = LNPayment.Status.SETLED
|
||||||
order.trade_escrow.save()
|
order.trade_escrow.save()
|
||||||
@ -1095,7 +1263,6 @@ class Logics:
|
|||||||
|
|
||||||
def settle_bond(bond):
|
def settle_bond(bond):
|
||||||
"""Settles the bond hold invoice"""
|
"""Settles the bond hold invoice"""
|
||||||
# TODO ERROR HANDLING
|
|
||||||
if LNNode.settle_hold_invoice(bond.preimage):
|
if LNNode.settle_hold_invoice(bond.preimage):
|
||||||
bond.status = LNPayment.Status.SETLED
|
bond.status = LNPayment.Status.SETLED
|
||||||
bond.save()
|
bond.save()
|
||||||
@ -1151,6 +1318,35 @@ class Logics:
|
|||||||
else:
|
else:
|
||||||
raise e
|
raise e
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def pay_buyer(cls, order):
|
||||||
|
'''Pays buyer invoice or onchain address'''
|
||||||
|
|
||||||
|
# Pay to buyer invoice
|
||||||
|
if not order.is_swap:
|
||||||
|
##### Background process "follow_invoices" will try to pay this invoice until success
|
||||||
|
order.status = Order.Status.PAY
|
||||||
|
order.payout.status = LNPayment.Status.FLIGHT
|
||||||
|
order.payout.save()
|
||||||
|
order.save()
|
||||||
|
send_message.delay(order.id,'trade_successful')
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Pay onchain to address
|
||||||
|
else:
|
||||||
|
if not order.payout_tx.status == OnchainPayment.Status.VALID:
|
||||||
|
return False
|
||||||
|
|
||||||
|
valid = LNNode.pay_onchain(order.payout_tx)
|
||||||
|
if valid:
|
||||||
|
order.payout_tx.status = OnchainPayment.Status.MEMPO
|
||||||
|
order.payout_tx.save()
|
||||||
|
order.status = Order.Status.SUC
|
||||||
|
order.save()
|
||||||
|
send_message.delay(order.id,'trade_successful')
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def confirm_fiat(cls, order, user):
|
def confirm_fiat(cls, order, user):
|
||||||
"""If Order is in the CHAT states:
|
"""If Order is in the CHAT states:
|
||||||
@ -1159,7 +1355,7 @@ class Logics:
|
|||||||
|
|
||||||
if (order.status == Order.Status.CHA
|
if (order.status == Order.Status.CHA
|
||||||
or order.status == Order.Status.FSE
|
or order.status == Order.Status.FSE
|
||||||
): # TODO Alternatively, if all collateral is locked? test out
|
):
|
||||||
|
|
||||||
# If buyer, settle escrow and mark fiat sent
|
# If buyer, settle escrow and mark fiat sent
|
||||||
if cls.is_buyer(order, user):
|
if cls.is_buyer(order, user):
|
||||||
@ -1175,30 +1371,24 @@ class Logics:
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Make sure the trade escrow is at least as big as the buyer invoice
|
# Make sure the trade escrow is at least as big as the buyer invoice
|
||||||
if order.trade_escrow.num_satoshis <= order.payout.num_satoshis:
|
num_satoshis = order.payout_tx.num_satoshis if order.is_swap else order.payout.num_satoshis
|
||||||
|
if order.trade_escrow.num_satoshis <= num_satoshis:
|
||||||
return False, {
|
return False, {
|
||||||
"bad_request":
|
"bad_request":
|
||||||
"Woah, something broke badly. Report in the public channels, or open a Github Issue."
|
"Woah, something broke badly. Report in the public channels, or open a Github Issue."
|
||||||
}
|
}
|
||||||
|
|
||||||
if cls.settle_escrow(
|
# !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
|
||||||
order
|
if cls.settle_escrow(order):
|
||||||
): ##### !!! KEY LINE - SETTLES THE TRADE ESCROW !!!
|
|
||||||
order.trade_escrow.status = LNPayment.Status.SETLED
|
order.trade_escrow.status = LNPayment.Status.SETLED
|
||||||
|
|
||||||
# Double check the escrow is settled.
|
# Double check the escrow is settled.
|
||||||
if LNNode.double_check_htlc_is_settled(
|
if LNNode.double_check_htlc_is_settled(order.trade_escrow.payment_hash):
|
||||||
order.trade_escrow.payment_hash):
|
# RETURN THE BONDS
|
||||||
# RETURN THE BONDS // Probably best also do it even if payment failed
|
|
||||||
cls.return_bond(order.taker_bond)
|
cls.return_bond(order.taker_bond)
|
||||||
cls.return_bond(order.maker_bond)
|
cls.return_bond(order.maker_bond)
|
||||||
##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
|
##### !!! KEY LINE - PAYS THE BUYER INVOICE !!!
|
||||||
##### Background process "follow_invoices" will try to pay this invoice until success
|
cls.pay_buyer(order)
|
||||||
order.status = Order.Status.PAY
|
|
||||||
order.payout.status = LNPayment.Status.FLIGHT
|
|
||||||
order.payout.save()
|
|
||||||
order.save()
|
|
||||||
send_message.delay(order.id,'trade_successful')
|
|
||||||
|
|
||||||
# Add referral rewards (safe)
|
# Add referral rewards (safe)
|
||||||
try:
|
try:
|
||||||
|
118
api/models.py
118
api/models.py
@ -17,8 +17,11 @@ from decouple import config
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import json
|
import json
|
||||||
|
|
||||||
|
from control.models import BalanceLog
|
||||||
|
|
||||||
MIN_TRADE = int(config("MIN_TRADE"))
|
MIN_TRADE = int(config("MIN_TRADE"))
|
||||||
MAX_TRADE = int(config("MAX_TRADE"))
|
MAX_TRADE = int(config("MAX_TRADE"))
|
||||||
|
MIN_SWAP_AMOUNT = int(config("MIN_SWAP_AMOUNT"))
|
||||||
FEE = float(config("FEE"))
|
FEE = float(config("FEE"))
|
||||||
DEFAULT_BOND_SIZE = float(config("DEFAULT_BOND_SIZE"))
|
DEFAULT_BOND_SIZE = float(config("DEFAULT_BOND_SIZE"))
|
||||||
|
|
||||||
@ -118,7 +121,7 @@ class LNPayment(models.Model):
|
|||||||
blank=True)
|
blank=True)
|
||||||
num_satoshis = models.PositiveBigIntegerField(validators=[
|
num_satoshis = models.PositiveBigIntegerField(validators=[
|
||||||
MinValueValidator(100),
|
MinValueValidator(100),
|
||||||
MaxValueValidator(MAX_TRADE * (1 + DEFAULT_BOND_SIZE + FEE)),
|
MaxValueValidator(1.5 * MAX_TRADE),
|
||||||
])
|
])
|
||||||
# Fee in sats with mSats decimals fee_msat
|
# Fee in sats with mSats decimals fee_msat
|
||||||
fee = models.DecimalField(max_digits=10, decimal_places=3, default=0, null=False, blank=False)
|
fee = models.DecimalField(max_digits=10, decimal_places=3, default=0, null=False, blank=False)
|
||||||
@ -163,6 +166,106 @@ class LNPayment(models.Model):
|
|||||||
# We created a truncated property for display 'hash'
|
# We created a truncated property for display 'hash'
|
||||||
return truncatechars(self.payment_hash, 10)
|
return truncatechars(self.payment_hash, 10)
|
||||||
|
|
||||||
|
class OnchainPayment(models.Model):
|
||||||
|
|
||||||
|
class Concepts(models.IntegerChoices):
|
||||||
|
PAYBUYER = 3, "Payment to buyer"
|
||||||
|
|
||||||
|
class Status(models.IntegerChoices):
|
||||||
|
CREAT = 0, "Created" # User was given platform fees and suggested mining fees
|
||||||
|
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,
|
||||||
|
null=False,
|
||||||
|
default=Concepts.PAYBUYER)
|
||||||
|
status = models.PositiveSmallIntegerField(choices=Status.choices,
|
||||||
|
null=False,
|
||||||
|
default=Status.CREAT)
|
||||||
|
|
||||||
|
# payment info
|
||||||
|
address = models.CharField(max_length=100,
|
||||||
|
unique=False,
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
blank=True)
|
||||||
|
|
||||||
|
txid = models.CharField(max_length=64,
|
||||||
|
unique=True,
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
|
blank=True)
|
||||||
|
|
||||||
|
num_satoshis = models.PositiveBigIntegerField(null=True,
|
||||||
|
validators=[
|
||||||
|
MinValueValidator(0.5 * MIN_SWAP_AMOUNT),
|
||||||
|
MaxValueValidator(1.5 * MAX_TRADE),
|
||||||
|
])
|
||||||
|
sent_satoshis = models.PositiveBigIntegerField(null=True,
|
||||||
|
validators=[
|
||||||
|
MinValueValidator(0.5 * MIN_SWAP_AMOUNT),
|
||||||
|
MaxValueValidator(1.5 * MAX_TRADE),
|
||||||
|
])
|
||||||
|
# fee in sats/vbyte with mSats decimals fee_msat
|
||||||
|
suggested_mining_fee_rate = models.DecimalField(max_digits=6,
|
||||||
|
decimal_places=3,
|
||||||
|
default=1.05,
|
||||||
|
null=False,
|
||||||
|
blank=False)
|
||||||
|
mining_fee_rate = models.DecimalField(max_digits=6,
|
||||||
|
decimal_places=3,
|
||||||
|
default=1.05,
|
||||||
|
null=False,
|
||||||
|
blank=False)
|
||||||
|
mining_fee_sats = models.PositiveBigIntegerField(default=0,
|
||||||
|
null=False,
|
||||||
|
blank=False)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
null=True,
|
||||||
|
default=get_balance)
|
||||||
|
|
||||||
|
swap_fee_rate = models.DecimalField(max_digits=4,
|
||||||
|
decimal_places=2,
|
||||||
|
default=float(config("MIN_SWAP_FEE"))*100,
|
||||||
|
null=False,
|
||||||
|
blank=False)
|
||||||
|
|
||||||
|
created_at = models.DateTimeField(default=timezone.now)
|
||||||
|
|
||||||
|
# involved parties
|
||||||
|
receiver = models.ForeignKey(User,
|
||||||
|
related_name="tx_receiver",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
default=None)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.txid:
|
||||||
|
txname = str(self.txid)[:8]
|
||||||
|
else:
|
||||||
|
txname = str(self.id)
|
||||||
|
|
||||||
|
return f"TX-{txname}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
verbose_name = "Onchain payment"
|
||||||
|
verbose_name_plural = "Onchain payments"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hash(self):
|
||||||
|
# Display txid as 'hash' truncated
|
||||||
|
return truncatechars(self.txid, 10)
|
||||||
|
|
||||||
class Order(models.Model):
|
class Order(models.Model):
|
||||||
|
|
||||||
@ -356,10 +459,21 @@ class Order(models.Model):
|
|||||||
default=None,
|
default=None,
|
||||||
blank=True,
|
blank=True,
|
||||||
)
|
)
|
||||||
|
# is buyer payout a LN invoice (false) or on chain address (true)
|
||||||
|
is_swap = models.BooleanField(default=False, null=False)
|
||||||
# buyer payment LN invoice
|
# buyer payment LN invoice
|
||||||
payout = models.OneToOneField(
|
payout = models.OneToOneField(
|
||||||
LNPayment,
|
LNPayment,
|
||||||
related_name="order_paid",
|
related_name="order_paid_LN",
|
||||||
|
on_delete=models.SET_NULL,
|
||||||
|
null=True,
|
||||||
|
default=None,
|
||||||
|
blank=True,
|
||||||
|
)
|
||||||
|
# buyer payment address
|
||||||
|
payout_tx = models.OneToOneField(
|
||||||
|
OnchainPayment,
|
||||||
|
related_name="order_paid_TX",
|
||||||
on_delete=models.SET_NULL,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
default=None,
|
default=None,
|
||||||
|
@ -54,6 +54,10 @@ class UpdateOrderSerializer(serializers.Serializer):
|
|||||||
allow_null=True,
|
allow_null=True,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
default=None)
|
default=None)
|
||||||
|
address = serializers.CharField(max_length=100,
|
||||||
|
allow_null=True,
|
||||||
|
allow_blank=True,
|
||||||
|
default=None)
|
||||||
statement = serializers.CharField(max_length=10000,
|
statement = serializers.CharField(max_length=10000,
|
||||||
allow_null=True,
|
allow_null=True,
|
||||||
allow_blank=True,
|
allow_blank=True,
|
||||||
@ -63,6 +67,7 @@ class UpdateOrderSerializer(serializers.Serializer):
|
|||||||
"pause",
|
"pause",
|
||||||
"take",
|
"take",
|
||||||
"update_invoice",
|
"update_invoice",
|
||||||
|
"update_address",
|
||||||
"submit_statement",
|
"submit_statement",
|
||||||
"dispute",
|
"dispute",
|
||||||
"cancel",
|
"cancel",
|
||||||
@ -79,6 +84,7 @@ class UpdateOrderSerializer(serializers.Serializer):
|
|||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
amount = serializers.DecimalField(max_digits=18, decimal_places=8, allow_null=True, required=False, default=None)
|
amount = serializers.DecimalField(max_digits=18, decimal_places=8, allow_null=True, required=False, default=None)
|
||||||
|
mining_fee_rate = serializers.DecimalField(max_digits=6, decimal_places=3, allow_null=True, required=False, default=None)
|
||||||
|
|
||||||
class UserGenSerializer(serializers.Serializer):
|
class UserGenSerializer(serializers.Serializer):
|
||||||
# Mandatory fields
|
# Mandatory fields
|
||||||
|
10
api/tasks.py
10
api/tasks.py
@ -78,14 +78,16 @@ def follow_send_payment(hash):
|
|||||||
lnpayment.num_satoshis *
|
lnpayment.num_satoshis *
|
||||||
float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
|
float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
|
||||||
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
|
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
|
||||||
)) # 200 ppm or 10 sats
|
)) # 1000 ppm or 10 sats
|
||||||
|
timeout_seconds = int(config("PAYOUT_TIMEOUT_SECONDS"))
|
||||||
|
|
||||||
request = LNNode.routerrpc.SendPaymentRequest(
|
request = LNNode.routerrpc.SendPaymentRequest(
|
||||||
payment_request=lnpayment.invoice,
|
payment_request=lnpayment.invoice,
|
||||||
fee_limit_sat=fee_limit_sat,
|
fee_limit_sat=fee_limit_sat,
|
||||||
timeout_seconds=75,
|
timeout_seconds=timeout_seconds,
|
||||||
) # time out payment in 75 seconds
|
)
|
||||||
|
|
||||||
order = lnpayment.order_paid
|
order = lnpayment.order_paid_LN
|
||||||
try:
|
try:
|
||||||
for response in LNNode.routerstub.SendPaymentV2(request,
|
for response in LNNode.routerstub.SendPaymentV2(request,
|
||||||
metadata=[
|
metadata=[
|
||||||
|
34
api/utils.py
34
api/utils.py
@ -1,8 +1,7 @@
|
|||||||
import requests, ring, os
|
import requests, ring, os
|
||||||
from decouple import config
|
from decouple import config
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import requests
|
import coinaddrvalidator as addr
|
||||||
|
|
||||||
from api.models import Order
|
from api.models import Order
|
||||||
|
|
||||||
def get_tor_session():
|
def get_tor_session():
|
||||||
@ -12,6 +11,37 @@ def get_tor_session():
|
|||||||
'https': 'socks5://127.0.0.1:9050'}
|
'https': 'socks5://127.0.0.1:9050'}
|
||||||
return session
|
return session
|
||||||
|
|
||||||
|
def validate_onchain_address(address):
|
||||||
|
'''
|
||||||
|
Validates an onchain address
|
||||||
|
'''
|
||||||
|
|
||||||
|
validation = addr.validate('btc', address.encode('utf-8'))
|
||||||
|
|
||||||
|
if not validation.valid:
|
||||||
|
return False, {
|
||||||
|
"bad_address":
|
||||||
|
"Does not look like a valid address"
|
||||||
|
}
|
||||||
|
|
||||||
|
NETWORK = str(config('NETWORK'))
|
||||||
|
if NETWORK == 'mainnet':
|
||||||
|
if validation.network == 'main':
|
||||||
|
return True, None
|
||||||
|
else:
|
||||||
|
return False, {
|
||||||
|
"bad_address":
|
||||||
|
"This is not a bitcoin mainnet address"
|
||||||
|
}
|
||||||
|
elif NETWORK == 'testnet':
|
||||||
|
if validation.network == 'test':
|
||||||
|
return True, None
|
||||||
|
else:
|
||||||
|
return False, {
|
||||||
|
"bad_address":
|
||||||
|
"This is not a bitcoin testnet address"
|
||||||
|
}
|
||||||
|
|
||||||
market_cache = {}
|
market_cache = {}
|
||||||
@ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds
|
@ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds
|
||||||
def get_exchange_rates(currencies):
|
def get_exchange_rates(currencies):
|
||||||
|
33
api/views.py
33
api/views.py
@ -12,8 +12,8 @@ from django.views.decorators.csrf import csrf_exempt
|
|||||||
from django.contrib.auth.models import User
|
from django.contrib.auth.models import User
|
||||||
|
|
||||||
from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer, UserGenSerializer
|
from api.serializers import ListOrderSerializer, MakeOrderSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer, UserGenSerializer
|
||||||
from api.models import LNPayment, MarketTick, Order, Currency, Profile
|
from api.models import LNPayment, MarketTick, OnchainPayment, Order, Currency, Profile
|
||||||
from control.models import AccountingDay
|
from control.models import AccountingDay, BalanceLog
|
||||||
from api.logics import Logics
|
from api.logics import Logics
|
||||||
from api.messages import Telegram
|
from api.messages import Telegram
|
||||||
from secrets import token_urlsafe
|
from secrets import token_urlsafe
|
||||||
@ -337,7 +337,7 @@ class OrderView(viewsets.ViewSet):
|
|||||||
elif data["is_buyer"] and (order.status == Order.Status.WF2
|
elif data["is_buyer"] and (order.status == Order.Status.WF2
|
||||||
or order.status == Order.Status.WFI):
|
or order.status == Order.Status.WFI):
|
||||||
|
|
||||||
# If the two bonds are locked, reply with an AMOUNT so he can send the buyer invoice.
|
# If the two bonds are locked, reply with an AMOUNT and onchain swap cost so he can send the buyer invoice/address.
|
||||||
if (order.maker_bond.status == order.taker_bond.status ==
|
if (order.maker_bond.status == order.taker_bond.status ==
|
||||||
LNPayment.Status.LOCKED):
|
LNPayment.Status.LOCKED):
|
||||||
valid, context = Logics.payout_amount(order, request.user)
|
valid, context = Logics.payout_amount(order, request.user)
|
||||||
@ -399,6 +399,20 @@ class OrderView(viewsets.ViewSet):
|
|||||||
if order.status == Order.Status.EXP:
|
if order.status == Order.Status.EXP:
|
||||||
data["expiry_reason"] = order.expiry_reason
|
data["expiry_reason"] = order.expiry_reason
|
||||||
data["expiry_message"] = Order.ExpiryReasons(order.expiry_reason).label
|
data["expiry_message"] = Order.ExpiryReasons(order.expiry_reason).label
|
||||||
|
|
||||||
|
# If status is 'Succes' add final stats and txid if it is a swap
|
||||||
|
if order.status == Order.Status.SUC:
|
||||||
|
# TODO: add summary of order for buyer/sellers: sats in/out, fee paid, total time? etc
|
||||||
|
# If buyer and is a swap, add TXID
|
||||||
|
if Logics.is_buyer(order,request.user):
|
||||||
|
if order.is_swap:
|
||||||
|
data["num_satoshis"] = order.payout_tx.num_satoshis
|
||||||
|
data["sent_satoshis"] = order.payout_tx.sent_satoshis
|
||||||
|
if order.payout_tx.status in [OnchainPayment.Status.MEMPO, OnchainPayment.Status.CONFI]:
|
||||||
|
data["txid"] = order.payout_tx.txid
|
||||||
|
data["network"] = str(config("NETWORK"))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return Response(data, status.HTTP_200_OK)
|
return Response(data, status.HTTP_200_OK)
|
||||||
|
|
||||||
@ -416,9 +430,11 @@ class OrderView(viewsets.ViewSet):
|
|||||||
order = Order.objects.get(id=order_id)
|
order = Order.objects.get(id=order_id)
|
||||||
|
|
||||||
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
|
# action is either 1)'take', 2)'confirm', 3)'cancel', 4)'dispute' , 5)'update_invoice'
|
||||||
# 6)'submit_statement' (in dispute), 7)'rate_user' , 'rate_platform'
|
# 5.b)'update_address' 6)'submit_statement' (in dispute), 7)'rate_user' , 8)'rate_platform'
|
||||||
action = serializer.data.get("action")
|
action = serializer.data.get("action")
|
||||||
invoice = serializer.data.get("invoice")
|
invoice = serializer.data.get("invoice")
|
||||||
|
address = serializer.data.get("address")
|
||||||
|
mining_fee_rate = serializer.data.get("mining_fee_rate")
|
||||||
statement = serializer.data.get("statement")
|
statement = serializer.data.get("statement")
|
||||||
rating = serializer.data.get("rating")
|
rating = serializer.data.get("rating")
|
||||||
|
|
||||||
@ -464,6 +480,13 @@ class OrderView(viewsets.ViewSet):
|
|||||||
invoice)
|
invoice)
|
||||||
if not valid:
|
if not valid:
|
||||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
|
# 2.b) If action is 'update address'
|
||||||
|
if action == "update_address":
|
||||||
|
valid, context = Logics.update_address(order, request.user,
|
||||||
|
address, mining_fee_rate)
|
||||||
|
if not valid:
|
||||||
|
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||||
|
|
||||||
# 3) If action is cancel
|
# 3) If action is cancel
|
||||||
elif action == "cancel":
|
elif action == "cancel":
|
||||||
@ -870,6 +893,8 @@ class InfoView(ListAPIView):
|
|||||||
context["taker_fee"] = float(config("FEE"))*(1 - float(config("MAKER_FEE_SPLIT")))
|
context["taker_fee"] = float(config("FEE"))*(1 - float(config("MAKER_FEE_SPLIT")))
|
||||||
context["bond_size"] = float(config("DEFAULT_BOND_SIZE"))
|
context["bond_size"] = float(config("DEFAULT_BOND_SIZE"))
|
||||||
|
|
||||||
|
context["current_swap_fee_rate"] = Logics.compute_swap_fee_rate(BalanceLog.objects.latest('time'))
|
||||||
|
|
||||||
if request.user.is_authenticated:
|
if request.user.is_authenticated:
|
||||||
context["nickname"] = request.user.username
|
context["nickname"] = request.user.username
|
||||||
context["referral_code"] = str(request.user.profile.referral_code)
|
context["referral_code"] = str(request.user.profile.referral_code)
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from control.models import AccountingDay, AccountingMonth, Dispute
|
from control.models import AccountingDay, BalanceLog
|
||||||
from import_export.admin import ImportExportModelAdmin
|
from import_export.admin import ImportExportModelAdmin
|
||||||
|
|
||||||
# Register your models here.
|
# Register your models here.
|
||||||
@ -17,6 +17,7 @@ class AccountingDayAdmin(ImportExportModelAdmin):
|
|||||||
"inflow",
|
"inflow",
|
||||||
"outflow",
|
"outflow",
|
||||||
"routing_fees",
|
"routing_fees",
|
||||||
|
"mining_fees",
|
||||||
"cashflow",
|
"cashflow",
|
||||||
"outstanding_earned_rewards",
|
"outstanding_earned_rewards",
|
||||||
"outstanding_pending_disputes",
|
"outstanding_pending_disputes",
|
||||||
@ -28,26 +29,32 @@ class AccountingDayAdmin(ImportExportModelAdmin):
|
|||||||
change_links = ["day"]
|
change_links = ["day"]
|
||||||
search_fields = ["day"]
|
search_fields = ["day"]
|
||||||
|
|
||||||
@admin.register(AccountingMonth)
|
@admin.register(BalanceLog)
|
||||||
class AccountingMonthAdmin(ImportExportModelAdmin):
|
class BalanceLogAdmin(ImportExportModelAdmin):
|
||||||
|
|
||||||
list_display = (
|
list_display = (
|
||||||
"month",
|
"time",
|
||||||
"contracted",
|
"total",
|
||||||
"num_contracts",
|
"onchain_fraction",
|
||||||
"net_settled",
|
"onchain_total",
|
||||||
"net_paid",
|
"onchain_confirmed",
|
||||||
"net_balance",
|
"onchain_unconfirmed",
|
||||||
"inflow",
|
"ln_local",
|
||||||
"outflow",
|
"ln_remote",
|
||||||
"routing_fees",
|
"ln_local_unsettled",
|
||||||
"cashflow",
|
"ln_remote_unsettled",
|
||||||
"outstanding_earned_rewards",
|
|
||||||
"outstanding_pending_disputes",
|
|
||||||
"lifetime_rewards_claimed",
|
|
||||||
"outstanding_earned_rewards",
|
|
||||||
"pending_disputes",
|
|
||||||
"rewards_claimed",
|
|
||||||
)
|
)
|
||||||
change_links = ["month"]
|
readonly_fields = [
|
||||||
search_fields = ["month"]
|
"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"]
|
@ -1,6 +1,8 @@
|
|||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
|
from api.lightning.node import LNNode
|
||||||
|
|
||||||
class AccountingDay(models.Model):
|
class AccountingDay(models.Model):
|
||||||
day = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False)
|
day = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False)
|
||||||
|
|
||||||
@ -21,6 +23,8 @@ class AccountingDay(models.Model):
|
|||||||
outflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
outflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
||||||
# Total cost in routing fees
|
# Total cost in routing fees
|
||||||
routing_fees = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
routing_fees = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
||||||
|
# Total cost in minig fees
|
||||||
|
mining_fees = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
||||||
# Total inflows minus outflows and routing fees
|
# Total inflows minus outflows and routing fees
|
||||||
cashflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
cashflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
||||||
# Balance on earned rewards (referral rewards, slashed bonds and solved disputes)
|
# Balance on earned rewards (referral rewards, slashed bonds and solved disputes)
|
||||||
@ -36,42 +40,42 @@ class AccountingDay(models.Model):
|
|||||||
# Rewards claimed on day
|
# Rewards claimed on day
|
||||||
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):
|
||||||
|
|
||||||
|
def get_total():
|
||||||
|
return LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance']
|
||||||
|
def get_frac():
|
||||||
|
return LNNode.wallet_balance()['total_balance'] / (LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_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)
|
||||||
|
|
||||||
class AccountingMonth(models.Model):
|
# Every field is denominated in Sats
|
||||||
month = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False)
|
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)
|
||||||
|
|
||||||
# Every field is denominated in Sats with (3 decimals for millisats)
|
def __str__(self):
|
||||||
# Total volume contracted
|
return f"Balance at {self.time.strftime('%d/%m/%Y %H:%M:%S')}"
|
||||||
contracted = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Number of contracts
|
|
||||||
num_contracts = models.BigIntegerField(default=0, null=False, blank=False)
|
|
||||||
# Net volume of trading invoices settled (excludes disputes)
|
|
||||||
net_settled = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Net volume of trading invoices paid (excludes rewards and disputes)
|
|
||||||
net_paid = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Sum of net settled and net paid
|
|
||||||
net_balance = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Total volume of invoices settled
|
|
||||||
inflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Total volume of invoices paid
|
|
||||||
outflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Total cost in routing fees
|
|
||||||
routing_fees = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Total inflows minus outflows and routing fees
|
|
||||||
cashflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Balance on earned rewards (referral rewards, slashed bonds and solved disputes)
|
|
||||||
outstanding_earned_rewards = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Balance on pending disputes (not resolved yet)
|
|
||||||
outstanding_pending_disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Rewards claimed lifetime
|
|
||||||
lifetime_rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Balance change from last day on earned rewards (referral rewards, slashed bonds and solved disputes)
|
|
||||||
earned_rewards = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Balance change on pending disputes (not resolved yet)
|
|
||||||
pending_disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
# Rewards claimed on day
|
|
||||||
rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
|
||||||
|
|
||||||
class Dispute(models.Model):
|
class Dispute(models.Model):
|
||||||
pass
|
pass
|
@ -1,10 +1,4 @@
|
|||||||
from celery import shared_task
|
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")
|
@shared_task(name="do_accounting")
|
||||||
def do_accounting():
|
def do_accounting():
|
||||||
@ -12,6 +6,13 @@ def do_accounting():
|
|||||||
Does all accounting from the beginning of time
|
Does all accounting from the beginning of time
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
from api.models import Order, LNPayment, OnchainPayment, 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_payments = LNPayment.objects.all()
|
||||||
all_ticks = MarketTick.objects.all()
|
all_ticks = MarketTick.objects.all()
|
||||||
today = timezone.now().date()
|
today = timezone.now().date()
|
||||||
@ -35,14 +36,16 @@ def do_accounting():
|
|||||||
result = {}
|
result = {}
|
||||||
while day <= today:
|
while day <= today:
|
||||||
day_payments = all_payments.filter(created_at__gte=day,created_at__lte=day+timedelta(days=1))
|
day_payments = all_payments.filter(created_at__gte=day,created_at__lte=day+timedelta(days=1))
|
||||||
|
day_onchain_payments = OnchainPayment.objects.filter(created_at__gte=day,created_at__lte=day+timedelta(days=1))
|
||||||
day_ticks = all_ticks.filter(timestamp__gte=day,timestamp__lte=day+timedelta(days=1))
|
day_ticks = all_ticks.filter(timestamp__gte=day,timestamp__lte=day+timedelta(days=1))
|
||||||
|
|
||||||
# Coarse accounting based on LNpayment objects
|
# Coarse accounting based on LNpayment and OnchainPayment objects
|
||||||
contracted = day_ticks.aggregate(Sum('volume'))['volume__sum']
|
contracted = day_ticks.aggregate(Sum('volume'))['volume__sum']
|
||||||
num_contracts = day_ticks.count()
|
num_contracts = day_ticks.count()
|
||||||
inflow = day_payments.filter(type=LNPayment.Types.HOLD,status=LNPayment.Status.SETLED).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
|
inflow = day_payments.filter(type=LNPayment.Types.HOLD,status=LNPayment.Status.SETLED).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
|
||||||
outflow = day_payments.filter(type=LNPayment.Types.NORM,status=LNPayment.Status.SUCCED).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
|
outflow = day_payments.filter(type=LNPayment.Types.NORM,status=LNPayment.Status.SUCCED).aggregate(Sum('num_satoshis'))['num_satoshis__sum'] + day_onchain_payments.filter(status__in=[OnchainPayment.Status.MEMPO,OnchainPayment.Status.CONFI]).aggregate(Sum('sent_satoshis'))['sent_satoshis__sum']
|
||||||
routing_fees = day_payments.filter(type=LNPayment.Types.NORM,status=LNPayment.Status.SUCCED).aggregate(Sum('fee'))['fee__sum']
|
routing_fees = day_payments.filter(type=LNPayment.Types.NORM,status=LNPayment.Status.SUCCED).aggregate(Sum('fee'))['fee__sum']
|
||||||
|
mining_fees = day_onchain_payments.filter(status__in=[OnchainPayment.Status.MEMPO,OnchainPayment.Status.CONFI]).aggregate(Sum('mining_fee_sats'))['mining_fee_sats__sum']
|
||||||
rewards_claimed = day_payments.filter(type=LNPayment.Types.NORM,concept=LNPayment.Concepts.WITHREWA,status=LNPayment.Status.SUCCED).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
|
rewards_claimed = day_payments.filter(type=LNPayment.Types.NORM,concept=LNPayment.Concepts.WITHREWA,status=LNPayment.Status.SUCCED).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
|
||||||
|
|
||||||
contracted = 0 if contracted == None else contracted
|
contracted = 0 if contracted == None else contracted
|
||||||
@ -58,6 +61,7 @@ def do_accounting():
|
|||||||
inflow = inflow,
|
inflow = inflow,
|
||||||
outflow = outflow,
|
outflow = outflow,
|
||||||
routing_fees = routing_fees,
|
routing_fees = routing_fees,
|
||||||
|
mining_fees = mining_fees,
|
||||||
cashflow = inflow - outflow - routing_fees,
|
cashflow = inflow - outflow - routing_fees,
|
||||||
rewards_claimed = rewards_claimed,
|
rewards_claimed = rewards_claimed,
|
||||||
)
|
)
|
||||||
@ -67,11 +71,19 @@ def do_accounting():
|
|||||||
payouts = day_payments.filter(type=LNPayment.Types.NORM,concept=LNPayment.Concepts.PAYBUYER, status=LNPayment.Status.SUCCED)
|
payouts = day_payments.filter(type=LNPayment.Types.NORM,concept=LNPayment.Concepts.PAYBUYER, status=LNPayment.Status.SUCCED)
|
||||||
escrows_settled = 0
|
escrows_settled = 0
|
||||||
payouts_paid = 0
|
payouts_paid = 0
|
||||||
routing_cost = 0
|
costs = 0
|
||||||
for payout in payouts:
|
for payout in payouts:
|
||||||
escrows_settled += payout.order_paid.trade_escrow.num_satoshis
|
escrows_settled += payout.order_paid_LN.trade_escrow.num_satoshis
|
||||||
payouts_paid += payout.num_satoshis
|
payouts_paid += payout.num_satoshis
|
||||||
routing_cost += payout.fee
|
costs += payout.fee
|
||||||
|
|
||||||
|
# Same for orders that use onchain payments.
|
||||||
|
payouts_tx = day_onchain_payments.filter(status__in=[OnchainPayment.Status.MEMPO,OnchainPayment.Status.CONFI])
|
||||||
|
for payout_tx in payouts_tx:
|
||||||
|
escrows_settled += payout_tx.order_paid_TX.trade_escrow.num_satoshis
|
||||||
|
payouts_paid += payout_tx.sent_satoshis
|
||||||
|
costs += payout_tx.fee
|
||||||
|
|
||||||
|
|
||||||
# account for those orders where bonds were lost
|
# account for those orders where bonds were lost
|
||||||
# + Settled bonds / bond_split
|
# + Settled bonds / bond_split
|
||||||
@ -83,7 +95,7 @@ def do_accounting():
|
|||||||
collected_slashed_bonds = 0
|
collected_slashed_bonds = 0
|
||||||
|
|
||||||
accounted_day.net_settled = escrows_settled + collected_slashed_bonds
|
accounted_day.net_settled = escrows_settled + collected_slashed_bonds
|
||||||
accounted_day.net_paid = payouts_paid + routing_cost
|
accounted_day.net_paid = payouts_paid + costs
|
||||||
accounted_day.net_balance = float(accounted_day.net_settled) - float(accounted_day.net_paid)
|
accounted_day.net_balance = float(accounted_day.net_settled) - float(accounted_day.net_paid)
|
||||||
|
|
||||||
# Differential accounting based on change of outstanding states and disputes unreslved
|
# Differential accounting based on change of outstanding states and disputes unreslved
|
||||||
@ -109,4 +121,15 @@ def do_accounting():
|
|||||||
result[str(day)]={'contracted':contracted,'inflow':inflow,'outflow':outflow}
|
result[str(day)]={'contracted':contracted,'inflow':inflow,'outflow':outflow}
|
||||||
day = day + timedelta(days=1)
|
day = day + timedelta(days=1)
|
||||||
|
|
||||||
return result
|
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
|
@ -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
|
||||||
|
@ -400,6 +400,7 @@ bottomBarPhone =()=>{
|
|||||||
lastDayNonkycBtcPremium={this.state.last_day_nonkyc_btc_premium}
|
lastDayNonkycBtcPremium={this.state.last_day_nonkyc_btc_premium}
|
||||||
makerFee={this.state.maker_fee}
|
makerFee={this.state.maker_fee}
|
||||||
takerFee={this.state.taker_fee}
|
takerFee={this.state.taker_fee}
|
||||||
|
swapFeeRate={this.state.current_swap_fee_rate}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ProfileDialog
|
<ProfileDialog
|
||||||
|
@ -19,6 +19,7 @@ import SmartToyIcon from '@mui/icons-material/SmartToy';
|
|||||||
import PercentIcon from '@mui/icons-material/Percent';
|
import PercentIcon from '@mui/icons-material/Percent';
|
||||||
import PriceChangeIcon from '@mui/icons-material/PriceChange';
|
import PriceChangeIcon from '@mui/icons-material/PriceChange';
|
||||||
import BookIcon from '@mui/icons-material/Book';
|
import BookIcon from '@mui/icons-material/Book';
|
||||||
|
import LinkIcon from '@mui/icons-material/Link';
|
||||||
|
|
||||||
import { pn } from "../../utils/prettyNumbers";
|
import { pn } from "../../utils/prettyNumbers";
|
||||||
|
|
||||||
@ -32,6 +33,7 @@ type Props = {
|
|||||||
lastDayNonkycBtcPremium: number;
|
lastDayNonkycBtcPremium: number;
|
||||||
makerFee: number;
|
makerFee: number;
|
||||||
takerFee: number;
|
takerFee: number;
|
||||||
|
swapFeeRate: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ExchangeSummaryDialog = ({
|
const ExchangeSummaryDialog = ({
|
||||||
@ -44,8 +46,12 @@ const ExchangeSummaryDialog = ({
|
|||||||
lastDayNonkycBtcPremium,
|
lastDayNonkycBtcPremium,
|
||||||
makerFee,
|
makerFee,
|
||||||
takerFee,
|
takerFee,
|
||||||
|
swapFeeRate,
|
||||||
}: Props): JSX.Element => {
|
}: Props): JSX.Element => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
if (swapFeeRate === null || swapFeeRate === undefined) {
|
||||||
|
swapFeeRate = 0
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@ -160,6 +166,22 @@ const ExchangeSummaryDialog = ({
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
<ListItem >
|
||||||
|
<ListItemIcon>
|
||||||
|
<LinkIcon />
|
||||||
|
</ListItemIcon>
|
||||||
|
|
||||||
|
<ListItemText
|
||||||
|
primaryTypographyProps={{fontSize: '14px'}}
|
||||||
|
secondaryTypographyProps={{fontSize: '12px'}}
|
||||||
|
primary={`${swapFeeRate.toPrecision(3)}%`}
|
||||||
|
secondary={t("Current onchain payout fee")}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
</List>
|
</List>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { withTranslation, Trans} from "react-i18next";
|
import { withTranslation, Trans} from "react-i18next";
|
||||||
import { IconButton, Box, Link, Paper, Rating, Button, Tooltip, CircularProgress, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
|
import { Alert, AlertTitle, ToggleButtonGroup, ToggleButton, IconButton, Box, Link, Paper, Rating, Button, Tooltip, CircularProgress, Grid, Typography, TextField, List, ListItem, ListItemText, Divider, ListItemIcon, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle} from "@mui/material"
|
||||||
import QRCode from "react-qr-code";
|
import QRCode from "react-qr-code";
|
||||||
import Countdown, { zeroPad} from 'react-countdown';
|
import Countdown, { zeroPad} from 'react-countdown';
|
||||||
import Chat from "./EncryptedChat"
|
import Chat from "./EncryptedChat"
|
||||||
@ -18,6 +18,8 @@ import BalanceIcon from '@mui/icons-material/Balance';
|
|||||||
import ContentCopy from "@mui/icons-material/ContentCopy";
|
import ContentCopy from "@mui/icons-material/ContentCopy";
|
||||||
import PauseCircleIcon from '@mui/icons-material/PauseCircle';
|
import PauseCircleIcon from '@mui/icons-material/PauseCircle';
|
||||||
import PlayCircleIcon from '@mui/icons-material/PlayCircle';
|
import PlayCircleIcon from '@mui/icons-material/PlayCircle';
|
||||||
|
import BoltIcon from '@mui/icons-material/Bolt';
|
||||||
|
import LinkIcon from '@mui/icons-material/Link';
|
||||||
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
import AccountBalanceWalletIcon from '@mui/icons-material/AccountBalanceWallet';
|
||||||
import { NewTabIcon } from "./Icons";
|
import { NewTabIcon } from "./Icons";
|
||||||
|
|
||||||
@ -33,7 +35,11 @@ class TradeBox extends Component {
|
|||||||
openConfirmFiatReceived: false,
|
openConfirmFiatReceived: false,
|
||||||
openConfirmDispute: false,
|
openConfirmDispute: false,
|
||||||
openEnableTelegram: false,
|
openEnableTelegram: false,
|
||||||
|
receiveTab: 0,
|
||||||
|
address: '',
|
||||||
|
miningFee: 1.05,
|
||||||
badInvoice: false,
|
badInvoice: false,
|
||||||
|
badAddress: false,
|
||||||
badStatement: false,
|
badStatement: false,
|
||||||
qrscanner: false,
|
qrscanner: false,
|
||||||
}
|
}
|
||||||
@ -540,6 +546,42 @@ class TradeBox extends Component {
|
|||||||
& this.props.completeSetState(data));
|
& this.props.completeSetState(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleInputAddressChanged=(e)=>{
|
||||||
|
this.setState({
|
||||||
|
address: e.target.value,
|
||||||
|
badAddress: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleMiningFeeChanged=(e)=>{
|
||||||
|
var fee = e.target.value
|
||||||
|
if (fee > 50){
|
||||||
|
fee = 50
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setState({
|
||||||
|
miningFee: fee,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClickSubmitAddressButton=()=>{
|
||||||
|
this.setState({badInvoice:false});
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {'Content-Type':'application/json', 'X-CSRFToken': getCookie('csrftoken'),},
|
||||||
|
body: JSON.stringify({
|
||||||
|
'action':'update_address',
|
||||||
|
'address': this.state.address,
|
||||||
|
'mining_fee_rate': Math.max(1, this.state.miningFee),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
fetch('/api/order/' + '?order_id=' + this.props.data.id, requestOptions)
|
||||||
|
.then((response) => response.json())
|
||||||
|
.then((data) => this.setState({badAddress:data.bad_address})
|
||||||
|
& this.props.completeSetState(data));
|
||||||
|
}
|
||||||
|
|
||||||
handleInputDisputeChanged=(e)=>{
|
handleInputDisputeChanged=(e)=>{
|
||||||
this.setState({
|
this.setState({
|
||||||
statement: e.target.value,
|
statement: e.target.value,
|
||||||
@ -599,57 +641,153 @@ class TradeBox extends Component {
|
|||||||
{/* Make confirmation sound for HTLC received. */}
|
{/* Make confirmation sound for HTLC received. */}
|
||||||
{this.Sound("locked-invoice")}
|
{this.Sound("locked-invoice")}
|
||||||
<Typography color="primary" variant="subtitle1">
|
<Typography color="primary" variant="subtitle1">
|
||||||
<b> {t("Submit an invoice for {{amountSats}} Sats",{amountSats: pn(this.props.data.invoice_amount)})}
|
<b> {t("Submit payout info for {{amountSats}} Sats",{amountSats: pn(this.props.data.invoice_amount)})}
|
||||||
</b> {" " + this.stepXofY()}
|
</b> {" " + this.stepXofY()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
<List dense={true}>
|
||||||
|
<Divider/>
|
||||||
|
<ListItem>
|
||||||
|
<Typography variant="body2">
|
||||||
|
{t("Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC.",
|
||||||
|
{amountFiat: parseFloat(parseFloat(this.props.data.amount).toFixed(4)),
|
||||||
|
currencyCode: this.props.data.currencyCode})}
|
||||||
|
</Typography>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
|
||||||
<Grid item xs={12} align="left">
|
<Grid item xs={12} align="center">
|
||||||
<Typography variant="body2">
|
<ToggleButtonGroup
|
||||||
{t("The taker is committed! Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC. Please provide a valid invoice for {{amountSats}} Satoshis.",
|
value={this.state.receiveTab}
|
||||||
{amountFiat: parseFloat(parseFloat(this.props.data.amount).toFixed(4)),
|
exclusive >
|
||||||
currencyCode: this.props.data.currencyCode,
|
<ToggleButton value={0} disableRipple={true} onClick={() => this.setState({receiveTab:0})}>
|
||||||
amountSats: pn(this.props.data.invoice_amount)}
|
<div style={{display:'flex', alignItems:'center', justifyContent:'center', flexWrap:'wrap'}}><BoltIcon/> {t("Lightning")}</div>
|
||||||
)
|
</ToggleButton>
|
||||||
}
|
<ToggleButton value={1} disabled={!this.props.data.swap_allowed} onClick={() => this.setState({receiveTab:1, miningFee: parseFloat(this.props.data.suggested_mining_fee_rate)})} >
|
||||||
</Typography>
|
<div style={{display:'flex', alignItems:'center', justifyContent:'center', flexWrap:'wrap'}}><LinkIcon/> {t("Onchain")}</div>
|
||||||
|
</ToggleButton>
|
||||||
|
</ToggleButtonGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|
||||||
<Grid item xs={12} align="center">
|
{/* LIGHTNING PAYOUT TAB */}
|
||||||
{this.compatibleWalletsButton()}
|
<div style={{display: this.state.receiveTab == 0 ? '':'none'}}>
|
||||||
</Grid>
|
<div style={{height:15}}/>
|
||||||
|
<Grid container spacing={1}>
|
||||||
|
<Grid item xs={12} align="center">
|
||||||
|
<Typography variant="body2">
|
||||||
|
{t("Submit a valid invoice for {{amountSats}} Satoshis.",
|
||||||
|
{amountSats: pn(this.props.data.invoice_amount)})}
|
||||||
|
</Typography>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<TextField
|
{this.compatibleWalletsButton()}
|
||||||
error={this.state.badInvoice}
|
</Grid>
|
||||||
helperText={this.state.badInvoice ? t(this.state.badInvoice) : "" }
|
|
||||||
label={t("Payout Lightning Invoice")}
|
<Grid item xs={12} align="center">
|
||||||
required
|
<TextField
|
||||||
value={this.state.invoice}
|
error={this.state.badInvoice}
|
||||||
inputProps={{
|
helperText={this.state.badInvoice ? t(this.state.badInvoice) : "" }
|
||||||
style: {textAlign:"center"},
|
label={t("Payout Lightning Invoice")}
|
||||||
maxHeight: 200,
|
required
|
||||||
}}
|
value={this.state.invoice}
|
||||||
multiline
|
inputProps={{
|
||||||
minRows={5}
|
style: {textAlign:"center"},
|
||||||
maxRows={this.state.qrscanner ? 5 : 10}
|
maxHeight: 200,
|
||||||
onChange={this.handleInputInvoiceChanged}
|
}}
|
||||||
/>
|
multiline
|
||||||
</Grid>
|
minRows={4}
|
||||||
{this.state.qrscanner ?
|
maxRows={this.state.qrscanner ? 4 : 8}
|
||||||
<Grid item xs={12} align="center">
|
onChange={this.handleInputInvoiceChanged}
|
||||||
<QrReader
|
/>
|
||||||
delay={300}
|
</Grid>
|
||||||
onError={this.handleError}
|
{this.state.qrscanner ?
|
||||||
onScan={this.handleScan}
|
<Grid item xs={12} align="center">
|
||||||
style={{ width: '75%' }}
|
<QrReader
|
||||||
/>
|
delay={300}
|
||||||
</Grid>
|
onError={this.handleError}
|
||||||
: null }
|
onScan={this.handleScan}
|
||||||
<Grid item xs={12} align="center">
|
style={{ width: '75%' }}
|
||||||
<IconButton><QrCodeScannerIcon onClick={this.handleQRbutton}/></IconButton>
|
/>
|
||||||
<Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>{t("Submit")}</Button>
|
</Grid>
|
||||||
</Grid>
|
: null }
|
||||||
|
<Grid item xs={12} align="center">
|
||||||
|
<IconButton><QrCodeScannerIcon onClick={this.handleQRbutton}/></IconButton>
|
||||||
|
<Button onClick={this.handleClickSubmitInvoiceButton} variant='contained' color='primary'>{t("Submit")}</Button>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* ONCHAIN PAYOUT TAB */}
|
||||||
|
<div style={{display: this.state.receiveTab == 1 ? '':'none'}}>
|
||||||
|
<List dense={true}>
|
||||||
|
<ListItem>
|
||||||
|
<Typography variant="body2">
|
||||||
|
<b>{t("EXPERIMENTAL: ")}</b>{t("RoboSats will do a swap and send the Sats to your onchain address.")}
|
||||||
|
</Typography>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<Divider/>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary={pn(parseInt(this.props.data.invoice_amount * this.props.data.swap_fee_rate/100)) + " Sats (" + this.props.data.swap_fee_rate + "%)"}
|
||||||
|
secondary={t("Swap fee")}/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<Divider/>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary={pn(parseInt(Math.max(1 , this.state.miningFee) * 141)) + " Sats (" + Math.max(1, this.state.miningFee) + " Sats/vByte)"}
|
||||||
|
secondary={t("Mining fee")}/>
|
||||||
|
</ListItem>
|
||||||
|
|
||||||
|
<Divider/>
|
||||||
|
|
||||||
|
<ListItem>
|
||||||
|
<ListItemText
|
||||||
|
primary={<b>{pn(parseInt(this.props.data.invoice_amount - (Math.max(1, this.state.miningFee) * 141) - (this.props.data.invoice_amount * this.props.data.swap_fee_rate/100)))+ " Sats"}</b>}
|
||||||
|
secondary={t("Final amount you will receive")}/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
<TextField
|
||||||
|
error={this.state.badAddress}
|
||||||
|
helperText={this.state.badAddress ? t(this.state.badAddress) : "" }
|
||||||
|
label={t("Bitcoin Address")}
|
||||||
|
required
|
||||||
|
value={this.state.invoice}
|
||||||
|
sx={{width:170}}
|
||||||
|
inputProps={{
|
||||||
|
style: {textAlign:"center"},
|
||||||
|
}}
|
||||||
|
onChange={this.handleInputAddressChanged}
|
||||||
|
/>
|
||||||
|
<TextField
|
||||||
|
error={this.state.miningFee < 1 || this.state.miningFee > 50}
|
||||||
|
helperText={this.state.miningFee < 1 || this.state.miningFee > 50 ? "Invalid" : ''}
|
||||||
|
label={t("Mining Fee")}
|
||||||
|
required
|
||||||
|
sx={{width:110}}
|
||||||
|
value={this.state.miningFee}
|
||||||
|
type="number"
|
||||||
|
inputProps={{
|
||||||
|
max:50,
|
||||||
|
min:1,
|
||||||
|
style: {textAlign:"center"},
|
||||||
|
}}
|
||||||
|
onChange={this.handleMiningFeeChanged}
|
||||||
|
/>
|
||||||
|
<div style={{height:10}}/>
|
||||||
|
|
||||||
|
<Grid item xs={12} align="center">
|
||||||
|
<Button onClick={this.handleClickSubmitAddressButton} variant='contained' color='primary'>{t("Submit")}</Button>
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
<List>
|
||||||
|
<Divider/>
|
||||||
|
</List>
|
||||||
|
|
||||||
{this.showBondIsLocked()}
|
{this.showBondIsLocked()}
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -803,7 +941,7 @@ class TradeBox extends Component {
|
|||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography variant="subtitle1">
|
<Typography variant="subtitle1">
|
||||||
<b>{t("Your invoice looks good!")}</b> {" " + this.stepXofY()}
|
<b>{t("Your info looks good!")}</b> {" " + this.stepXofY()}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
@ -1095,19 +1233,42 @@ handleRatingRobosatsChange=(e)=>{
|
|||||||
{this.state.rating_platform==5 ?
|
{this.state.rating_platform==5 ?
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography variant="body2" align="center">
|
<Typography variant="body2" align="center">
|
||||||
<p><b>{t("Thank you! RoboSats loves you too ❤️")}</b></p>
|
<b>{t("Thank you! RoboSats loves you too ❤️")}</b>
|
||||||
<p>{t("RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!")}</p>
|
</Typography>
|
||||||
|
<Typography variant="body2" align="center">
|
||||||
|
{t("RoboSats gets better with more liquidity and users. Tell a bitcoiner friend about Robosats!")}
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
: null}
|
: null}
|
||||||
{this.state.rating_platform!=5 & this.state.rating_platform!=null ?
|
{this.state.rating_platform!=5 & this.state.rating_platform!=null ?
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Typography variant="body2" align="center">
|
<Typography variant="body2" align="center">
|
||||||
<p><b>{t("Thank you for using Robosats!")}</b></p>
|
<b>{t("Thank you for using Robosats!")}</b>
|
||||||
<p><Trans i18nKey="let_us_know_hot_to_improve">Let us know how the platform could improve (<Link target='_blank' href="https://t.me/robosats">Telegram</Link> / <Link target='_blank' href="https://github.com/Reckless-Satoshi/robosats/issues">Github</Link>)</Trans></p>
|
</Typography>
|
||||||
|
<Typography variant="body2" align="center">
|
||||||
|
<Trans i18nKey="let_us_know_hot_to_improve">Let us know how the platform could improve (<Link target='_blank' href="https://t.me/robosats">Telegram</Link> / <Link target='_blank' href="https://github.com/Reckless-Satoshi/robosats/issues">Github</Link>)</Trans>
|
||||||
</Typography>
|
</Typography>
|
||||||
</Grid>
|
</Grid>
|
||||||
: null}
|
: null}
|
||||||
|
|
||||||
|
{/* SHOW TXID IF USER RECEIVES ONCHAIN */}
|
||||||
|
{this.props.data.txid ?
|
||||||
|
<Grid item xs={12} align="left">
|
||||||
|
<Alert severity="success">
|
||||||
|
<AlertTitle>{t("Your TXID")}
|
||||||
|
<Tooltip disableHoverListener enterTouchDelay={0} title={t("Copied!")}>
|
||||||
|
<IconButton color="inherit" onClick={() => {navigator.clipboard.writeText(this.props.data.txid)}}>
|
||||||
|
<ContentCopy sx={{width:16,height:16}}/>
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</AlertTitle>
|
||||||
|
<Typography variant="body2" align="center" sx={{ wordWrap: "break-word", width:220}}>
|
||||||
|
<Link target='_blank' href={"http://mempoolhqx4isw62xs7abwphsq7ldayuidyx2v2oethdhhj6mlo2r6ad.onion/"+(this.props.data.network =="testnet"? "testnet/": "")+"tx/"+this.props.data.txid}>{this.props.data.txid}</Link>
|
||||||
|
</Typography>
|
||||||
|
</Alert>
|
||||||
|
</Grid>
|
||||||
|
: null}
|
||||||
|
|
||||||
<Grid item xs={12} align="center">
|
<Grid item xs={12} align="center">
|
||||||
<Button color='primary' onClick={() => {this.props.push('/')}}>{t("Start Again")}</Button>
|
<Button color='primary' onClick={() => {this.props.push('/')}}>{t("Start Again")}</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
@ -128,7 +128,7 @@ class UserGenPage extends Component {
|
|||||||
|
|
||||||
handleChangeToken=(e)=>{
|
handleChangeToken=(e)=>{
|
||||||
this.setState({
|
this.setState({
|
||||||
token: e.target.value,
|
token: e.target.value.split(' ').join(''),
|
||||||
tokenHasChanged: true,
|
tokenHasChanged: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -181,6 +181,7 @@
|
|||||||
"You do not have previous orders":"You do not have previous orders",
|
"You do not have previous orders":"You do not have previous orders",
|
||||||
"Join RoboSats' Subreddit":"Join RoboSats' Subreddit",
|
"Join RoboSats' Subreddit":"Join RoboSats' Subreddit",
|
||||||
"RoboSats in Reddit":"RoboSats in Reddit",
|
"RoboSats in Reddit":"RoboSats in Reddit",
|
||||||
|
"Current onchain payout fee":"Current onchain payout fee",
|
||||||
|
|
||||||
"ORDER PAGE - OrderPage.js": "Order details page",
|
"ORDER PAGE - OrderPage.js": "Order details page",
|
||||||
"Order Box":"Order Box",
|
"Order Box":"Order Box",
|
||||||
@ -313,10 +314,8 @@
|
|||||||
"Among public {{currencyCode}} orders (higher is cheaper)": "Among public {{currencyCode}} orders (higher is cheaper)",
|
"Among public {{currencyCode}} orders (higher is cheaper)": "Among public {{currencyCode}} orders (higher is cheaper)",
|
||||||
"A taker has been found!":"A taker has been found!",
|
"A taker has been found!":"A taker has been found!",
|
||||||
"Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.":"Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.",
|
"Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.":"Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.",
|
||||||
"Submit an invoice for {{amountSats}} Sats":"Submit an invoice for {{amountSats}} Sats",
|
|
||||||
"The taker is committed! Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC. Please provide a valid invoice for {{amountSats}} Satoshis.":"The taker is committed! Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC. Please provide a valid invoice for {{amountSats}} Satoshis.",
|
|
||||||
"Payout Lightning Invoice":"Payout Lightning Invoice",
|
"Payout Lightning Invoice":"Payout Lightning Invoice",
|
||||||
"Your invoice looks good!":"Your invoice looks good!",
|
"Your info looks good!":"Your info looks good!",
|
||||||
"We are waiting for the seller to lock the trade amount.":"We are waiting for the seller to lock the trade amount.",
|
"We are waiting for the seller to lock the trade amount.":"We are waiting for the seller to lock the trade amount.",
|
||||||
"Just hang on for a moment. If the seller does not deposit, you will get your bond back automatically. In addition, you will receive a compensation (check the rewards in your profile).":"Just hang on for a moment. If the seller does not deposit, you will get your bond back automatically. In addition, you will receive a compensation (check the rewards in your profile).",
|
"Just hang on for a moment. If the seller does not deposit, you will get your bond back automatically. In addition, you will receive a compensation (check the rewards in your profile).":"Just hang on for a moment. If the seller does not deposit, you will get your bond back automatically. In addition, you will receive a compensation (check the rewards in your profile).",
|
||||||
"The trade collateral is locked!":"The trade collateral is locked!",
|
"The trade collateral is locked!":"The trade collateral is locked!",
|
||||||
@ -390,6 +389,26 @@
|
|||||||
"Does not look like a valid lightning invoice":"Does not look like a valid lightning invoice",
|
"Does not look like a valid lightning invoice":"Does not look like a valid lightning invoice",
|
||||||
"The invoice provided has already expired":"The invoice provided has already expired",
|
"The invoice provided has already expired":"The invoice provided has already expired",
|
||||||
"Make sure to EXPORT the chat log. The staff might request your exported chat log JSON in order to solve discrepancies. It is your responsibility to store it.":"Make sure to EXPORT the chat log. The staff might request your exported chat log JSON in order to solve discrepancies. It is your responsibility to store it.",
|
"Make sure to EXPORT the chat log. The staff might request your exported chat log JSON in order to solve discrepancies. It is your responsibility to store it.":"Make sure to EXPORT the chat log. The staff might request your exported chat log JSON in order to solve discrepancies. It is your responsibility to store it.",
|
||||||
|
"Does not look like a valid address":"Does not look like a valid address",
|
||||||
|
"This is not a bitcoin mainnet address":"This is not a bitcoin mainnet address",
|
||||||
|
"This is not a bitcoin testnet address":"This is not a bitcoin testnet address",
|
||||||
|
"Submit payout info for {{amountSats}} Sats":"Submit payout info for {{amountSats}} Sats",
|
||||||
|
"Submit a valid invoice for {{amountSats}} Satoshis.":"Submit a valid invoice for {{amountSats}} Satoshis.",
|
||||||
|
"Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC.":"Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC.",
|
||||||
|
"RoboSats will do a swap and send the Sats to your onchain address.":"RoboSats will do a swap and send the Sats to your onchain address.",
|
||||||
|
"Swap fee":"Swap fee",
|
||||||
|
"Mining fee":"Mining fee",
|
||||||
|
"Mining Fee":"Mining Fee",
|
||||||
|
"Final amount you will receive":"Final amount you will receive",
|
||||||
|
"Bitcoin Address":"Bitcoin Address",
|
||||||
|
"Your TXID":"Your TXID",
|
||||||
|
"Lightning":"Lightning",
|
||||||
|
"Onchain":"Onchain",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"INFO DIALOG - InfoDiagog.js":"App information and clarifications and terms of use",
|
"INFO DIALOG - InfoDiagog.js":"App information and clarifications and terms of use",
|
||||||
|
@ -181,6 +181,9 @@
|
|||||||
"You do not have previous orders":"No tienes órdenes previas",
|
"You do not have previous orders":"No tienes órdenes previas",
|
||||||
"Join RoboSats' Subreddit":"Únete al subreddit de RoboSats",
|
"Join RoboSats' Subreddit":"Únete al subreddit de RoboSats",
|
||||||
"RoboSats in Reddit":"RoboSats en Reddit",
|
"RoboSats in Reddit":"RoboSats en Reddit",
|
||||||
|
"Current onchain payout fee":"Coste actual de recibir onchain",
|
||||||
|
"Lightning":"Lightning",
|
||||||
|
"Onchain":"Onchain",
|
||||||
|
|
||||||
"ORDER PAGE - OrderPage.js": "Order details page",
|
"ORDER PAGE - OrderPage.js": "Order details page",
|
||||||
"Order Box": "Orden",
|
"Order Box": "Orden",
|
||||||
@ -312,10 +315,8 @@
|
|||||||
"Among public {{currencyCode}} orders (higher is cheaper)": "Entre las órdenes públicas de {{currencyCode}} (más alto, más barato)",
|
"Among public {{currencyCode}} orders (higher is cheaper)": "Entre las órdenes públicas de {{currencyCode}} (más alto, más barato)",
|
||||||
"A taker has been found!": "¡Un tomador ha sido encontrado!",
|
"A taker has been found!": "¡Un tomador ha sido encontrado!",
|
||||||
"Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.": "Por favor, espera a que el tomador bloquee su fianza. Si no lo hace a tiempo, la orden será pública de nuevo.",
|
"Please wait for the taker to lock a bond. If the taker does not lock a bond in time, the order will be made public again.": "Por favor, espera a que el tomador bloquee su fianza. Si no lo hace a tiempo, la orden será pública de nuevo.",
|
||||||
"Submit an invoice for {{amountSats}} Sats": "Envía una factura por {{amountSats}} Sats",
|
|
||||||
"The taker is committed! Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC. Please provide a valid invoice for {{amountSats}} Satoshis.": "¡El tomador está comprometido! Antes de dejarte enviar {{amountFiat}} {{currencyCode}}, queremos asegurarnos de que puedes recibir en Lightning. Por favor proporciona una factura válida por {{amountSats}} Sats.",
|
|
||||||
"Payout Lightning Invoice": "Factura Lightning",
|
"Payout Lightning Invoice": "Factura Lightning",
|
||||||
"Your invoice looks good!": "¡Tu factura es buena!",
|
"Your info looks good!": "¡Info del envio recibida!",
|
||||||
"We are waiting for the seller lock the trade amount.": "Esperando a que el vendedor bloquee el colateral.",
|
"We are waiting for the seller lock the trade amount.": "Esperando a que el vendedor bloquee el colateral.",
|
||||||
"Just hang on for a moment. If the seller does not deposit, you will get your bond back automatically. In addition, you will receive a compensation (check the rewards in your profile).": "Espera un momento. Si el vendedor no deposita, recuperarás tu fianza automáticamente. Además, recibirás una compensación (comprueba las recompensas en tu perfil).",
|
"Just hang on for a moment. If the seller does not deposit, you will get your bond back automatically. In addition, you will receive a compensation (check the rewards in your profile).": "Espera un momento. Si el vendedor no deposita, recuperarás tu fianza automáticamente. Además, recibirás una compensación (comprueba las recompensas en tu perfil).",
|
||||||
"The trade collateral is locked!": "¡El colateral está bloqueado!",
|
"The trade collateral is locked!": "¡El colateral está bloqueado!",
|
||||||
@ -388,6 +389,20 @@
|
|||||||
"The invoice provided has no explicit amount":"La factura entregada no contiene una cantidad explícita",
|
"The invoice provided has no explicit amount":"La factura entregada no contiene una cantidad explícita",
|
||||||
"Does not look like a valid lightning invoice":"No parece ser una factura lightning válida",
|
"Does not look like a valid lightning invoice":"No parece ser una factura lightning válida",
|
||||||
"The invoice provided has already expired":"La factura que has entregado ya ha caducado",
|
"The invoice provided has already expired":"La factura que has entregado ya ha caducado",
|
||||||
|
"Make sure to EXPORT the chat log. The staff might request your exported chat log JSON in order to solve discrepancies. It is your responsibility to store it.":"Asegurate de EXPORTAR el registro del chat. Los administradores pueden pedirte el registro del chat en caso de discrepancias. Es tu responsabilidad proveerlo.",
|
||||||
|
"Does not look like a valid address":"No parece una dirección Bitcoin válida",
|
||||||
|
"This is not a bitcoin mainnet address":"No es una dirección de mainnet",
|
||||||
|
"This is not a bitcoin testnet address":"No es una dirección de testnet",
|
||||||
|
"Submit payout info for {{amountSats}} Sats":"Envia info para recibir {{amountSats}} Sats",
|
||||||
|
"Submit a valid invoice for {{amountSats}} Satoshis.": "Envía una factura por {{amountSats}} Sats",
|
||||||
|
"Before letting you send {{amountFiat}} {{currencyCode}}, we want to make sure you are able to receive the BTC.":"Antes de dejarte enviar {{amountFiat}} {{currencyCode}}, queremos asegurarnos de que puedes recibir el Bitcoin.",
|
||||||
|
"RoboSats will do a swap and send the Sats to your onchain address.":"RoboSats hará un swap y enviará los Sats a tu dirección en la cadena.",
|
||||||
|
"Swap fee":"Comisión del swap",
|
||||||
|
"Mining fee":"Comisión minera",
|
||||||
|
"Mining Fee":"Comisión Minera",
|
||||||
|
"Final amount you will receive":"Monto final que vas a recibir",
|
||||||
|
"Bitcoin Address":"Dirección Bitcoin",
|
||||||
|
"Your TXID":"Tu TXID",
|
||||||
|
|
||||||
|
|
||||||
"INFO DIALOG - InfoDiagog.js": "App information and clarifications and terms of use",
|
"INFO DIALOG - InfoDiagog.js": "App information and clarifications and terms of use",
|
||||||
|
@ -27,3 +27,4 @@ django-import-export==2.7.1
|
|||||||
requests[socks]
|
requests[socks]
|
||||||
python-gnupg==0.4.9
|
python-gnupg==0.4.9
|
||||||
daphne==3.0.2
|
daphne==3.0.2
|
||||||
|
coinaddrvalidator==1.1.3
|
||||||
|
@ -55,6 +55,10 @@ app.conf.beat_schedule = {
|
|||||||
"task": "cache_external_market_prices",
|
"task": "cache_external_market_prices",
|
||||||
"schedule": timedelta(seconds=60),
|
"schedule": timedelta(seconds=60),
|
||||||
},
|
},
|
||||||
|
"compute-node-balance": { # Logs LND channel and wallet balance
|
||||||
|
"task":"compute_node_balance",
|
||||||
|
"schedule": timedelta(minutes=60),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
app.conf.timezone = "UTC"
|
app.conf.timezone = "UTC"
|
||||||
|
Loading…
Reference in New Issue
Block a user