mirror of
https://github.com/RoboSats/robosats.git
synced 2024-12-13 02:46:28 +00:00
Fix code style issues with Black
This commit is contained in:
parent
c32c07eaa6
commit
3d3da78f8a
172
api/admin.py
172
api/admin.py
@ -5,6 +5,7 @@ from django.contrib.auth.admin import UserAdmin
|
||||
from api.models import OnchainPayment, Order, LNPayment, Profile, MarketTick, Currency
|
||||
from api.logics import Logics
|
||||
from statistics import median
|
||||
|
||||
admin.site.unregister(Group)
|
||||
admin.site.unregister(User)
|
||||
|
||||
@ -12,10 +13,11 @@ admin.site.unregister(User)
|
||||
class ProfileInline(admin.StackedInline):
|
||||
model = Profile
|
||||
can_delete = False
|
||||
fields = ("avatar_tag", )
|
||||
fields = ("avatar_tag",)
|
||||
readonly_fields = ["avatar_tag"]
|
||||
show_change_link = True
|
||||
|
||||
|
||||
# extended users with avatars
|
||||
@admin.register(User)
|
||||
class EUserAdmin(AdminChangeLinksMixin, UserAdmin):
|
||||
@ -30,14 +32,13 @@ class EUserAdmin(AdminChangeLinksMixin, UserAdmin):
|
||||
"is_staff",
|
||||
)
|
||||
list_display_links = ("id", "username")
|
||||
change_links = (
|
||||
"profile",
|
||||
)
|
||||
ordering = ("-id", )
|
||||
change_links = ("profile",)
|
||||
ordering = ("-id",)
|
||||
|
||||
def avatar_tag(self, obj):
|
||||
return obj.profile.avatar_tag()
|
||||
|
||||
|
||||
@admin.register(Order)
|
||||
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = (
|
||||
@ -79,19 +80,34 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"taker_bond",
|
||||
"trade_escrow",
|
||||
)
|
||||
list_filter = ("is_disputed", "is_fiat_sent", "is_swap","type", "currency", "status")
|
||||
search_fields = ["id","amount","min_amount","max_amount"]
|
||||
list_filter = (
|
||||
"is_disputed",
|
||||
"is_fiat_sent",
|
||||
"is_swap",
|
||||
"type",
|
||||
"currency",
|
||||
"status",
|
||||
)
|
||||
search_fields = ["id", "amount", "min_amount", "max_amount"]
|
||||
|
||||
actions = ['maker_wins', 'taker_wins', 'return_everything','compite_median_trade_time']
|
||||
actions = [
|
||||
"maker_wins",
|
||||
"taker_wins",
|
||||
"return_everything",
|
||||
"compite_median_trade_time",
|
||||
]
|
||||
|
||||
@admin.action(description='Solve dispute: maker wins')
|
||||
@admin.action(description="Solve dispute: maker wins")
|
||||
def maker_wins(self, request, queryset):
|
||||
'''
|
||||
"""
|
||||
Solves a dispute on favor of the maker.
|
||||
Adds Sats to compensations (earned_rewards) of the maker profile.
|
||||
'''
|
||||
"""
|
||||
for order in queryset:
|
||||
if order.status in [Order.Status.DIS, Order.Status.WFR] and order.is_disputed:
|
||||
if (
|
||||
order.status in [Order.Status.DIS, Order.Status.WFR]
|
||||
and order.is_disputed
|
||||
):
|
||||
own_bond_sats = order.maker_bond.num_satoshis
|
||||
if Logics.is_buyer(order, order.maker):
|
||||
if order.is_swap:
|
||||
@ -105,19 +121,30 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
order.maker.profile.earned_rewards = own_bond_sats + trade_sats
|
||||
order.maker.profile.save()
|
||||
order.save()
|
||||
self.message_user(request,f"Dispute of order {order.id} solved successfully on favor of the maker", messages.SUCCESS)
|
||||
self.message_user(
|
||||
request,
|
||||
f"Dispute of order {order.id} solved successfully on favor of the maker",
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
else:
|
||||
self.message_user(request,f"Order {order.id} is not in a disputed state", messages.ERROR)
|
||||
self.message_user(
|
||||
request,
|
||||
f"Order {order.id} is not in a disputed state",
|
||||
messages.ERROR,
|
||||
)
|
||||
|
||||
@admin.action(description='Solve dispute: taker wins')
|
||||
@admin.action(description="Solve dispute: taker wins")
|
||||
def taker_wins(self, request, queryset):
|
||||
'''
|
||||
"""
|
||||
Solves a dispute on favor of the taker.
|
||||
Adds Sats to compensations (earned_rewards) of the taker profile.
|
||||
'''
|
||||
"""
|
||||
for order in queryset:
|
||||
if order.status in [Order.Status.DIS, Order.Status.WFR] and order.is_disputed:
|
||||
if (
|
||||
order.status in [Order.Status.DIS, Order.Status.WFR]
|
||||
and order.is_disputed
|
||||
):
|
||||
own_bond_sats = order.maker_bond.num_satoshis
|
||||
if Logics.is_buyer(order, order.taker):
|
||||
if order.is_swap:
|
||||
@ -131,56 +158,90 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
order.taker.profile.earned_rewards = own_bond_sats + trade_sats
|
||||
order.taker.profile.save()
|
||||
order.save()
|
||||
self.message_user(request,f"Dispute of order {order.id} solved successfully on favor of the taker", messages.SUCCESS)
|
||||
self.message_user(
|
||||
request,
|
||||
f"Dispute of order {order.id} solved successfully on favor of the taker",
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
else:
|
||||
self.message_user(request,f"Order {order.id} is not in a disputed state", messages.ERROR)
|
||||
self.message_user(
|
||||
request,
|
||||
f"Order {order.id} is not in a disputed state",
|
||||
messages.ERROR,
|
||||
)
|
||||
|
||||
@admin.action(description='Solve dispute: return everything')
|
||||
@admin.action(description="Solve dispute: return everything")
|
||||
def return_everything(self, request, queryset):
|
||||
'''
|
||||
"""
|
||||
Solves a dispute by pushing back every bond and escrow to their sender.
|
||||
'''
|
||||
"""
|
||||
for order in queryset:
|
||||
if order.status in [Order.Status.DIS, Order.Status.WFR] and order.is_disputed:
|
||||
order.maker_bond.sender.profile.earned_rewards += order.maker_bond.num_satoshis
|
||||
if (
|
||||
order.status in [Order.Status.DIS, Order.Status.WFR]
|
||||
and order.is_disputed
|
||||
):
|
||||
order.maker_bond.sender.profile.earned_rewards += (
|
||||
order.maker_bond.num_satoshis
|
||||
)
|
||||
order.maker_bond.sender.profile.save()
|
||||
order.taker_bond.sender.profile.earned_rewards += order.taker_bond.num_satoshis
|
||||
order.taker_bond.sender.profile.earned_rewards += (
|
||||
order.taker_bond.num_satoshis
|
||||
)
|
||||
order.taker_bond.sender.profile.save()
|
||||
order.trade_escrow.sender.profile.earned_rewards += order.trade_escrow.num_satoshis
|
||||
order.trade_escrow.sender.profile.earned_rewards += (
|
||||
order.trade_escrow.num_satoshis
|
||||
)
|
||||
order.trade_escrow.sender.profile.save()
|
||||
order.status = Order.Status.CCA
|
||||
order.save()
|
||||
self.message_user(request,f"Dispute of order {order.id} solved successfully, everything returned as compensations", messages.SUCCESS)
|
||||
self.message_user(
|
||||
request,
|
||||
f"Dispute of order {order.id} solved successfully, everything returned as compensations",
|
||||
messages.SUCCESS,
|
||||
)
|
||||
|
||||
else:
|
||||
self.message_user(request,f"Order {order.id} is not in a disputed state", messages.ERROR)
|
||||
self.message_user(
|
||||
request,
|
||||
f"Order {order.id} is not in a disputed state",
|
||||
messages.ERROR,
|
||||
)
|
||||
|
||||
@admin.action(description='Compute median trade completion time')
|
||||
@admin.action(description="Compute median trade completion time")
|
||||
def compite_median_trade_time(self, request, queryset):
|
||||
'''
|
||||
Computes the median time from an order taken to finishing
|
||||
"""
|
||||
Computes the median time from an order taken to finishing
|
||||
successfully for the set of selected orders.
|
||||
'''
|
||||
"""
|
||||
times = []
|
||||
for order in queryset:
|
||||
if order.contract_finalization_time:
|
||||
timedelta = order.contract_finalization_time - order.last_satoshis_time
|
||||
times.append(timedelta.total_seconds())
|
||||
|
||||
|
||||
if len(times) > 0:
|
||||
median_time_secs = median(times)
|
||||
mins = int(median_time_secs/60)
|
||||
secs = int(median_time_secs - mins*60)
|
||||
self.message_user(request, f"The median time to complete the trades is {mins}m {secs}s", messages.SUCCESS)
|
||||
mins = int(median_time_secs / 60)
|
||||
secs = int(median_time_secs - mins * 60)
|
||||
self.message_user(
|
||||
request,
|
||||
f"The median time to complete the trades is {mins}m {secs}s",
|
||||
messages.SUCCESS,
|
||||
)
|
||||
else:
|
||||
self.message_user(request, "There is no successfully finished orders in the selection", messages.ERROR)
|
||||
|
||||
self.message_user(
|
||||
request,
|
||||
"There is no successfully finished orders in the selection",
|
||||
messages.ERROR,
|
||||
)
|
||||
|
||||
def amt(self, obj):
|
||||
if obj.has_range and obj.amount == None:
|
||||
return str(float(obj.min_amount))+"-"+ str(float(obj.max_amount))
|
||||
return str(float(obj.min_amount)) + "-" + str(float(obj.max_amount))
|
||||
else:
|
||||
return float(obj.amount)
|
||||
return float(obj.amount)
|
||||
|
||||
|
||||
@admin.register(LNPayment)
|
||||
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
@ -210,8 +271,15 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"order_paid_LN",
|
||||
)
|
||||
list_filter = ("type", "concept", "status")
|
||||
ordering = ("-expires_at", )
|
||||
search_fields = ["payment_hash","num_satoshis","sender__username","receiver__username","description"]
|
||||
ordering = ("-expires_at",)
|
||||
search_fields = [
|
||||
"payment_hash",
|
||||
"num_satoshis",
|
||||
"sender__username",
|
||||
"receiver__username",
|
||||
"description",
|
||||
]
|
||||
|
||||
|
||||
@admin.register(OnchainPayment)
|
||||
class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
@ -231,9 +299,10 @@ class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"balance",
|
||||
"order_paid_TX",
|
||||
)
|
||||
list_display_links = ("id","address", "concept")
|
||||
list_display_links = ("id", "address", "concept")
|
||||
list_filter = ("concept", "status")
|
||||
search_fields = ["address","num_satoshis","receiver__username","txid"]
|
||||
search_fields = ["address", "num_satoshis", "receiver__username", "txid"]
|
||||
|
||||
|
||||
@admin.register(Profile)
|
||||
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
@ -261,7 +330,7 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display_links = ("avatar_tag", "id")
|
||||
change_links = ["user"]
|
||||
readonly_fields = ["avatar_tag"]
|
||||
search_fields = ["user__username","id"]
|
||||
search_fields = ["user__username", "id"]
|
||||
readonly_fields = ("public_key", "encrypted_private_key")
|
||||
|
||||
|
||||
@ -270,13 +339,12 @@ class CurrencieAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", "currency", "exchange_rate", "timestamp")
|
||||
list_display_links = ("id", "currency")
|
||||
readonly_fields = ("currency", "exchange_rate", "timestamp")
|
||||
ordering = ("id", )
|
||||
ordering = ("id",)
|
||||
|
||||
|
||||
@admin.register(MarketTick)
|
||||
class MarketTickAdmin(admin.ModelAdmin):
|
||||
list_display = ("timestamp", "price", "volume", "premium", "currency",
|
||||
"fee")
|
||||
readonly_fields = ("timestamp", "price", "volume", "premium", "currency",
|
||||
"fee")
|
||||
list_display = ("timestamp", "price", "volume", "premium", "currency", "fee")
|
||||
readonly_fields = ("timestamp", "price", "volume", "premium", "currency", "fee")
|
||||
list_filter = ["currency"]
|
||||
ordering = ("-timestamp", )
|
||||
ordering = ("-timestamp",)
|
||||
|
@ -24,8 +24,9 @@ except:
|
||||
|
||||
# Read macaroon from file or .env variable string encoded as base64
|
||||
try:
|
||||
MACAROON = open(os.path.join(config("LND_DIR"), config("MACAROON_path")),
|
||||
"rb").read()
|
||||
MACAROON = open(
|
||||
os.path.join(config("LND_DIR"), config("MACAROON_path")), "rb"
|
||||
).read()
|
||||
except:
|
||||
MACAROON = b64decode(config("LND_MACAROON_BASE64"))
|
||||
|
||||
@ -49,13 +50,10 @@ class LNNode:
|
||||
|
||||
payment_failure_context = {
|
||||
0: "Payment isn't failed (yet)",
|
||||
1:
|
||||
"There are more routes to try, but the payment timeout was exceeded.",
|
||||
2:
|
||||
"All possible routes were tried and failed permanently. Or were no routes to the destination at all.",
|
||||
1: "There are more routes to try, but the payment timeout was exceeded.",
|
||||
2: "All possible routes were tried and failed permanently. Or were no routes to the destination at all.",
|
||||
3: "A non-recoverable error has occured.",
|
||||
4:
|
||||
"Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
|
||||
4: "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
|
||||
5: "Insufficient local balance.",
|
||||
}
|
||||
|
||||
@ -63,9 +61,9 @@ class LNNode:
|
||||
def decode_payreq(cls, invoice):
|
||||
"""Decodes a lightning payment request (invoice)"""
|
||||
request = lnrpc.PayReqString(pay_req=invoice)
|
||||
response = cls.lightningstub.DecodePayReq(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())])
|
||||
response = cls.lightningstub.DecodePayReq(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
)
|
||||
return response
|
||||
|
||||
@classmethod
|
||||
@ -73,46 +71,56 @@ class LNNode:
|
||||
"""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)
|
||||
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())])
|
||||
response = cls.lightningstub.EstimateFee(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
)
|
||||
|
||||
return {'mining_fee_sats': response.fee_sat, 'mining_fee_rate': response.sat_per_vbyte}
|
||||
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())])
|
||||
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}
|
||||
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())])
|
||||
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}
|
||||
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):
|
||||
@ -121,15 +129,17 @@ class LNNode:
|
||||
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)
|
||||
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())])
|
||||
response = cls.lightningstub.SendCoins(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
)
|
||||
|
||||
onchainpayment.txid = response.txid
|
||||
onchainpayment.save()
|
||||
@ -139,28 +149,27 @@ class LNNode:
|
||||
@classmethod
|
||||
def cancel_return_hold_invoice(cls, payment_hash):
|
||||
"""Cancels or returns a hold invoice"""
|
||||
request = invoicesrpc.CancelInvoiceMsg(
|
||||
payment_hash=bytes.fromhex(payment_hash))
|
||||
response = cls.invoicesstub.CancelInvoice(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())])
|
||||
request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
|
||||
response = cls.invoicesstub.CancelInvoice(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
)
|
||||
# Fix this: tricky because canceling sucessfully an invoice has no response. TODO
|
||||
return str(response) == "" # True if no response, false otherwise.
|
||||
|
||||
@classmethod
|
||||
def settle_hold_invoice(cls, preimage):
|
||||
"""settles a hold invoice"""
|
||||
request = invoicesrpc.SettleInvoiceMsg(
|
||||
preimage=bytes.fromhex(preimage))
|
||||
response = cls.invoicesstub.SettleInvoice(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())])
|
||||
request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage))
|
||||
response = cls.invoicesstub.SettleInvoice(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
)
|
||||
# Fix this: tricky because settling sucessfully an invoice has None response. TODO
|
||||
return str(response) == "" # True if no response, false otherwise.
|
||||
|
||||
@classmethod
|
||||
def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry,
|
||||
cltv_expiry_blocks):
|
||||
def gen_hold_invoice(
|
||||
cls, num_satoshis, description, invoice_expiry, cltv_expiry_blocks
|
||||
):
|
||||
"""Generates hold invoice"""
|
||||
|
||||
hold_payment = {}
|
||||
@ -179,18 +188,20 @@ class LNNode:
|
||||
), # actual expiry is padded by 50%, if tight, wrong client system clock will say invoice is expired.
|
||||
cltv_expiry=cltv_expiry_blocks,
|
||||
)
|
||||
response = cls.invoicesstub.AddHoldInvoice(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())])
|
||||
response = cls.invoicesstub.AddHoldInvoice(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
)
|
||||
|
||||
hold_payment["invoice"] = response.payment_request
|
||||
payreq_decoded = cls.decode_payreq(hold_payment["invoice"])
|
||||
hold_payment["preimage"] = preimage.hex()
|
||||
hold_payment["payment_hash"] = payreq_decoded.payment_hash
|
||||
hold_payment["created_at"] = timezone.make_aware(
|
||||
datetime.fromtimestamp(payreq_decoded.timestamp))
|
||||
datetime.fromtimestamp(payreq_decoded.timestamp)
|
||||
)
|
||||
hold_payment["expires_at"] = hold_payment["created_at"] + timedelta(
|
||||
seconds=payreq_decoded.expiry)
|
||||
seconds=payreq_decoded.expiry
|
||||
)
|
||||
hold_payment["cltv_expiry"] = cltv_expiry_blocks
|
||||
|
||||
return hold_payment
|
||||
@ -201,11 +212,11 @@ class LNNode:
|
||||
from api.models import LNPayment
|
||||
|
||||
request = invoicesrpc.LookupInvoiceMsg(
|
||||
payment_hash=bytes.fromhex(lnpayment.payment_hash))
|
||||
response = cls.invoicesstub.LookupInvoiceV2(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())
|
||||
])
|
||||
payment_hash=bytes.fromhex(lnpayment.payment_hash)
|
||||
)
|
||||
response = cls.invoicesstub.LookupInvoiceV2(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
)
|
||||
|
||||
# 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
|
||||
@ -225,11 +236,9 @@ class LNNode:
|
||||
@classmethod
|
||||
def resetmc(cls):
|
||||
request = routerrpc.ResetMissionControlRequest()
|
||||
response = cls.routerstub.ResetMissionControl(request,
|
||||
metadata=[
|
||||
("macaroon",
|
||||
MACAROON.hex())
|
||||
])
|
||||
response = cls.routerstub.ResetMissionControl(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
)
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
@ -258,7 +267,10 @@ class LNNode:
|
||||
route_hints = payreq_decoded.route_hints
|
||||
|
||||
# Max amount RoboSats will pay for routing
|
||||
max_routing_fee_sats = max(num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")))
|
||||
max_routing_fee_sats = max(
|
||||
num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
|
||||
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
|
||||
)
|
||||
|
||||
if route_hints:
|
||||
routes_cost = []
|
||||
@ -268,15 +280,17 @@ class LNNode:
|
||||
# ...add up the cost of every hinted hop...
|
||||
for hop_hint in hinted_route.hop_hints:
|
||||
route_cost += hop_hint.fee_base_msat / 1000
|
||||
route_cost += hop_hint.fee_proportional_millionths * num_satoshis / 1000000
|
||||
route_cost += (
|
||||
hop_hint.fee_proportional_millionths * num_satoshis / 1000000
|
||||
)
|
||||
|
||||
# ...and store the cost of the route to the array
|
||||
routes_cost.append(route_cost)
|
||||
|
||||
# If the cheapest possible private route is more expensive than what RoboSats is willing to pay
|
||||
if min(routes_cost) >= max_routing_fee_sats :
|
||||
if min(routes_cost) >= max_routing_fee_sats:
|
||||
payout["context"] = {
|
||||
"bad_invoice": "The invoice submitted only has a trick on the routing hints, you might be using an incompatible wallet (probably Muun? Use an onchain address instead!). Check the wallet compatibility guide at wallets.robosats.com"
|
||||
"bad_invoice": "The invoice submitted only has a trick on the routing hints, you might be using an incompatible wallet (probably Muun? Use an onchain address instead!). Check the wallet compatibility guide at wallets.robosats.com"
|
||||
}
|
||||
return payout
|
||||
|
||||
@ -288,16 +302,18 @@ class LNNode:
|
||||
|
||||
if not payreq_decoded.num_satoshis == num_satoshis:
|
||||
payout["context"] = {
|
||||
"bad_invoice":
|
||||
"The invoice provided is not for " +
|
||||
"{:,}".format(num_satoshis) + " Sats"
|
||||
"bad_invoice": "The invoice provided is not for "
|
||||
+ "{:,}".format(num_satoshis)
|
||||
+ " Sats"
|
||||
}
|
||||
return payout
|
||||
|
||||
payout["created_at"] = timezone.make_aware(
|
||||
datetime.fromtimestamp(payreq_decoded.timestamp))
|
||||
datetime.fromtimestamp(payreq_decoded.timestamp)
|
||||
)
|
||||
payout["expires_at"] = payout["created_at"] + timedelta(
|
||||
seconds=payreq_decoded.expiry)
|
||||
seconds=payreq_decoded.expiry
|
||||
)
|
||||
|
||||
if payout["expires_at"] < timezone.now():
|
||||
payout["context"] = {
|
||||
@ -315,21 +331,24 @@ class LNNode:
|
||||
def pay_invoice(cls, lnpayment):
|
||||
"""Sends sats. Used for rewards payouts"""
|
||||
from api.models import LNPayment
|
||||
|
||||
|
||||
fee_limit_sat = int(
|
||||
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")),
|
||||
)) # 200 ppm or 10 sats
|
||||
)
|
||||
) # 200 ppm or 10 sats
|
||||
timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS"))
|
||||
request = routerrpc.SendPaymentRequest(payment_request=lnpayment.invoice,
|
||||
fee_limit_sat=fee_limit_sat,
|
||||
timeout_seconds=timeout_seconds)
|
||||
request = routerrpc.SendPaymentRequest(
|
||||
payment_request=lnpayment.invoice,
|
||||
fee_limit_sat=fee_limit_sat,
|
||||
timeout_seconds=timeout_seconds,
|
||||
)
|
||||
|
||||
for response in cls.routerstub.SendPaymentV2(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())
|
||||
]):
|
||||
for response in cls.routerstub.SendPaymentV2(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
):
|
||||
|
||||
if response.status == 0: # Status 0 'UNKNOWN'
|
||||
# Not sure when this status happens
|
||||
@ -354,7 +373,7 @@ class LNNode:
|
||||
|
||||
if response.status == 2: # STATUS 'SUCCEEDED'
|
||||
lnpayment.status = LNPayment.Status.SUCCED
|
||||
lnpayment.fee = float(response.fee_msat)/1000
|
||||
lnpayment.fee = float(response.fee_msat) / 1000
|
||||
lnpayment.preimage = response.payment_preimage
|
||||
lnpayment.save()
|
||||
return True, None
|
||||
@ -364,12 +383,10 @@ class LNNode:
|
||||
@classmethod
|
||||
def double_check_htlc_is_settled(cls, payment_hash):
|
||||
"""Just as it sounds. Better safe than sorry!"""
|
||||
request = invoicesrpc.LookupInvoiceMsg(
|
||||
payment_hash=bytes.fromhex(payment_hash))
|
||||
response = cls.invoicesstub.LookupInvoiceV2(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())
|
||||
])
|
||||
request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
|
||||
response = cls.invoicesstub.LookupInvoiceV2(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
)
|
||||
|
||||
return (
|
||||
response.state == 1
|
||||
|
671
api/logics.py
671
api/logics.py
File diff suppressed because it is too large
Load Diff
@ -36,7 +36,8 @@ class Command(BaseCommand):
|
||||
|
||||
queryset = Order.objects.exclude(status__in=do_nothing)
|
||||
queryset = queryset.filter(
|
||||
expires_at__lt=timezone.now()) # expires at lower than now
|
||||
expires_at__lt=timezone.now()
|
||||
) # expires at lower than now
|
||||
|
||||
debug = {}
|
||||
debug["num_expired_orders"] = len(queryset)
|
||||
@ -45,11 +46,9 @@ class Command(BaseCommand):
|
||||
debug["reason_failure"] = []
|
||||
|
||||
for idx, order in enumerate(queryset):
|
||||
context = str(order) + " was " + Order.Status(
|
||||
order.status).label
|
||||
context = str(order) + " was " + Order.Status(order.status).label
|
||||
try:
|
||||
if Logics.order_expires(
|
||||
order): # Order send to expire here
|
||||
if Logics.order_expires(order): # Order send to expire here
|
||||
debug["expired_orders"].append({idx: context})
|
||||
|
||||
# It should not happen, but if it cannot locate the hold invoice
|
||||
@ -57,7 +56,7 @@ class Command(BaseCommand):
|
||||
except Exception as e:
|
||||
debug["failed_order_expiry"].append({idx: context})
|
||||
debug["reason_failure"].append({idx: str(e)})
|
||||
|
||||
|
||||
if "unable to locate invoice" in str(e):
|
||||
self.stdout.write(str(e))
|
||||
order.status = Order.Status.EXP
|
||||
|
@ -73,18 +73,17 @@ class Command(BaseCommand):
|
||||
try:
|
||||
# this is similar to LNNnode.validate_hold_invoice_locked
|
||||
request = LNNode.invoicesrpc.LookupInvoiceMsg(
|
||||
payment_hash=bytes.fromhex(hold_lnpayment.payment_hash))
|
||||
response = stub.LookupInvoiceV2(request,
|
||||
metadata=[("macaroon",
|
||||
MACAROON.hex())])
|
||||
hold_lnpayment.status = lnd_state_to_lnpayment_status[
|
||||
response.state]
|
||||
payment_hash=bytes.fromhex(hold_lnpayment.payment_hash)
|
||||
)
|
||||
response = stub.LookupInvoiceV2(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
)
|
||||
hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
|
||||
|
||||
# try saving expiry height
|
||||
if hasattr(response, "htlcs"):
|
||||
try:
|
||||
hold_lnpayment.expiry_height = response.htlcs[
|
||||
0].expiry_height
|
||||
hold_lnpayment.expiry_height = response.htlcs[0].expiry_height
|
||||
except:
|
||||
pass
|
||||
|
||||
@ -97,8 +96,7 @@ class Command(BaseCommand):
|
||||
|
||||
# LND restarted.
|
||||
if "wallet locked, unlock it" in str(e):
|
||||
self.stdout.write(
|
||||
str(timezone.now()) + " :: Wallet Locked")
|
||||
self.stdout.write(str(timezone.now()) + " :: Wallet Locked")
|
||||
# Other write to logs
|
||||
else:
|
||||
self.stdout.write(str(e))
|
||||
@ -114,13 +112,15 @@ class Command(BaseCommand):
|
||||
|
||||
# Report for debugging
|
||||
new_status = LNPayment.Status(hold_lnpayment.status).label
|
||||
debug["invoices"].append({
|
||||
idx: {
|
||||
"payment_hash": str(hold_lnpayment.payment_hash),
|
||||
"old_status": old_status,
|
||||
"new_status": new_status,
|
||||
debug["invoices"].append(
|
||||
{
|
||||
idx: {
|
||||
"payment_hash": str(hold_lnpayment.payment_hash),
|
||||
"old_status": old_status,
|
||||
"new_status": new_status,
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
at_least_one_changed = at_least_one_changed or changed
|
||||
|
||||
@ -148,7 +148,8 @@ class Command(BaseCommand):
|
||||
status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
|
||||
in_flight=False,
|
||||
last_routing_time__lt=(
|
||||
timezone.now() - timedelta(minutes=int(config("RETRY_TIME")))),
|
||||
timezone.now() - timedelta(minutes=int(config("RETRY_TIME")))
|
||||
),
|
||||
)
|
||||
|
||||
queryset = queryset.union(queryset_retries)
|
||||
@ -167,7 +168,7 @@ class Command(BaseCommand):
|
||||
# It is a maker bond => Publish order.
|
||||
if hasattr(lnpayment, "order_made"):
|
||||
Logics.publish_order(lnpayment.order_made)
|
||||
send_message.delay(lnpayment.order_made.id,'order_published')
|
||||
send_message.delay(lnpayment.order_made.id, "order_published")
|
||||
return
|
||||
|
||||
# It is a taker bond => close contract.
|
||||
|
@ -7,50 +7,54 @@ from decouple import config
|
||||
import requests
|
||||
import time
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
|
||||
help = "Polls telegram /getUpdates method"
|
||||
rest = 3 # seconds between consecutive polls
|
||||
rest = 3 # seconds between consecutive polls
|
||||
|
||||
bot_token = config('TELEGRAM_TOKEN')
|
||||
updates_url = f'https://api.telegram.org/bot{bot_token}/getUpdates'
|
||||
bot_token = config("TELEGRAM_TOKEN")
|
||||
updates_url = f"https://api.telegram.org/bot{bot_token}/getUpdates"
|
||||
|
||||
session = get_session()
|
||||
telegram = Telegram()
|
||||
|
||||
def handle(self, *args, **options):
|
||||
"""Infinite loop to check for telegram updates.
|
||||
If it finds a new user (/start), enables it's taker found
|
||||
notification and sends a 'Hey {username} {order_id}' message back"""
|
||||
|
||||
|
||||
offset = 0
|
||||
while True:
|
||||
time.sleep(self.rest)
|
||||
|
||||
params = {'offset' : offset + 1 , 'timeout' : 5}
|
||||
params = {"offset": offset + 1, "timeout": 5}
|
||||
response = self.session.get(self.updates_url, params=params).json()
|
||||
if len(list(response['result'])) == 0:
|
||||
if len(list(response["result"])) == 0:
|
||||
continue
|
||||
for result in response['result']:
|
||||
for result in response["result"]:
|
||||
|
||||
try: # if there is no key message, skips this result.
|
||||
text = result['message']['text']
|
||||
try: # if there is no key message, skips this result.
|
||||
text = result["message"]["text"]
|
||||
except:
|
||||
continue
|
||||
|
||||
splitted_text = text.split(' ')
|
||||
if splitted_text[0] == '/start':
|
||||
|
||||
splitted_text = text.split(" ")
|
||||
if splitted_text[0] == "/start":
|
||||
token = splitted_text[-1]
|
||||
try :
|
||||
try:
|
||||
profile = Profile.objects.get(telegram_token=token)
|
||||
except:
|
||||
print(f'No profile with token {token}')
|
||||
print(f"No profile with token {token}")
|
||||
continue
|
||||
|
||||
|
||||
attempts = 5
|
||||
while attempts >= 0:
|
||||
try:
|
||||
profile.telegram_chat_id = result['message']['from']['id']
|
||||
profile.telegram_lang_code = result['message']['from']['language_code']
|
||||
profile.telegram_chat_id = result["message"]["from"]["id"]
|
||||
profile.telegram_lang_code = result["message"]["from"][
|
||||
"language_code"
|
||||
]
|
||||
self.telegram.welcome(profile.user)
|
||||
profile.telegram_enabled = True
|
||||
profile.save()
|
||||
@ -59,6 +63,4 @@ class Command(BaseCommand):
|
||||
time.sleep(5)
|
||||
attempts = attempts - 1
|
||||
|
||||
offset = response['result'][-1]['update_id']
|
||||
|
||||
|
||||
offset = response["result"][-1]["update_id"]
|
||||
|
@ -3,37 +3,38 @@ from secrets import token_urlsafe
|
||||
from api.models import Order
|
||||
from api.utils import get_session
|
||||
|
||||
class Telegram():
|
||||
''' Simple telegram messages by requesting to API'''
|
||||
|
||||
class Telegram:
|
||||
"""Simple telegram messages by requesting to API"""
|
||||
|
||||
session = get_session()
|
||||
site = config('HOST_NAME')
|
||||
site = config("HOST_NAME")
|
||||
|
||||
def get_context(user):
|
||||
"""returns context needed to enable TG notifications"""
|
||||
context = {}
|
||||
if user.profile.telegram_enabled :
|
||||
context['tg_enabled'] = True
|
||||
if user.profile.telegram_enabled:
|
||||
context["tg_enabled"] = True
|
||||
else:
|
||||
context['tg_enabled'] = False
|
||||
|
||||
context["tg_enabled"] = False
|
||||
|
||||
if user.profile.telegram_token == None:
|
||||
user.profile.telegram_token = token_urlsafe(15)
|
||||
user.profile.save()
|
||||
|
||||
context['tg_token'] = user.profile.telegram_token
|
||||
context['tg_bot_name'] = config("TELEGRAM_BOT_NAME")
|
||||
context["tg_token"] = user.profile.telegram_token
|
||||
context["tg_bot_name"] = config("TELEGRAM_BOT_NAME")
|
||||
|
||||
return context
|
||||
|
||||
def send_message(self, user, text):
|
||||
""" sends a message to a user with telegram notifications enabled"""
|
||||
"""sends a message to a user with telegram notifications enabled"""
|
||||
|
||||
bot_token=config('TELEGRAM_TOKEN')
|
||||
bot_token = config("TELEGRAM_TOKEN")
|
||||
|
||||
chat_id = user.profile.telegram_chat_id
|
||||
message_url = f'https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}'
|
||||
|
||||
message_url = f"https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}"
|
||||
|
||||
# if it fails, it should keep trying
|
||||
while True:
|
||||
try:
|
||||
@ -41,13 +42,13 @@ class Telegram():
|
||||
return
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def welcome(self, user):
|
||||
''' User enabled Telegram Notifications'''
|
||||
"""User enabled Telegram Notifications"""
|
||||
lang = user.profile.telegram_lang_code
|
||||
|
||||
if lang == 'es':
|
||||
text = f'Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats.'
|
||||
if lang == "es":
|
||||
text = f"Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats."
|
||||
else:
|
||||
text = f"Hey {user.username}, I will send you notifications about your RoboSats orders."
|
||||
self.send_message(user, text)
|
||||
@ -75,18 +76,18 @@ class Telegram():
|
||||
def order_taken_confirmed(self, order):
|
||||
if order.maker.profile.telegram_enabled:
|
||||
lang = order.maker.profile.telegram_lang_code
|
||||
if lang == 'es':
|
||||
text = f'Hey {order.maker.username} ¡Tu orden con ID {order.id} ha sido tomada por {order.taker.username}!🥳 Visita http://{self.site}/order/{order.id} para continuar.'
|
||||
if lang == "es":
|
||||
text = f"Hey {order.maker.username} ¡Tu orden con ID {order.id} ha sido tomada por {order.taker.username}!🥳 Visita http://{self.site}/order/{order.id} para continuar."
|
||||
else:
|
||||
text = f'Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳 Visit http://{self.site}/order/{order.id} to proceed with the trade.'
|
||||
text = f"Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳 Visit http://{self.site}/order/{order.id} to proceed with the trade."
|
||||
self.send_message(order.maker, text)
|
||||
|
||||
if order.taker.profile.telegram_enabled:
|
||||
lang = order.taker.profile.telegram_lang_code
|
||||
if lang == 'es':
|
||||
text = f'Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}.'
|
||||
if lang == "es":
|
||||
text = f"Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}."
|
||||
else:
|
||||
text = f'Hey {order.taker.username}, you just took the order with ID {order.id}.'
|
||||
text = f"Hey {order.taker.username}, you just took the order with ID {order.id}."
|
||||
self.send_message(order.taker, text)
|
||||
|
||||
return
|
||||
@ -95,20 +96,20 @@ class Telegram():
|
||||
for user in [order.maker, order.taker]:
|
||||
if user.profile.telegram_enabled:
|
||||
lang = user.profile.telegram_lang_code
|
||||
if lang == 'es':
|
||||
text = f'Hey {user.username}, el depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat. Visita http://{self.site}/order/{order.id} para hablar con tu contraparte.'
|
||||
if lang == "es":
|
||||
text = f"Hey {user.username}, el depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat. Visita http://{self.site}/order/{order.id} para hablar con tu contraparte."
|
||||
else:
|
||||
text = f'Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat. Visit http://{self.site}/order/{order.id} to talk with your counterpart.'
|
||||
text = f"Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat. Visit http://{self.site}/order/{order.id} to talk with your counterpart."
|
||||
self.send_message(user, text)
|
||||
return
|
||||
|
||||
def order_expired_untaken(self, order):
|
||||
if order.maker.profile.telegram_enabled:
|
||||
lang = order.maker.profile.telegram_lang_code
|
||||
if lang == 'es':
|
||||
text = f'Hey {order.maker.username}, tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{self.site}/order/{order.id} para renovarla.'
|
||||
if lang == "es":
|
||||
text = f"Hey {order.maker.username}, tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{self.site}/order/{order.id} para renovarla."
|
||||
else:
|
||||
text = f'Hey {order.maker.username}, your order with ID {order.id} has expired without a taker. Visit http://{self.site}/order/{order.id} to renew it.'
|
||||
text = f"Hey {order.maker.username}, your order with ID {order.id} has expired without a taker. Visit http://{self.site}/order/{order.id} to renew it."
|
||||
self.send_message(order.maker, text)
|
||||
return
|
||||
|
||||
@ -116,42 +117,42 @@ class Telegram():
|
||||
for user in [order.maker, order.taker]:
|
||||
if user.profile.telegram_enabled:
|
||||
lang = user.profile.telegram_lang_code
|
||||
if lang == 'es':
|
||||
text = f'¡Tu orden con ID {order.id} ha finalizado exitosamente!⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar.'
|
||||
if lang == "es":
|
||||
text = f"¡Tu orden con ID {order.id} ha finalizado exitosamente!⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar."
|
||||
else:
|
||||
text = f'Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve.'
|
||||
text = f"Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve."
|
||||
self.send_message(user, text)
|
||||
return
|
||||
|
||||
def public_order_cancelled(self, order):
|
||||
if order.maker.profile.telegram_enabled:
|
||||
lang = order.maker.profile.telegram_lang_code
|
||||
if lang == 'es':
|
||||
text = f'Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}.'
|
||||
if lang == "es":
|
||||
text = f"Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}."
|
||||
else:
|
||||
text = f'Hey {order.maker.username}, you have cancelled your public order with ID {order.id}.'
|
||||
text = f"Hey {order.maker.username}, you have cancelled your public order with ID {order.id}."
|
||||
self.send_message(order.maker, text)
|
||||
return
|
||||
|
||||
|
||||
def collaborative_cancelled(self, order):
|
||||
for user in [order.maker, order.taker]:
|
||||
if user.profile.telegram_enabled:
|
||||
lang = user.profile.telegram_lang_code
|
||||
if lang == 'es':
|
||||
text = f'Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente.'
|
||||
if lang == "es":
|
||||
text = f"Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente."
|
||||
else:
|
||||
text = f'Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled.'
|
||||
text = f"Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled."
|
||||
self.send_message(user, text)
|
||||
return
|
||||
|
||||
|
||||
def dispute_opened(self, order):
|
||||
for user in [order.maker, order.taker]:
|
||||
if user.profile.telegram_enabled:
|
||||
lang = user.profile.telegram_lang_code
|
||||
if lang == 'es':
|
||||
text = f'Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa.'
|
||||
if lang == "es":
|
||||
text = f"Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa."
|
||||
else:
|
||||
text = f'Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}.'
|
||||
text = f"Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}."
|
||||
self.send_message(user, text)
|
||||
return
|
||||
|
||||
@ -163,8 +164,8 @@ class Telegram():
|
||||
if len(queryset) == 0:
|
||||
return
|
||||
order = queryset.last()
|
||||
if lang == 'es':
|
||||
text = f'Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes.'
|
||||
if lang == "es":
|
||||
text = f"Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes."
|
||||
else:
|
||||
text = f"Hey {order.maker.username}, your order with ID {str(order.id)} is public in the order book."
|
||||
self.send_message(order.maker, text)
|
||||
|
474
api/models.py
474
api/models.py
@ -29,12 +29,11 @@ DEFAULT_BOND_SIZE = float(config("DEFAULT_BOND_SIZE"))
|
||||
class Currency(models.Model):
|
||||
|
||||
currency_dict = json.load(open("frontend/static/assets/currencies.json"))
|
||||
currency_choices = [(int(val), label)
|
||||
for val, label in list(currency_dict.items())]
|
||||
currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
|
||||
|
||||
currency = models.PositiveSmallIntegerField(choices=currency_choices,
|
||||
null=False,
|
||||
unique=True)
|
||||
currency = models.PositiveSmallIntegerField(
|
||||
choices=currency_choices, null=False, unique=True
|
||||
)
|
||||
exchange_rate = models.DecimalField(
|
||||
max_digits=14,
|
||||
decimal_places=4,
|
||||
@ -54,7 +53,6 @@ class Currency(models.Model):
|
||||
|
||||
|
||||
class LNPayment(models.Model):
|
||||
|
||||
class Types(models.IntegerChoices):
|
||||
NORM = 0, "Regular invoice"
|
||||
HOLD = 1, "hold invoice"
|
||||
@ -80,77 +78,78 @@ class LNPayment(models.Model):
|
||||
|
||||
class FailureReason(models.IntegerChoices):
|
||||
NOTYETF = 0, "Payment isn't failed (yet)"
|
||||
TIMEOUT = 1, "There are more routes to try, but the payment timeout was exceeded."
|
||||
NOROUTE = 2, "All possible routes were tried and failed permanently. Or there were no routes to the destination at all."
|
||||
TIMEOUT = (
|
||||
1,
|
||||
"There are more routes to try, but the payment timeout was exceeded.",
|
||||
)
|
||||
NOROUTE = (
|
||||
2,
|
||||
"All possible routes were tried and failed permanently. Or there were no routes to the destination at all.",
|
||||
)
|
||||
NONRECO = 3, "A non-recoverable error has occurred."
|
||||
INCORRE = 4, "Payment details are incorrect (unknown hash, invalid amount or invalid final CLTV delta)."
|
||||
INCORRE = (
|
||||
4,
|
||||
"Payment details are incorrect (unknown hash, invalid amount or invalid final CLTV delta).",
|
||||
)
|
||||
NOBALAN = 5, "Insufficient unlocked balance in RoboSats' node."
|
||||
|
||||
# payment use details
|
||||
type = models.PositiveSmallIntegerField(choices=Types.choices,
|
||||
null=False,
|
||||
default=Types.HOLD)
|
||||
concept = models.PositiveSmallIntegerField(choices=Concepts.choices,
|
||||
null=False,
|
||||
default=Concepts.MAKEBOND)
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices,
|
||||
null=False,
|
||||
default=Status.INVGEN)
|
||||
failure_reason = models.PositiveSmallIntegerField(choices=FailureReason.choices,
|
||||
null=True,
|
||||
default=None)
|
||||
type = models.PositiveSmallIntegerField(
|
||||
choices=Types.choices, null=False, default=Types.HOLD
|
||||
)
|
||||
concept = models.PositiveSmallIntegerField(
|
||||
choices=Concepts.choices, null=False, default=Concepts.MAKEBOND
|
||||
)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
choices=Status.choices, null=False, default=Status.INVGEN
|
||||
)
|
||||
failure_reason = models.PositiveSmallIntegerField(
|
||||
choices=FailureReason.choices, null=True, default=None
|
||||
)
|
||||
|
||||
# payment info
|
||||
payment_hash = models.CharField(max_length=100,
|
||||
unique=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
primary_key=True)
|
||||
payment_hash = models.CharField(
|
||||
max_length=100, unique=True, default=None, blank=True, primary_key=True
|
||||
)
|
||||
invoice = models.CharField(
|
||||
max_length=1200, unique=True, null=True, default=None,
|
||||
blank=True) # Some invoices with lots of routing hints might be long
|
||||
preimage = models.CharField(max_length=64,
|
||||
unique=True,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
description = models.CharField(max_length=500,
|
||||
unique=False,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
num_satoshis = models.PositiveBigIntegerField(validators=[
|
||||
MinValueValidator(100),
|
||||
MaxValueValidator(1.5 * MAX_TRADE),
|
||||
])
|
||||
max_length=1200, unique=True, null=True, default=None, blank=True
|
||||
) # Some invoices with lots of routing hints might be long
|
||||
preimage = models.CharField(
|
||||
max_length=64, unique=True, null=True, default=None, blank=True
|
||||
)
|
||||
description = models.CharField(
|
||||
max_length=500, unique=False, null=True, default=None, blank=True
|
||||
)
|
||||
num_satoshis = models.PositiveBigIntegerField(
|
||||
validators=[
|
||||
MinValueValidator(100),
|
||||
MaxValueValidator(1.5 * MAX_TRADE),
|
||||
]
|
||||
)
|
||||
# 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
|
||||
)
|
||||
created_at = models.DateTimeField()
|
||||
expires_at = models.DateTimeField()
|
||||
cltv_expiry = models.PositiveSmallIntegerField(null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
expiry_height = models.PositiveBigIntegerField(null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
cltv_expiry = models.PositiveSmallIntegerField(null=True, default=None, blank=True)
|
||||
expiry_height = models.PositiveBigIntegerField(null=True, default=None, blank=True)
|
||||
|
||||
# routing
|
||||
routing_attempts = models.PositiveSmallIntegerField(null=False, default=0)
|
||||
last_routing_time = models.DateTimeField(null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
last_routing_time = models.DateTimeField(null=True, default=None, blank=True)
|
||||
in_flight = models.BooleanField(default=False, null=False, blank=False)
|
||||
# involved parties
|
||||
sender = models.ForeignKey(User,
|
||||
related_name="sender",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None)
|
||||
receiver = models.ForeignKey(User,
|
||||
related_name="receiver",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None)
|
||||
sender = models.ForeignKey(
|
||||
User, related_name="sender", on_delete=models.SET_NULL, null=True, default=None
|
||||
)
|
||||
receiver = models.ForeignKey(
|
||||
User,
|
||||
related_name="receiver",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
|
||||
@ -166,89 +165,89 @@ class LNPayment(models.Model):
|
||||
# We created a truncated property for display 'hash'
|
||||
return truncatechars(self.payment_hash, 10)
|
||||
|
||||
class OnchainPayment(models.Model):
|
||||
|
||||
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
|
||||
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)
|
||||
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)
|
||||
address = models.CharField(
|
||||
max_length=100, unique=False, default=None, null=True, 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),
|
||||
])
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
receiver = models.ForeignKey(
|
||||
User,
|
||||
related_name="tx_receiver",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"TX-{str(self.id)}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
|
||||
@ -262,8 +261,8 @@ class OnchainPayment(models.Model):
|
||||
# Display txid as 'hash' truncated
|
||||
return truncatechars(self.txid, 10)
|
||||
|
||||
class Order(models.Model):
|
||||
|
||||
class Order(models.Model):
|
||||
class Types(models.IntegerChoices):
|
||||
BUY = 0, "BUY"
|
||||
SELL = 1, "SELL"
|
||||
@ -297,30 +296,30 @@ class Order(models.Model):
|
||||
NESINV = 4, "Neither escrow locked or invoice submitted"
|
||||
|
||||
# order info
|
||||
reference = models.UUIDField(default = uuid.uuid4, editable = False)
|
||||
status = models.PositiveSmallIntegerField(choices=Status.choices,
|
||||
null=False,
|
||||
default=Status.WFB)
|
||||
reference = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
status = models.PositiveSmallIntegerField(
|
||||
choices=Status.choices, null=False, default=Status.WFB
|
||||
)
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
expires_at = models.DateTimeField()
|
||||
expiry_reason = models.PositiveSmallIntegerField(choices=ExpiryReasons.choices,
|
||||
null=True,
|
||||
blank=True,
|
||||
default=None)
|
||||
expiry_reason = models.PositiveSmallIntegerField(
|
||||
choices=ExpiryReasons.choices, null=True, blank=True, default=None
|
||||
)
|
||||
|
||||
# order details
|
||||
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
|
||||
currency = models.ForeignKey(Currency,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
|
||||
amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
|
||||
has_range = models.BooleanField(default=False, null=False, blank=False)
|
||||
min_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
|
||||
max_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
|
||||
payment_method = models.CharField(max_length=70,
|
||||
null=False,
|
||||
default="not specified",
|
||||
blank=True)
|
||||
min_amount = models.DecimalField(
|
||||
max_digits=18, decimal_places=8, null=True, blank=True
|
||||
)
|
||||
max_amount = models.DecimalField(
|
||||
max_digits=18, decimal_places=8, null=True, blank=True
|
||||
)
|
||||
payment_method = models.CharField(
|
||||
max_length=70, null=False, default="not specified", blank=True
|
||||
)
|
||||
bondless_taker = models.BooleanField(default=False, null=False, blank=False)
|
||||
# order pricing method. A explicit amount of sats, or a relative premium above/below market.
|
||||
is_explicit = models.BooleanField(default=False, null=False)
|
||||
@ -330,37 +329,37 @@ class Order(models.Model):
|
||||
decimal_places=2,
|
||||
default=0,
|
||||
null=True,
|
||||
validators=[MinValueValidator(-100),
|
||||
MaxValueValidator(999)],
|
||||
validators=[MinValueValidator(-100), MaxValueValidator(999)],
|
||||
blank=True,
|
||||
)
|
||||
# explicit
|
||||
satoshis = models.PositiveBigIntegerField(
|
||||
null=True,
|
||||
validators=[
|
||||
MinValueValidator(MIN_TRADE),
|
||||
MaxValueValidator(MAX_TRADE)
|
||||
],
|
||||
validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)],
|
||||
blank=True,
|
||||
)
|
||||
# optionally makers can choose the public order duration length (seconds)
|
||||
public_duration = models.PositiveBigIntegerField(
|
||||
default=60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1,
|
||||
default=60 * 60 * int(config("DEFAULT_PUBLIC_ORDER_DURATION")) - 1,
|
||||
null=False,
|
||||
validators=[
|
||||
MinValueValidator(60*60*float(config("MIN_PUBLIC_ORDER_DURATION"))), # Min is 10 minutes
|
||||
MaxValueValidator(60*60*float(config("MAX_PUBLIC_ORDER_DURATION"))), # Max is 24 Hours
|
||||
MinValueValidator(
|
||||
60 * 60 * float(config("MIN_PUBLIC_ORDER_DURATION"))
|
||||
), # Min is 10 minutes
|
||||
MaxValueValidator(
|
||||
60 * 60 * float(config("MAX_PUBLIC_ORDER_DURATION"))
|
||||
), # Max is 24 Hours
|
||||
],
|
||||
blank=False,
|
||||
)
|
||||
|
||||
# optionally makers can choose the escrow lock / invoice submission step length (seconds)
|
||||
escrow_duration = models.PositiveBigIntegerField(
|
||||
default=60 * int(config("INVOICE_AND_ESCROW_DURATION"))-1,
|
||||
default=60 * int(config("INVOICE_AND_ESCROW_DURATION")) - 1,
|
||||
null=False,
|
||||
validators=[
|
||||
MinValueValidator(60*30), # Min is 30 minutes
|
||||
MaxValueValidator(60*60*8), # Max is 8 Hours
|
||||
MinValueValidator(60 * 30), # Min is 30 minutes
|
||||
MaxValueValidator(60 * 60 * 8), # Max is 8 Hours
|
||||
],
|
||||
blank=False,
|
||||
)
|
||||
@ -372,8 +371,8 @@ class Order(models.Model):
|
||||
default=DEFAULT_BOND_SIZE,
|
||||
null=False,
|
||||
validators=[
|
||||
MinValueValidator(float(config("MIN_BOND_SIZE"))), # 1 %
|
||||
MaxValueValidator(float(config("MAX_BOND_SIZE"))), # 15 %
|
||||
MinValueValidator(float(config("MIN_BOND_SIZE"))), # 1 %
|
||||
MaxValueValidator(float(config("MAX_BOND_SIZE"))), # 15 %
|
||||
],
|
||||
blank=False,
|
||||
)
|
||||
@ -381,29 +380,24 @@ class Order(models.Model):
|
||||
# how many sats at creation and at last check (relevant for marked to market)
|
||||
t0_satoshis = models.PositiveBigIntegerField(
|
||||
null=True,
|
||||
validators=[
|
||||
MinValueValidator(MIN_TRADE),
|
||||
MaxValueValidator(MAX_TRADE)
|
||||
],
|
||||
validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)],
|
||||
blank=True,
|
||||
) # sats at creation
|
||||
last_satoshis = models.PositiveBigIntegerField(
|
||||
null=True,
|
||||
validators=[MinValueValidator(0),
|
||||
MaxValueValidator(MAX_TRADE * 2)],
|
||||
validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE * 2)],
|
||||
blank=True,
|
||||
) # sats last time checked. Weird if 2* trade max...
|
||||
# timestamp of last_satoshis
|
||||
last_satoshis_time = models.DateTimeField(null=True, default=None, blank=True)
|
||||
# time the fiat exchange is confirmed and Sats released to buyer
|
||||
contract_finalization_time = models.DateTimeField(null=True, default=None, blank=True)
|
||||
contract_finalization_time = models.DateTimeField(
|
||||
null=True, default=None, blank=True
|
||||
)
|
||||
# order participants
|
||||
maker = models.ForeignKey(
|
||||
User,
|
||||
related_name="maker",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None) # unique = True, a maker can only make one order
|
||||
User, related_name="maker", on_delete=models.SET_NULL, null=True, default=None
|
||||
) # unique = True, a maker can only make one order
|
||||
taker = models.ForeignKey(
|
||||
User,
|
||||
related_name="taker",
|
||||
@ -416,21 +410,19 @@ class Order(models.Model):
|
||||
taker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
|
||||
|
||||
# When collaborative cancel is needed and one partner has cancelled.
|
||||
maker_asked_cancel = models.BooleanField(default=False, null=False)
|
||||
maker_asked_cancel = models.BooleanField(default=False, null=False)
|
||||
taker_asked_cancel = models.BooleanField(default=False, null=False)
|
||||
|
||||
is_fiat_sent = models.BooleanField(default=False, null=False)
|
||||
|
||||
# in dispute
|
||||
is_disputed = models.BooleanField(default=False, null=False)
|
||||
maker_statement = models.TextField(max_length=5000,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
taker_statement = models.TextField(max_length=5000,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
maker_statement = models.TextField(
|
||||
max_length=5000, null=True, default=None, blank=True
|
||||
)
|
||||
taker_statement = models.TextField(
|
||||
max_length=5000, null=True, default=None, blank=True
|
||||
)
|
||||
|
||||
# LNpayments
|
||||
# Order collateral
|
||||
@ -487,7 +479,7 @@ class Order(models.Model):
|
||||
|
||||
def __str__(self):
|
||||
if self.has_range and self.amount == None:
|
||||
amt = str(float(self.min_amount))+"-"+ str(float(self.max_amount))
|
||||
amt = str(float(self.min_amount)) + "-" + str(float(self.max_amount))
|
||||
else:
|
||||
amt = float(self.amount)
|
||||
return f"Order {self.id}: {self.Types(self.type).label} BTC for {amt} {self.currency}"
|
||||
@ -495,27 +487,33 @@ class Order(models.Model):
|
||||
def t_to_expire(self, status):
|
||||
|
||||
t_to_expire = {
|
||||
0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
|
||||
1: self.public_duration, # 'Public'
|
||||
2: 0, # 'Deleted'
|
||||
3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
|
||||
4: 0, # 'Cancelled'
|
||||
5: 0, # 'Expired'
|
||||
6: int(self.escrow_duration), # 'Waiting for trade collateral and buyer invoice'
|
||||
7: int(self.escrow_duration), # 'Waiting only for seller trade collateral'
|
||||
8: int(self.escrow_duration), # 'Waiting only for buyer invoice'
|
||||
9: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
|
||||
10: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")),# 'Fiat sent - In chatroom'
|
||||
11: 1 * 24 * 60 * 60, # 'In dispute'
|
||||
12: 0, # 'Collaboratively cancelled'
|
||||
13: 100 * 24 * 60 * 60, # 'Sending satoshis to buyer'
|
||||
14: 100 * 24 * 60 * 60, # 'Sucessful trade'
|
||||
15: 100 * 24 * 60 * 60, # 'Failed lightning network routing'
|
||||
16: 100 * 24 * 60 * 60, # 'Wait for dispute resolution'
|
||||
17: 100 * 24 * 60 * 60, # 'Maker lost dispute'
|
||||
18: 100 * 24 * 60 * 60, # 'Taker lost dispute'
|
||||
0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
|
||||
1: self.public_duration, # 'Public'
|
||||
2: 0, # 'Deleted'
|
||||
3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
|
||||
4: 0, # 'Cancelled'
|
||||
5: 0, # 'Expired'
|
||||
6: int(
|
||||
self.escrow_duration
|
||||
), # 'Waiting for trade collateral and buyer invoice'
|
||||
7: int(self.escrow_duration), # 'Waiting only for seller trade collateral'
|
||||
8: int(self.escrow_duration), # 'Waiting only for buyer invoice'
|
||||
9: 60
|
||||
* 60
|
||||
* int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
|
||||
10: 60
|
||||
* 60
|
||||
* int(config("FIAT_EXCHANGE_DURATION")), # 'Fiat sent - In chatroom'
|
||||
11: 1 * 24 * 60 * 60, # 'In dispute'
|
||||
12: 0, # 'Collaboratively cancelled'
|
||||
13: 100 * 24 * 60 * 60, # 'Sending satoshis to buyer'
|
||||
14: 100 * 24 * 60 * 60, # 'Sucessful trade'
|
||||
15: 100 * 24 * 60 * 60, # 'Failed lightning network routing'
|
||||
16: 100 * 24 * 60 * 60, # 'Wait for dispute resolution'
|
||||
17: 100 * 24 * 60 * 60, # 'Maker lost dispute'
|
||||
18: 100 * 24 * 60 * 60, # 'Taker lost dispute'
|
||||
}
|
||||
|
||||
|
||||
return t_to_expire[status]
|
||||
|
||||
|
||||
@ -570,53 +568,27 @@ class Profile(models.Model):
|
||||
decimal_places=1,
|
||||
default=None,
|
||||
null=True,
|
||||
validators=[MinValueValidator(0),
|
||||
MaxValueValidator(100)],
|
||||
validators=[MinValueValidator(0), MaxValueValidator(100)],
|
||||
blank=True,
|
||||
)
|
||||
# Used to deep link telegram chat in case telegram notifications are enabled
|
||||
telegram_token = models.CharField(
|
||||
max_length=20,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
telegram_chat_id = models.BigIntegerField(
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True
|
||||
)
|
||||
telegram_enabled = models.BooleanField(
|
||||
default=False,
|
||||
null=False
|
||||
)
|
||||
telegram_lang_code = models.CharField(
|
||||
max_length=10,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
telegram_welcomed = models.BooleanField(
|
||||
default=False,
|
||||
null=False
|
||||
)
|
||||
telegram_token = models.CharField(max_length=20, null=True, blank=True)
|
||||
telegram_chat_id = models.BigIntegerField(null=True, default=None, blank=True)
|
||||
telegram_enabled = models.BooleanField(default=False, null=False)
|
||||
telegram_lang_code = models.CharField(max_length=10, null=True, blank=True)
|
||||
telegram_welcomed = models.BooleanField(default=False, null=False)
|
||||
|
||||
# Referral program
|
||||
is_referred = models.BooleanField(
|
||||
default=False,
|
||||
null=False
|
||||
)
|
||||
is_referred = models.BooleanField(default=False, null=False)
|
||||
referred_by = models.ForeignKey(
|
||||
'self',
|
||||
"self",
|
||||
related_name="referee",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True,
|
||||
)
|
||||
referral_code = models.CharField(
|
||||
max_length=15,
|
||||
null=True,
|
||||
blank=True
|
||||
)
|
||||
referral_code = models.CharField(max_length=15, null=True, blank=True)
|
||||
# Recent rewards from referred trades that will be "earned" at a later point to difficult spionage.
|
||||
pending_rewards = models.PositiveIntegerField(null=False, default=0)
|
||||
# Claimable rewards
|
||||
@ -644,18 +616,13 @@ class Profile(models.Model):
|
||||
)
|
||||
|
||||
# Penalty expiration (only used then taking/cancelling repeatedly orders in the book before comitting bond)
|
||||
penalty_expiration = models.DateTimeField(null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
penalty_expiration = models.DateTimeField(null=True, default=None, blank=True)
|
||||
|
||||
# Platform rate
|
||||
platform_rating = models.PositiveIntegerField(null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
platform_rating = models.PositiveIntegerField(null=True, default=None, blank=True)
|
||||
|
||||
# Stealth invoices
|
||||
wants_stealth = models.BooleanField(default=True,
|
||||
null=False)
|
||||
wants_stealth = models.BooleanField(default=True, null=False)
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def create_user_profile(sender, instance, created, **kwargs):
|
||||
@ -669,8 +636,9 @@ class Profile(models.Model):
|
||||
@receiver(pre_delete, sender=User)
|
||||
def del_avatar_from_disk(sender, instance, **kwargs):
|
||||
try:
|
||||
avatar_file = Path(settings.AVATAR_ROOT +
|
||||
instance.profile.avatar.url.split("/")[-1])
|
||||
avatar_file = Path(
|
||||
settings.AVATAR_ROOT + instance.profile.avatar.url.split("/")[-1]
|
||||
)
|
||||
avatar_file.unlink()
|
||||
except:
|
||||
pass
|
||||
@ -686,8 +654,7 @@ class Profile(models.Model):
|
||||
|
||||
# method to create a fake table field in read only mode
|
||||
def avatar_tag(self):
|
||||
return mark_safe('<img src="%s" width="50" height="50" />' %
|
||||
self.get_avatar())
|
||||
return mark_safe('<img src="%s" width="50" height="50" />' % self.get_avatar())
|
||||
|
||||
|
||||
class MarketTick(models.Model):
|
||||
@ -723,13 +690,10 @@ class MarketTick(models.Model):
|
||||
decimal_places=2,
|
||||
default=None,
|
||||
null=True,
|
||||
validators=[MinValueValidator(-100),
|
||||
MaxValueValidator(999)],
|
||||
validators=[MinValueValidator(-100), MaxValueValidator(999)],
|
||||
blank=True,
|
||||
)
|
||||
currency = models.ForeignKey(Currency,
|
||||
null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
|
||||
timestamp = models.DateTimeField(default=timezone.now)
|
||||
|
||||
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed
|
||||
@ -737,8 +701,7 @@ class MarketTick(models.Model):
|
||||
max_digits=4,
|
||||
decimal_places=4,
|
||||
default=FEE,
|
||||
validators=[MinValueValidator(0),
|
||||
MaxValueValidator(1)],
|
||||
validators=[MinValueValidator(0), MaxValueValidator(1)],
|
||||
)
|
||||
|
||||
def log_a_tick(order):
|
||||
@ -755,10 +718,9 @@ class MarketTick(models.Model):
|
||||
market_exchange_rate = float(order.currency.exchange_rate)
|
||||
premium = 100 * (price / market_exchange_rate - 1)
|
||||
|
||||
tick = MarketTick.objects.create(price=price,
|
||||
volume=volume,
|
||||
premium=premium,
|
||||
currency=order.currency)
|
||||
tick = MarketTick.objects.create(
|
||||
price=price, volume=volume, premium=premium, currency=order.currency
|
||||
)
|
||||
|
||||
tick.save()
|
||||
|
||||
@ -767,4 +729,4 @@ class MarketTick(models.Model):
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Market tick"
|
||||
verbose_name_plural = "Market ticks"
|
||||
verbose_name_plural = "Market ticks"
|
||||
|
@ -4823,8 +4823,7 @@ adjectives = [
|
||||
"Vindictive",
|
||||
"Chatting",
|
||||
"Nightmarish",
|
||||
"Niggardly"
|
||||
"Hated",
|
||||
"Niggardly" "Hated",
|
||||
"Satiric",
|
||||
"Shattering",
|
||||
"Fabled",
|
||||
|
@ -2,6 +2,7 @@ from .utils import human_format
|
||||
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
"""
|
||||
Deterministic nick generator from SHA256 hash.
|
||||
|
||||
@ -15,7 +16,6 @@ is a total of to 450*4800*12500*1000 =
|
||||
|
||||
|
||||
class NickGenerator:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
lang="English",
|
||||
@ -43,11 +43,13 @@ class NickGenerator:
|
||||
raise ValueError("Language not implemented.")
|
||||
|
||||
if verbose:
|
||||
print(f"{lang} SHA256 Nick Generator initialized with:" +
|
||||
f"\nUp to {len(adverbs)} adverbs." +
|
||||
f"\nUp to {len(adjectives)} adjectives." +
|
||||
f"\nUp to {len(nouns)} nouns." +
|
||||
f"\nUp to {max_num+1} numerics.\n")
|
||||
print(
|
||||
f"{lang} SHA256 Nick Generator initialized with:"
|
||||
+ f"\nUp to {len(adverbs)} adverbs."
|
||||
+ f"\nUp to {len(adjectives)} adjectives."
|
||||
+ f"\nUp to {len(nouns)} nouns."
|
||||
+ f"\nUp to {max_num+1} numerics.\n"
|
||||
)
|
||||
|
||||
self.use_adv = use_adv
|
||||
self.use_adj = use_adj
|
||||
@ -147,10 +149,7 @@ class NickGenerator:
|
||||
i = i + 1
|
||||
return "", 0, 0, i
|
||||
|
||||
def compute_pool_size_loss(self,
|
||||
max_length=22,
|
||||
max_iter=1000000,
|
||||
num_runs=5000):
|
||||
def compute_pool_size_loss(self, max_length=22, max_iter=1000000, num_runs=5000):
|
||||
"""
|
||||
Computes median an average loss of
|
||||
nick pool diversity due to max_lenght
|
||||
@ -196,13 +195,16 @@ if __name__ == "__main__":
|
||||
|
||||
# Generates a short nick with length limit from SHA256
|
||||
nick, nick_id, pool_size, iterations = GenNick.short_from_SHA256(
|
||||
hash, max_length, max_iter)
|
||||
hash, max_length, max_iter
|
||||
)
|
||||
|
||||
# Output
|
||||
print(f"Nick number {nick_id} has been selected among" +
|
||||
f" {human_format(pool_size)} possible nicks.\n" +
|
||||
f"Needed {iterations} iterations to find one " +
|
||||
f"this short.\nYour nick is {nick} !\n")
|
||||
print(
|
||||
f"Nick number {nick_id} has been selected among"
|
||||
+ f" {human_format(pool_size)} possible nicks.\n"
|
||||
+ f"Needed {iterations} iterations to find one "
|
||||
+ f"this short.\nYour nick is {nick} !\n"
|
||||
)
|
||||
print(f"Nick lenght is {len(nick)} characters.")
|
||||
print(f"Nick landed at height {nick_id/(pool_size+1)} on the pool.")
|
||||
print(f"Took {time.time()-t0} secs.\n")
|
||||
@ -216,9 +218,8 @@ if __name__ == "__main__":
|
||||
string = str(random.uniform(0, 1000000))
|
||||
hash = hashlib.sha256(str.encode(string)).hexdigest()
|
||||
print(
|
||||
GenNick.short_from_SHA256(hash,
|
||||
max_length=max_length,
|
||||
max_iter=max_iter)[0])
|
||||
GenNick.short_from_SHA256(hash, max_length=max_length, max_iter=max_iter)[0]
|
||||
)
|
||||
|
||||
# Other analysis
|
||||
GenNick.compute_pool_size_loss(max_length, max_iter, 200)
|
||||
|
@ -2,9 +2,7 @@ from math import log, floor
|
||||
|
||||
|
||||
def human_format(number):
|
||||
units = [
|
||||
"", " Thousand", " Million", " Billion", " Trillion", " Quatrillion"
|
||||
]
|
||||
units = ["", " Thousand", " Million", " Billion", " Trillion", " Quatrillion"]
|
||||
k = 1000.0
|
||||
magnitude = int(floor(log(number, k)))
|
||||
return "%.2f%s" % (number / k**magnitude, units[magnitude])
|
||||
|
@ -3,17 +3,26 @@ from .models import MarketTick, Order
|
||||
from decouple import config
|
||||
|
||||
RETRY_TIME = int(config("RETRY_TIME"))
|
||||
MIN_PUBLIC_ORDER_DURATION_SECS=60*60*float(config("MIN_PUBLIC_ORDER_DURATION"))
|
||||
MAX_PUBLIC_ORDER_DURATION_SECS=60*60*float(config("MAX_PUBLIC_ORDER_DURATION"))
|
||||
MIN_PUBLIC_ORDER_DURATION_SECS = 60 * 60 * float(config("MIN_PUBLIC_ORDER_DURATION"))
|
||||
MAX_PUBLIC_ORDER_DURATION_SECS = 60 * 60 * float(config("MAX_PUBLIC_ORDER_DURATION"))
|
||||
|
||||
|
||||
class InfoSerializer(serializers.Serializer):
|
||||
num_public_buy_orders = serializers.IntegerField()
|
||||
num_public_sell_orders = serializers.IntegerField()
|
||||
book_liquidity = serializers.IntegerField(help_text='Total amount of BTC in the order book')
|
||||
book_liquidity = serializers.IntegerField(
|
||||
help_text="Total amount of BTC in the order book"
|
||||
)
|
||||
active_robots_today = serializers.CharField()
|
||||
last_day_nonkyc_btc_premium = serializers.FloatField(help_text='Average premium (weighted by volume) of the orders in the last 24h')
|
||||
last_day_volume = serializers.FloatField(help_text='Total volume in BTC in the last 24h')
|
||||
lifetime_volume = serializers.FloatField(help_text='Total volume in BTC since exchange\'s inception')
|
||||
last_day_nonkyc_btc_premium = serializers.FloatField(
|
||||
help_text="Average premium (weighted by volume) of the orders in the last 24h"
|
||||
)
|
||||
last_day_volume = serializers.FloatField(
|
||||
help_text="Total volume in BTC in the last 24h"
|
||||
)
|
||||
lifetime_volume = serializers.FloatField(
|
||||
help_text="Total volume in BTC since exchange's inception"
|
||||
)
|
||||
lnd_version = serializers.CharField()
|
||||
robosats_running_commit_hash = serializers.CharField()
|
||||
alternative_site = serializers.CharField()
|
||||
@ -21,17 +30,20 @@ class InfoSerializer(serializers.Serializer):
|
||||
node_alias = serializers.CharField()
|
||||
node_id = serializers.CharField()
|
||||
network = serializers.CharField()
|
||||
maker_fee = serializers.FloatField(help_text='Exchange\'s set maker fee')
|
||||
taker_fee = serializers.FloatField(help_text='Exchange\'s set taker fee ')
|
||||
bond_size = serializers.FloatField(help_text='Default bond size (percent)')
|
||||
current_swap_fee_rate = serializers.FloatField(help_text='Swap fees to perform on-chain transaction (percent)')
|
||||
nickname = serializers.CharField(help_text='Currenlty logged in Robot name')
|
||||
referral_code = serializers.CharField(help_text='Logged in users\'s referral code')
|
||||
earned_rewards = serializers.IntegerField(help_text='Logged in user\'s earned rewards in satoshis')
|
||||
maker_fee = serializers.FloatField(help_text="Exchange's set maker fee")
|
||||
taker_fee = serializers.FloatField(help_text="Exchange's set taker fee ")
|
||||
bond_size = serializers.FloatField(help_text="Default bond size (percent)")
|
||||
current_swap_fee_rate = serializers.FloatField(
|
||||
help_text="Swap fees to perform on-chain transaction (percent)"
|
||||
)
|
||||
nickname = serializers.CharField(help_text="Currenlty logged in Robot name")
|
||||
referral_code = serializers.CharField(help_text="Logged in users's referral code")
|
||||
earned_rewards = serializers.IntegerField(
|
||||
help_text="Logged in user's earned rewards in satoshis"
|
||||
)
|
||||
|
||||
|
||||
class ListOrderSerializer(serializers.ModelSerializer):
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = (
|
||||
@ -53,51 +65,46 @@ class ListOrderSerializer(serializers.ModelSerializer):
|
||||
"maker",
|
||||
"taker",
|
||||
"escrow_duration",
|
||||
"bond_size"
|
||||
"bond_size",
|
||||
)
|
||||
|
||||
|
||||
# Only used in oas_schemas
|
||||
class SummarySerializer(serializers.Serializer):
|
||||
sent_fiat = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="same as `amount` (only for buyer)"
|
||||
required=False, help_text="same as `amount` (only for buyer)"
|
||||
)
|
||||
received_sats = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="same as `trade_satoshis` (only for buyer)"
|
||||
required=False, help_text="same as `trade_satoshis` (only for buyer)"
|
||||
)
|
||||
is_swap = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="True if the payout was on-chain (only for buyer)"
|
||||
required=False, help_text="True if the payout was on-chain (only for buyer)"
|
||||
)
|
||||
received_onchain_sats = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="The on-chain sats received (only for buyer and if `is_swap` is `true`)"
|
||||
help_text="The on-chain sats received (only for buyer and if `is_swap` is `true`)",
|
||||
)
|
||||
mining_fee_sats = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="Mining fees paid in satoshis (only for buyer and if `is_swap` is `true`)"
|
||||
help_text="Mining fees paid in satoshis (only for buyer and if `is_swap` is `true`)",
|
||||
)
|
||||
swap_fee_sats = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="Exchange swap fee in sats (i.e excluding miner fees) (only for buyer and if `is_swap` is `true`)"
|
||||
help_text="Exchange swap fee in sats (i.e excluding miner fees) (only for buyer and if `is_swap` is `true`)",
|
||||
)
|
||||
swap_fee_percent = serializers.FloatField(
|
||||
required=False,
|
||||
help_text="same as `swap_fee_rate` (only for buyer and if `is_swap` is `true`"
|
||||
help_text="same as `swap_fee_rate` (only for buyer and if `is_swap` is `true`",
|
||||
)
|
||||
sent_sats = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="The total sats you sent (only for seller)"
|
||||
required=False, help_text="The total sats you sent (only for seller)"
|
||||
)
|
||||
received_fiat = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="same as `amount` (only for seller)"
|
||||
required=False, help_text="same as `amount` (only for seller)"
|
||||
)
|
||||
trade_fee_sats = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="Exchange fees in sats (Does not include swap fee and miner fee)"
|
||||
help_text="Exchange fees in sats (Does not include swap fee and miner fee)",
|
||||
)
|
||||
|
||||
|
||||
@ -105,45 +112,42 @@ class SummarySerializer(serializers.Serializer):
|
||||
class PlatformSummarySerializer(serializers.Serializer):
|
||||
contract_timestamp = serializers.DateTimeField(
|
||||
required=False,
|
||||
help_text="Timestamp of when the contract was finalized (price and sats fixed)"
|
||||
help_text="Timestamp of when the contract was finalized (price and sats fixed)",
|
||||
)
|
||||
contract_total_time = serializers.FloatField(
|
||||
required=False,
|
||||
help_text="The time taken for the contract to complete (from taker taking the order to completion of order) in seconds"
|
||||
help_text="The time taken for the contract to complete (from taker taking the order to completion of order) in seconds",
|
||||
)
|
||||
routing_fee_sats = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="Sats payed by the exchange for routing fees. Mining fee in case of on-chain swap payout"
|
||||
help_text="Sats payed by the exchange for routing fees. Mining fee in case of on-chain swap payout",
|
||||
)
|
||||
trade_revenue_sats = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="The sats the exchange earned from the trade"
|
||||
required=False, help_text="The sats the exchange earned from the trade"
|
||||
)
|
||||
|
||||
|
||||
|
||||
# Only used in oas_schemas
|
||||
class OrderDetailSerializer(serializers.ModelSerializer):
|
||||
total_secs_exp = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="Duration of time (in seconds) to expire, according to the current status of order."
|
||||
"This is duration of time after `created_at` (in seconds) that the order will automatically expire."
|
||||
"This value changes according to which stage the order is in"
|
||||
"This is duration of time after `created_at` (in seconds) that the order will automatically expire."
|
||||
"This value changes according to which stage the order is in",
|
||||
)
|
||||
penalty = serializers.DateTimeField(
|
||||
required=False,
|
||||
help_text="Time when the user penalty will expire. Penalty applies when you create orders repeatedly without commiting a bond"
|
||||
help_text="Time when the user penalty will expire. Penalty applies when you create orders repeatedly without commiting a bond",
|
||||
)
|
||||
is_maker = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="Whether you are the maker or not"
|
||||
required=False, help_text="Whether you are the maker or not"
|
||||
)
|
||||
is_taker = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="Whether you are the taker or not"
|
||||
required=False, help_text="Whether you are the taker or not"
|
||||
)
|
||||
is_participant = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="True if you are either a taker or maker, False otherwise"
|
||||
help_text="True if you are either a taker or maker, False otherwise",
|
||||
)
|
||||
maker_status = serializers.CharField(
|
||||
required=False,
|
||||
@ -151,193 +155,170 @@ class OrderDetailSerializer(serializers.ModelSerializer):
|
||||
"- **'Active'** (seen within last 2 min)\n"
|
||||
"- **'Seen Recently'** (seen within last 10 min)\n"
|
||||
"- **'Inactive'** (seen more than 10 min ago)\n\n"
|
||||
"Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty"
|
||||
"Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty",
|
||||
)
|
||||
taker_status = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="True if you are either a taker or maker, False otherwise"
|
||||
help_text="True if you are either a taker or maker, False otherwise",
|
||||
)
|
||||
price_now = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)"
|
||||
help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)",
|
||||
)
|
||||
premium = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="Premium over the CEX price at the current time"
|
||||
required=False, help_text="Premium over the CEX price at the current time"
|
||||
)
|
||||
premium_percentile = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="(Only if `is_maker`) Premium percentile of your order compared to other public orders in the same currency currently in the order book"
|
||||
help_text="(Only if `is_maker`) Premium percentile of your order compared to other public orders in the same currency currently in the order book",
|
||||
)
|
||||
num_similar_orders = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="(Only if `is_maker`) The number of public orders of the same currency currently in the order book"
|
||||
help_text="(Only if `is_maker`) The number of public orders of the same currency currently in the order book",
|
||||
)
|
||||
tg_enabled = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="(Only if `is_maker`) Whether Telegram notification is enabled or not"
|
||||
help_text="(Only if `is_maker`) Whether Telegram notification is enabled or not",
|
||||
)
|
||||
tg_token = serializers.CharField(
|
||||
required=False,
|
||||
help_text="(Only if `is_maker`) Your telegram bot token required to enable notifications."
|
||||
help_text="(Only if `is_maker`) Your telegram bot token required to enable notifications.",
|
||||
)
|
||||
tg_bot_name = serializers.CharField(
|
||||
required=False,
|
||||
help_text="(Only if `is_maker`) The Telegram username of the bot"
|
||||
help_text="(Only if `is_maker`) The Telegram username of the bot",
|
||||
)
|
||||
is_buyer = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="Whether you are a buyer of sats (you will be receiving sats)"
|
||||
help_text="Whether you are a buyer of sats (you will be receiving sats)",
|
||||
)
|
||||
is_seller = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="Whether you are a seller of sats or not (you will be sending sats)"
|
||||
help_text="Whether you are a seller of sats or not (you will be sending sats)",
|
||||
)
|
||||
maker_nick = serializers.CharField(
|
||||
required=False,
|
||||
help_text="Nickname (Robot name) of the maker"
|
||||
required=False, help_text="Nickname (Robot name) of the maker"
|
||||
)
|
||||
taker_nick = serializers.CharField(
|
||||
required=False,
|
||||
help_text="Nickname (Robot name) of the taker"
|
||||
required=False, help_text="Nickname (Robot name) of the taker"
|
||||
)
|
||||
status_message = serializers.CharField(
|
||||
required=False,
|
||||
help_text="The current status of the order corresponding to the `status`"
|
||||
help_text="The current status of the order corresponding to the `status`",
|
||||
)
|
||||
is_fiat_sent = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="Whether or not the fiat amount is sent by the buyer"
|
||||
required=False, help_text="Whether or not the fiat amount is sent by the buyer"
|
||||
)
|
||||
is_disputed = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="Whether or not the counterparty raised a dispute"
|
||||
)
|
||||
ur_nick = serializers.CharField(
|
||||
required=False,
|
||||
help_text="Your Nickname"
|
||||
)
|
||||
ur_nick = serializers.CharField(
|
||||
required=False,
|
||||
help_text="Your Nick"
|
||||
required=False, help_text="Whether or not the counterparty raised a dispute"
|
||||
)
|
||||
ur_nick = serializers.CharField(required=False, help_text="Your Nickname")
|
||||
ur_nick = serializers.CharField(required=False, help_text="Your Nick")
|
||||
maker_locked = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="True if maker bond is locked, False otherwise"
|
||||
required=False, help_text="True if maker bond is locked, False otherwise"
|
||||
)
|
||||
taker_locked = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="True if taker bond is locked, False otherwise"
|
||||
required=False, help_text="True if taker bond is locked, False otherwise"
|
||||
)
|
||||
escrow_locked = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="True if escrow is locked, False otherwise. Escrow is the sats to be sold, held by Robosats until the trade is finised."
|
||||
help_text="True if escrow is locked, False otherwise. Escrow is the sats to be sold, held by Robosats until the trade is finised.",
|
||||
)
|
||||
trade_satoshis = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="Seller sees the amount of sats they need to send. Buyer sees the amount of sats they will receive "
|
||||
help_text="Seller sees the amount of sats they need to send. Buyer sees the amount of sats they will receive ",
|
||||
)
|
||||
bond_invoice = serializers.CharField(
|
||||
required=False,
|
||||
help_text="When `status` = `0`, `3`. Bond invoice to be paid"
|
||||
required=False, help_text="When `status` = `0`, `3`. Bond invoice to be paid"
|
||||
)
|
||||
bond_satoshis = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="The bond amount in satoshis"
|
||||
required=False, help_text="The bond amount in satoshis"
|
||||
)
|
||||
escrow_invoice = serializers.CharField(
|
||||
required=False,
|
||||
help_text="For the seller, the escrow invoice to be held by RoboSats"
|
||||
help_text="For the seller, the escrow invoice to be held by RoboSats",
|
||||
)
|
||||
escrow_satoshis = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="The escrow amount in satoshis"
|
||||
required=False, help_text="The escrow amount in satoshis"
|
||||
)
|
||||
invoice_amount = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="The amount in sats the buyer needs to submit an invoice of to receive the trade amount"
|
||||
help_text="The amount in sats the buyer needs to submit an invoice of to receive the trade amount",
|
||||
)
|
||||
swap_allowed = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="Whether on-chain swap is allowed"
|
||||
required=False, help_text="Whether on-chain swap is allowed"
|
||||
)
|
||||
swap_failure_reason = serializers.CharField(
|
||||
required=False,
|
||||
help_text="Reason for why on-chain swap is not available"
|
||||
required=False, help_text="Reason for why on-chain swap is not available"
|
||||
)
|
||||
suggested_mining_fee_rate = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="fee in sats/vbyte for the on-chain swap"
|
||||
required=False, help_text="fee in sats/vbyte for the on-chain swap"
|
||||
)
|
||||
swap_fee_rate = serializers.FloatField(
|
||||
swap_fee_rate = serializers.FloatField(
|
||||
required=False,
|
||||
help_text="in percentage, the swap fee rate the platform charges"
|
||||
help_text="in percentage, the swap fee rate the platform charges",
|
||||
)
|
||||
pending_cancel = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="Your counterparty requested for a collaborative cancel when `status` is either `8`, `9` or `10`"
|
||||
help_text="Your counterparty requested for a collaborative cancel when `status` is either `8`, `9` or `10`",
|
||||
)
|
||||
asked_for_cancel = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="You requested for a collaborative cancel `status` is either `8`, `9` or `10`"
|
||||
help_text="You requested for a collaborative cancel `status` is either `8`, `9` or `10`",
|
||||
)
|
||||
statement_submitted = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="True if you have submitted a statement. Available when `status` is `11`"
|
||||
help_text="True if you have submitted a statement. Available when `status` is `11`",
|
||||
)
|
||||
retries = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="Number of times ln node has tried to make the payment to you (only if you are the buyer)"
|
||||
help_text="Number of times ln node has tried to make the payment to you (only if you are the buyer)",
|
||||
)
|
||||
next_retry_time = serializers.DateTimeField(
|
||||
required=False,
|
||||
help_text=f"The next time payment will be retried. Payment is retried every {RETRY_TIME} sec"
|
||||
help_text=f"The next time payment will be retried. Payment is retried every {RETRY_TIME} sec",
|
||||
)
|
||||
failure_reason = serializers.CharField(
|
||||
required=False,
|
||||
help_text="The reason the payout failed"
|
||||
required=False, help_text="The reason the payout failed"
|
||||
)
|
||||
invoice_expired = serializers.BooleanField(
|
||||
required=False,
|
||||
help_text="True if the payout invoice expired. `invoice_amount` will be re-set and sent which means the user has to submit a new invoice to be payed"
|
||||
help_text="True if the payout invoice expired. `invoice_amount` will be re-set and sent which means the user has to submit a new invoice to be payed",
|
||||
)
|
||||
trade_fee_percent = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="The fee for the trade (fees differ for maker and taker)"
|
||||
help_text="The fee for the trade (fees differ for maker and taker)",
|
||||
)
|
||||
bond_size_sats = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="The size of the bond in sats"
|
||||
required=False, help_text="The size of the bond in sats"
|
||||
)
|
||||
bond_size_percent = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="same as `bond_size`"
|
||||
required=False, help_text="same as `bond_size`"
|
||||
)
|
||||
maker_summary = SummarySerializer(required=False)
|
||||
taker_summary = SummarySerializer(required=False)
|
||||
platform_summary = PlatformSummarySerializer(required=True)
|
||||
expiry_message = serializers.CharField(
|
||||
required=False,
|
||||
help_text="The reason the order expired (message associated with the `expiry_reason`)"
|
||||
help_text="The reason the order expired (message associated with the `expiry_reason`)",
|
||||
)
|
||||
num_satoshis = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="only if status = `14` (Successful Trade) and is_buyer = `true`"
|
||||
help_text="only if status = `14` (Successful Trade) and is_buyer = `true`",
|
||||
)
|
||||
sent_satoshis = serializers.IntegerField(
|
||||
required=False,
|
||||
help_text="only if status = `14` (Successful Trade) and is_buyer = `true`"
|
||||
help_text="only if status = `14` (Successful Trade) and is_buyer = `true`",
|
||||
)
|
||||
txid = serializers.CharField(
|
||||
required=False,
|
||||
help_text="Transaction id of the on-chain swap payout. Only if status = `14` (Successful Trade) and is_buyer = `true`"
|
||||
help_text="Transaction id of the on-chain swap payout. Only if status = `14` (Successful Trade) and is_buyer = `true`",
|
||||
)
|
||||
network = serializers.CharField(
|
||||
required=False,
|
||||
help_text="The network eg. 'testnet', 'mainnet'. Only if status = `14` (Successful Trade) and is_buyer = `true`"
|
||||
help_text="The network eg. 'testnet', 'mainnet'. Only if status = `14` (Successful Trade) and is_buyer = `true`",
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
fields = (
|
||||
@ -392,7 +373,7 @@ class OrderDetailSerializer(serializers.ModelSerializer):
|
||||
"escrow_satoshis",
|
||||
"invoice_amount",
|
||||
"swap_allowed",
|
||||
'swap_failure_reason',
|
||||
"swap_failure_reason",
|
||||
"suggested_mining_fee_rate",
|
||||
"swap_fee_rate",
|
||||
"pending_cancel",
|
||||
@ -421,9 +402,16 @@ class OrderDetailSerializer(serializers.ModelSerializer):
|
||||
|
||||
class OrderPublicSerializer(serializers.ModelSerializer):
|
||||
maker_nick = serializers.CharField(required=False)
|
||||
maker_status = serializers.CharField(help_text='Status of the nick - "Active" or "Inactive"', required=False)
|
||||
price = serializers.FloatField(help_text="Price in order's fiat currency", required=False)
|
||||
satoshis_now = serializers.IntegerField(help_text="The amount of sats to be traded at the present moment (not including the fees)", required=False)
|
||||
maker_status = serializers.CharField(
|
||||
help_text='Status of the nick - "Active" or "Inactive"', required=False
|
||||
)
|
||||
price = serializers.FloatField(
|
||||
help_text="Price in order's fiat currency", required=False
|
||||
)
|
||||
satoshis_now = serializers.IntegerField(
|
||||
help_text="The amount of sats to be traded at the present moment (not including the fees)",
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Order
|
||||
@ -448,7 +436,7 @@ class OrderPublicSerializer(serializers.ModelSerializer):
|
||||
"price",
|
||||
"escrow_duration",
|
||||
"satoshis_now",
|
||||
"bond_size"
|
||||
"bond_size",
|
||||
)
|
||||
|
||||
|
||||
@ -461,19 +449,19 @@ class MakeOrderSerializer(serializers.ModelSerializer):
|
||||
max_length=70,
|
||||
default="not specified",
|
||||
required=False,
|
||||
help_text="Can be any string. The UI recognizes [these payment methods](https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/src/components/payment-methods/Methods.js) and displays them with a logo."
|
||||
help_text="Can be any string. The UI recognizes [these payment methods](https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/src/components/payment-methods/Methods.js) and displays them with a logo.",
|
||||
)
|
||||
is_explicit = serializers.BooleanField(
|
||||
default=False,
|
||||
help_text='Whether the order is explicitly priced or not. If set to `true` then `satoshis` need to be specified'
|
||||
help_text="Whether the order is explicitly priced or not. If set to `true` then `satoshis` need to be specified",
|
||||
)
|
||||
has_range = serializers.BooleanField(
|
||||
default=False,
|
||||
help_text='Whether the order specifies a range of amount or a fixed amount.\n\nIf `true`, then `min_amount` and `max_amount` fields are **required**.\n\n If `false` then `amount` is **required**',
|
||||
help_text="Whether the order specifies a range of amount or a fixed amount.\n\nIf `true`, then `min_amount` and `max_amount` fields are **required**.\n\n If `false` then `amount` is **required**",
|
||||
)
|
||||
bondless_taker = serializers.BooleanField(
|
||||
default=False,
|
||||
help_text='Whether bondless takers are allowed for this order or not',
|
||||
help_text="Whether bondless takers are allowed for this order or not",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -495,19 +483,17 @@ class MakeOrderSerializer(serializers.ModelSerializer):
|
||||
"bondless_taker",
|
||||
)
|
||||
|
||||
|
||||
class UpdateOrderSerializer(serializers.Serializer):
|
||||
invoice = serializers.CharField(max_length=2000,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
default=None)
|
||||
address = serializers.CharField(max_length=100,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
default=None)
|
||||
statement = serializers.CharField(max_length=10000,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
default=None)
|
||||
invoice = serializers.CharField(
|
||||
max_length=2000, allow_null=True, allow_blank=True, default=None
|
||||
)
|
||||
address = serializers.CharField(
|
||||
max_length=100, allow_null=True, allow_blank=True, default=None
|
||||
)
|
||||
statement = serializers.CharField(
|
||||
max_length=10000, allow_null=True, allow_blank=True, default=None
|
||||
)
|
||||
action = serializers.ChoiceField(
|
||||
choices=(
|
||||
"pause",
|
||||
@ -529,64 +515,86 @@ class UpdateOrderSerializer(serializers.Serializer):
|
||||
allow_blank=True,
|
||||
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)
|
||||
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):
|
||||
# Mandatory fields
|
||||
token_sha256 = serializers.CharField(
|
||||
min_length=64,
|
||||
max_length=64,
|
||||
allow_null=False,
|
||||
allow_blank=False,
|
||||
required=True,
|
||||
help_text="SHA256 of user secret")
|
||||
public_key = serializers.CharField(max_length=2000,
|
||||
allow_null=False,
|
||||
allow_blank=False,
|
||||
required=True,
|
||||
help_text="Armored ASCII PGP public key block")
|
||||
encrypted_private_key = serializers.CharField(max_length=2000,
|
||||
allow_null=False,
|
||||
allow_blank=False,
|
||||
required=True,
|
||||
help_text="Armored ASCII PGP encrypted private key block")
|
||||
min_length=64,
|
||||
max_length=64,
|
||||
allow_null=False,
|
||||
allow_blank=False,
|
||||
required=True,
|
||||
help_text="SHA256 of user secret",
|
||||
)
|
||||
public_key = serializers.CharField(
|
||||
max_length=2000,
|
||||
allow_null=False,
|
||||
allow_blank=False,
|
||||
required=True,
|
||||
help_text="Armored ASCII PGP public key block",
|
||||
)
|
||||
encrypted_private_key = serializers.CharField(
|
||||
max_length=2000,
|
||||
allow_null=False,
|
||||
allow_blank=False,
|
||||
required=True,
|
||||
help_text="Armored ASCII PGP encrypted private key block",
|
||||
)
|
||||
|
||||
# Optional fields
|
||||
ref_code = serializers.CharField(max_length=30,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
required=False,
|
||||
default=None,
|
||||
help_text="Referal code")
|
||||
counts = serializers.ListField(child=serializers.IntegerField(),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None,
|
||||
help_text="Counts of the unique characters in the token")
|
||||
length = serializers.IntegerField(allow_null=True,
|
||||
default=None,
|
||||
required=False,
|
||||
min_value=1,
|
||||
help_text="Length of the token")
|
||||
unique_values = serializers.IntegerField(allow_null=True,
|
||||
default=None,
|
||||
required=False,
|
||||
min_value=1,
|
||||
help_text="Number of unique values in the token")
|
||||
ref_code = serializers.CharField(
|
||||
max_length=30,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
required=False,
|
||||
default=None,
|
||||
help_text="Referal code",
|
||||
)
|
||||
counts = serializers.ListField(
|
||||
child=serializers.IntegerField(),
|
||||
allow_null=True,
|
||||
required=False,
|
||||
default=None,
|
||||
help_text="Counts of the unique characters in the token",
|
||||
)
|
||||
length = serializers.IntegerField(
|
||||
allow_null=True,
|
||||
default=None,
|
||||
required=False,
|
||||
min_value=1,
|
||||
help_text="Length of the token",
|
||||
)
|
||||
unique_values = serializers.IntegerField(
|
||||
allow_null=True,
|
||||
default=None,
|
||||
required=False,
|
||||
min_value=1,
|
||||
help_text="Number of unique values in the token",
|
||||
)
|
||||
|
||||
|
||||
class ClaimRewardSerializer(serializers.Serializer):
|
||||
invoice = serializers.CharField(max_length=2000,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
default=None,
|
||||
help_text="A valid LN invoice with the reward amount to withdraw")
|
||||
invoice = serializers.CharField(
|
||||
max_length=2000,
|
||||
allow_null=True,
|
||||
allow_blank=True,
|
||||
default=None,
|
||||
help_text="A valid LN invoice with the reward amount to withdraw",
|
||||
)
|
||||
|
||||
|
||||
class PriceSerializer(serializers.Serializer):
|
||||
pass
|
||||
|
||||
class TickSerializer(serializers.ModelSerializer):
|
||||
|
||||
class TickSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = MarketTick
|
||||
fields = (
|
||||
@ -599,5 +607,6 @@ class TickSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
depth = 1
|
||||
|
||||
|
||||
class StealthSerializer(serializers.Serializer):
|
||||
wantsStealth = serializers.BooleanField()
|
||||
|
110
api/tasks.py
110
api/tasks.py
@ -1,5 +1,6 @@
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task(name="users_cleansing")
|
||||
def users_cleansing():
|
||||
"""
|
||||
@ -21,7 +22,11 @@ def users_cleansing():
|
||||
for user in queryset:
|
||||
# Try an except, due to unknown cause for users lacking profiles.
|
||||
try:
|
||||
if user.profile.pending_rewards > 0 or user.profile.earned_rewards > 0 or user.profile.claimed_rewards > 0:
|
||||
if (
|
||||
user.profile.pending_rewards > 0
|
||||
or user.profile.earned_rewards > 0
|
||||
or user.profile.claimed_rewards > 0
|
||||
):
|
||||
continue
|
||||
if not user.profile.total_contracts == 0:
|
||||
continue
|
||||
@ -38,6 +43,7 @@ def users_cleansing():
|
||||
}
|
||||
return results
|
||||
|
||||
|
||||
@shared_task(name="give_rewards")
|
||||
def give_rewards():
|
||||
"""
|
||||
@ -57,10 +63,14 @@ def give_rewards():
|
||||
profile.pending_rewards = 0
|
||||
profile.save()
|
||||
|
||||
results[profile.user.username] = {'given_reward':given_reward,'earned_rewards':profile.earned_rewards}
|
||||
results[profile.user.username] = {
|
||||
"given_reward": given_reward,
|
||||
"earned_rewards": profile.earned_rewards,
|
||||
}
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@shared_task(name="follow_send_payment")
|
||||
def follow_send_payment(hash):
|
||||
"""Sends sats to buyer, continuous update"""
|
||||
@ -75,10 +85,10 @@ def follow_send_payment(hash):
|
||||
lnpayment = LNPayment.objects.get(payment_hash=hash)
|
||||
fee_limit_sat = int(
|
||||
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")),
|
||||
)) # 1000 ppm or 10 sats
|
||||
)
|
||||
) # 1000 ppm or 10 sats
|
||||
timeout_seconds = int(config("PAYOUT_TIMEOUT_SECONDS"))
|
||||
|
||||
request = LNNode.routerrpc.SendPaymentRequest(
|
||||
@ -89,15 +99,13 @@ def follow_send_payment(hash):
|
||||
|
||||
order = lnpayment.order_paid_LN
|
||||
try:
|
||||
for response in LNNode.routerstub.SendPaymentV2(request,
|
||||
metadata=[
|
||||
("macaroon",
|
||||
MACAROON.hex())
|
||||
]):
|
||||
|
||||
for response in LNNode.routerstub.SendPaymentV2(
|
||||
request, metadata=[("macaroon", MACAROON.hex())]
|
||||
):
|
||||
|
||||
lnpayment.in_flight = True
|
||||
lnpayment.save()
|
||||
|
||||
|
||||
if response.status == 0: # Status 0 'UNKNOWN'
|
||||
# Not sure when this status happens
|
||||
lnpayment.in_flight = False
|
||||
@ -125,18 +133,20 @@ def follow_send_payment(hash):
|
||||
|
||||
order.status = Order.Status.FAI
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.FAI))
|
||||
seconds=order.t_to_expire(Order.Status.FAI)
|
||||
)
|
||||
order.save()
|
||||
context = {
|
||||
"routing_failed":
|
||||
LNNode.payment_failure_context[response.failure_reason],
|
||||
"IN_FLIGHT":False,
|
||||
"routing_failed": LNNode.payment_failure_context[
|
||||
response.failure_reason
|
||||
],
|
||||
"IN_FLIGHT": False,
|
||||
}
|
||||
print(context)
|
||||
|
||||
# If failed due to not route, reset mission control. (This won't scale well, just a temporary fix)
|
||||
# ResetMC deactivate temporary for tests
|
||||
#if response.failure_reason==2:
|
||||
# if response.failure_reason==2:
|
||||
# LNNode.resetmc()
|
||||
|
||||
return False, context
|
||||
@ -144,12 +154,13 @@ def follow_send_payment(hash):
|
||||
if response.status == 2: # Status 2 'SUCCEEDED'
|
||||
print("SUCCEEDED")
|
||||
lnpayment.status = LNPayment.Status.SUCCED
|
||||
lnpayment.fee = float(response.fee_msat)/1000
|
||||
lnpayment.fee = float(response.fee_msat) / 1000
|
||||
lnpayment.preimage = response.payment_preimage
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.SUC
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.SUC))
|
||||
seconds=order.t_to_expire(Order.Status.SUC)
|
||||
)
|
||||
order.save()
|
||||
return True, None
|
||||
|
||||
@ -162,17 +173,19 @@ def follow_send_payment(hash):
|
||||
lnpayment.save()
|
||||
order.status = Order.Status.FAI
|
||||
order.expires_at = timezone.now() + timedelta(
|
||||
seconds=order.t_to_expire(Order.Status.FAI))
|
||||
seconds=order.t_to_expire(Order.Status.FAI)
|
||||
)
|
||||
order.save()
|
||||
context = {"routing_failed": "The payout invoice has expired"}
|
||||
return False, context
|
||||
|
||||
|
||||
@shared_task(name="payments_cleansing")
|
||||
def payments_cleansing():
|
||||
"""
|
||||
Deletes cancelled payments (hodl invoices never locked) that
|
||||
Deletes cancelled payments (hodl invoices never locked) that
|
||||
belong to orders expired more than 3 days ago.
|
||||
Deletes 'cancelled' or 'create' onchain_payments
|
||||
Deletes 'cancelled' or 'create' onchain_payments
|
||||
"""
|
||||
|
||||
from django.db.models import Q
|
||||
@ -185,10 +198,11 @@ def payments_cleansing():
|
||||
# Usually expiry is 1 day for every finished order. So ~4 days until
|
||||
# a never locked hodl invoice is removed.
|
||||
finished_time = timezone.now() - timedelta(days=3)
|
||||
queryset = LNPayment.objects.filter(Q(status=LNPayment.Status.CANCEL),
|
||||
Q(order_made__expires_at__lt=finished_time)|
|
||||
Q(order_taken__expires_at__lt=finished_time))
|
||||
|
||||
queryset = LNPayment.objects.filter(
|
||||
Q(status=LNPayment.Status.CANCEL),
|
||||
Q(order_made__expires_at__lt=finished_time)
|
||||
| Q(order_taken__expires_at__lt=finished_time),
|
||||
)
|
||||
|
||||
# And do not have an active trade, any past contract or any reward.
|
||||
deleted_lnpayments = []
|
||||
@ -200,10 +214,12 @@ def payments_cleansing():
|
||||
deleted_lnpayments.append(name)
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
# same for onchain payments
|
||||
queryset = OnchainPayment.objects.filter(Q(status__in=[OnchainPayment.Status.CANCE, OnchainPayment.Status.CREAT]),
|
||||
Q(order_paid_TX__expires_at__lt=finished_time)|Q(order_paid_TX__isnull=True))
|
||||
queryset = OnchainPayment.objects.filter(
|
||||
Q(status__in=[OnchainPayment.Status.CANCE, OnchainPayment.Status.CREAT]),
|
||||
Q(order_paid_TX__expires_at__lt=finished_time) | Q(order_paid_TX__isnull=True),
|
||||
)
|
||||
|
||||
# And do not have an active trade, any past contract or any reward.
|
||||
deleted_onchainpayments = []
|
||||
@ -224,6 +240,7 @@ def payments_cleansing():
|
||||
}
|
||||
return results
|
||||
|
||||
|
||||
@shared_task(name="cache_external_market_prices", ignore_result=True)
|
||||
def cache_market():
|
||||
|
||||
@ -236,7 +253,9 @@ def cache_market():
|
||||
exchange_rates = get_exchange_rates(currency_codes)
|
||||
|
||||
results = {}
|
||||
for i in range(len(Currency.currency_dict.values())): # currencies are indexed starting at 1 (USD)
|
||||
for i in range(
|
||||
len(Currency.currency_dict.values())
|
||||
): # currencies are indexed starting at 1 (USD)
|
||||
|
||||
rate = exchange_rates[i]
|
||||
results[i] = {currency_codes[i], rate}
|
||||
@ -259,45 +278,48 @@ def cache_market():
|
||||
|
||||
return results
|
||||
|
||||
|
||||
@shared_task(name="send_message", ignore_result=True)
|
||||
def send_message(order_id, message):
|
||||
|
||||
from api.models import Order
|
||||
|
||||
order = Order.objects.get(id=order_id)
|
||||
if not order.maker.profile.telegram_enabled:
|
||||
return
|
||||
|
||||
from api.messages import Telegram
|
||||
|
||||
telegram = Telegram()
|
||||
|
||||
if message == 'welcome':
|
||||
if message == "welcome":
|
||||
telegram.welcome(order)
|
||||
|
||||
elif message == 'order_expired_untaken':
|
||||
|
||||
elif message == "order_expired_untaken":
|
||||
telegram.order_expired_untaken(order)
|
||||
|
||||
elif message == 'trade_successful':
|
||||
elif message == "trade_successful":
|
||||
telegram.trade_successful(order)
|
||||
|
||||
elif message == 'public_order_cancelled':
|
||||
elif message == "public_order_cancelled":
|
||||
telegram.public_order_cancelled(order)
|
||||
|
||||
elif message == 'taker_expired_b4bond':
|
||||
elif message == "taker_expired_b4bond":
|
||||
telegram.taker_expired_b4bond(order)
|
||||
|
||||
elif message == 'order_published':
|
||||
elif message == "order_published":
|
||||
telegram.order_published(order)
|
||||
|
||||
elif message == 'order_taken_confirmed':
|
||||
|
||||
elif message == "order_taken_confirmed":
|
||||
telegram.order_taken_confirmed(order)
|
||||
|
||||
elif message == 'fiat_exchange_starts':
|
||||
|
||||
elif message == "fiat_exchange_starts":
|
||||
telegram.fiat_exchange_starts(order)
|
||||
|
||||
elif message == 'dispute_opened':
|
||||
elif message == "dispute_opened":
|
||||
telegram.dispute_opened(order)
|
||||
|
||||
elif message == 'collaborative_cancelled':
|
||||
elif message == "collaborative_cancelled":
|
||||
telegram.collaborative_cancelled(order)
|
||||
|
||||
return
|
||||
return
|
||||
|
27
api/urls.py
27
api/urls.py
@ -1,16 +1,27 @@
|
||||
from django.urls import path
|
||||
from .views import MakerView, OrderView, UserView, BookView, InfoView, RewardView, PriceView, LimitView, HistoricalView, TickView, StealthView
|
||||
from .views import (
|
||||
MakerView,
|
||||
OrderView,
|
||||
UserView,
|
||||
BookView,
|
||||
InfoView,
|
||||
RewardView,
|
||||
PriceView,
|
||||
LimitView,
|
||||
HistoricalView,
|
||||
TickView,
|
||||
StealthView,
|
||||
)
|
||||
from drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
|
||||
from chat.views import ChatView
|
||||
|
||||
urlpatterns = [
|
||||
path('schema/', SpectacularAPIView.as_view(), name='schema'),
|
||||
path('', SpectacularRedocView.as_view(url_name='schema'), name='redoc'),
|
||||
path("schema/", SpectacularAPIView.as_view(), name="schema"),
|
||||
path("", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
|
||||
path("make/", MakerView.as_view()),
|
||||
path("order/",OrderView.as_view({
|
||||
"get": "get",
|
||||
"post": "take_update_confirm_dispute_cancel"
|
||||
}),
|
||||
path(
|
||||
"order/",
|
||||
OrderView.as_view({"get": "get", "post": "take_update_confirm_dispute_cancel"}),
|
||||
),
|
||||
path("user/", UserView.as_view()),
|
||||
path("book/", BookView.as_view()),
|
||||
@ -21,5 +32,5 @@ urlpatterns = [
|
||||
path("historical/", HistoricalView.as_view()),
|
||||
path("ticks/", TickView.as_view()),
|
||||
path("stealth/", StealthView.as_view()),
|
||||
path("chat/", ChatView.as_view({"get": "get","post":"post"})),
|
||||
path("chat/", ChatView.as_view({"get": "get", "post": "post"})),
|
||||
]
|
||||
|
88
api/utils.py
88
api/utils.py
@ -7,17 +7,20 @@ from decouple import config
|
||||
|
||||
from api.models import Order
|
||||
|
||||
logger = logging.getLogger('api.utils')
|
||||
logger = logging.getLogger("api.utils")
|
||||
|
||||
TOR_PROXY = config("TOR_PROXY", default="127.0.0.1:9050")
|
||||
USE_TOR = config("USE_TOR", cast=bool, default=True)
|
||||
|
||||
TOR_PROXY = config('TOR_PROXY', default='127.0.0.1:9050')
|
||||
USE_TOR = config('USE_TOR', cast=bool, default=True)
|
||||
|
||||
def get_session():
|
||||
session = requests.session()
|
||||
# Tor uses the 9050 port as the default socks port
|
||||
if USE_TOR:
|
||||
session.proxies = {'http': 'socks5://' + TOR_PROXY,
|
||||
'https': 'socks5://' + TOR_PROXY}
|
||||
session.proxies = {
|
||||
"http": "socks5://" + TOR_PROXY,
|
||||
"https": "socks5://" + TOR_PROXY,
|
||||
}
|
||||
return session
|
||||
|
||||
|
||||
@ -29,22 +32,19 @@ def bitcoind_rpc(method, params=None):
|
||||
:return:
|
||||
"""
|
||||
|
||||
BITCOIND_RPCURL = config('BITCOIND_RPCURL')
|
||||
BITCOIND_RPCUSER = config('BITCOIND_RPCUSER')
|
||||
BITCOIND_RPCPASSWORD = config('BITCOIND_RPCPASSWORD')
|
||||
BITCOIND_RPCURL = config("BITCOIND_RPCURL")
|
||||
BITCOIND_RPCUSER = config("BITCOIND_RPCUSER")
|
||||
BITCOIND_RPCPASSWORD = config("BITCOIND_RPCPASSWORD")
|
||||
|
||||
if params is None:
|
||||
params = []
|
||||
|
||||
payload = json.dumps(
|
||||
{
|
||||
"jsonrpc": "2.0",
|
||||
"id": "robosats",
|
||||
"method": method,
|
||||
"params": params
|
||||
}
|
||||
{"jsonrpc": "2.0", "id": "robosats", "method": method, "params": params}
|
||||
)
|
||||
return requests.post(BITCOIND_RPCURL, auth=(BITCOIND_RPCUSER, BITCOIND_RPCPASSWORD), data=payload).json()['result']
|
||||
return requests.post(
|
||||
BITCOIND_RPCURL, auth=(BITCOIND_RPCUSER, BITCOIND_RPCPASSWORD), data=payload
|
||||
).json()["result"]
|
||||
|
||||
|
||||
def validate_onchain_address(address):
|
||||
@ -53,17 +53,21 @@ def validate_onchain_address(address):
|
||||
"""
|
||||
|
||||
try:
|
||||
validation = bitcoind_rpc('validateaddress', [address])
|
||||
if not validation['isvalid']:
|
||||
validation = bitcoind_rpc("validateaddress", [address])
|
||||
if not validation["isvalid"]:
|
||||
return False, {"bad_address": "Invalid address"}
|
||||
except Exception as e:
|
||||
logger.error(e)
|
||||
return False, {"bad_address": 'Unable to validate address, check bitcoind backend'}
|
||||
return False, {
|
||||
"bad_address": "Unable to validate address, check bitcoind backend"
|
||||
}
|
||||
|
||||
return True, None
|
||||
|
||||
|
||||
market_cache = {}
|
||||
|
||||
|
||||
@ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds
|
||||
def get_exchange_rates(currencies):
|
||||
"""
|
||||
@ -74,8 +78,7 @@ def get_exchange_rates(currencies):
|
||||
|
||||
session = get_session()
|
||||
|
||||
APIS = config("MARKET_PRICE_APIS",
|
||||
cast=lambda v: [s.strip() for s in v.split(",")])
|
||||
APIS = config("MARKET_PRICE_APIS", cast=lambda v: [s.strip() for s in v.split(",")])
|
||||
|
||||
api_rates = []
|
||||
for api_url in APIS:
|
||||
@ -86,7 +89,8 @@ def get_exchange_rates(currencies):
|
||||
for currency in currencies:
|
||||
try: # If a currency is missing place a None
|
||||
blockchain_rates.append(
|
||||
float(blockchain_prices[currency]["last"]))
|
||||
float(blockchain_prices[currency]["last"])
|
||||
)
|
||||
except:
|
||||
blockchain_rates.append(np.nan)
|
||||
api_rates.append(blockchain_rates)
|
||||
@ -96,8 +100,7 @@ def get_exchange_rates(currencies):
|
||||
yadio_rates = []
|
||||
for currency in currencies:
|
||||
try:
|
||||
yadio_rates.append(float(
|
||||
yadio_prices["BTC"][currency]))
|
||||
yadio_rates.append(float(yadio_prices["BTC"][currency]))
|
||||
except:
|
||||
yadio_rates.append(np.nan)
|
||||
api_rates.append(yadio_rates)
|
||||
@ -133,6 +136,8 @@ def get_lnd_version():
|
||||
|
||||
|
||||
robosats_commit_cache = {}
|
||||
|
||||
|
||||
@ring.dict(robosats_commit_cache, expire=3600)
|
||||
def get_robosats_commit():
|
||||
|
||||
@ -140,13 +145,16 @@ def get_robosats_commit():
|
||||
commit_hash = commit.read()
|
||||
|
||||
# .git folder is included in .dockerignore. But automatic build will drop in a commit_sha.txt file on root
|
||||
if commit_hash == None or commit_hash =="":
|
||||
if commit_hash == None or commit_hash == "":
|
||||
with open("commit_sha.txt") as f:
|
||||
commit_hash = f.read()
|
||||
|
||||
return commit_hash
|
||||
|
||||
|
||||
robosats_version_cache = {}
|
||||
|
||||
|
||||
@ring.dict(robosats_commit_cache, expire=99999)
|
||||
def get_robosats_version():
|
||||
|
||||
@ -156,12 +164,16 @@ def get_robosats_version():
|
||||
print(version_dict)
|
||||
return version_dict
|
||||
|
||||
|
||||
premium_percentile = {}
|
||||
|
||||
|
||||
@ring.dict(premium_percentile, expire=300)
|
||||
def compute_premium_percentile(order):
|
||||
|
||||
queryset = Order.objects.filter(
|
||||
currency=order.currency, status=Order.Status.PUB, type=order.type).exclude(id=order.id)
|
||||
currency=order.currency, status=Order.Status.PUB, type=order.type
|
||||
).exclude(id=order.id)
|
||||
|
||||
print(len(queryset))
|
||||
if len(queryset) <= 1:
|
||||
@ -171,15 +183,18 @@ def compute_premium_percentile(order):
|
||||
order_rate = float(order.last_satoshis) / float(amount)
|
||||
rates = []
|
||||
for similar_order in queryset:
|
||||
similar_order_amount = similar_order.amount if not similar_order.has_range else similar_order.max_amount
|
||||
rates.append(
|
||||
float(similar_order.last_satoshis) / float(similar_order_amount))
|
||||
similar_order_amount = (
|
||||
similar_order.amount
|
||||
if not similar_order.has_range
|
||||
else similar_order.max_amount
|
||||
)
|
||||
rates.append(float(similar_order.last_satoshis) / float(similar_order_amount))
|
||||
|
||||
rates = np.array(rates)
|
||||
return round(np.sum(rates < order_rate) / len(rates), 2)
|
||||
|
||||
|
||||
def weighted_median(values, sample_weight=None, quantiles= 0.5, values_sorted=False):
|
||||
def weighted_median(values, sample_weight=None, quantiles=0.5, values_sorted=False):
|
||||
"""Very close to numpy.percentile, but it supports weights.
|
||||
NOTE: quantiles should be in [0, 1]!
|
||||
:param values: numpy.array with data
|
||||
@ -194,8 +209,9 @@ def weighted_median(values, sample_weight=None, quantiles= 0.5, values_sorted=Fa
|
||||
if sample_weight is None:
|
||||
sample_weight = np.ones(len(values))
|
||||
sample_weight = np.array(sample_weight)
|
||||
assert np.all(quantiles >= 0) and np.all(quantiles <= 1), \
|
||||
'quantiles should be in [0, 1]'
|
||||
assert np.all(quantiles >= 0) and np.all(
|
||||
quantiles <= 1
|
||||
), "quantiles should be in [0, 1]"
|
||||
|
||||
if not values_sorted:
|
||||
sorter = np.argsort(values)
|
||||
@ -208,6 +224,7 @@ def weighted_median(values, sample_weight=None, quantiles= 0.5, values_sorted=Fa
|
||||
|
||||
return np.interp(quantiles, weighted_quantiles, values)
|
||||
|
||||
|
||||
def compute_avg_premium(queryset):
|
||||
premiums = []
|
||||
volumes = []
|
||||
@ -221,11 +238,10 @@ def compute_avg_premium(queryset):
|
||||
total_volume = sum(volumes)
|
||||
|
||||
# weighted_median_premium is the weighted median of the premiums by volume
|
||||
if len(premiums) > 0 and len(volumes)>0:
|
||||
weighted_median_premium = weighted_median(values=premiums,
|
||||
sample_weight=volumes,
|
||||
quantiles=0.5,
|
||||
values_sorted=False)
|
||||
if len(premiums) > 0 and len(volumes) > 0:
|
||||
weighted_median_premium = weighted_median(
|
||||
values=premiums, sample_weight=volumes, quantiles=0.5, values_sorted=False
|
||||
)
|
||||
else:
|
||||
weighted_median_premium = 0.0
|
||||
return weighted_median_premium, total_volume
|
||||
|
429
api/views.py
429
api/views.py
@ -7,16 +7,45 @@ from rest_framework.response import Response
|
||||
|
||||
from django.contrib.auth import authenticate, login, logout
|
||||
from django.contrib.auth.models import User
|
||||
from api.oas_schemas import BookViewSchema, HistoricalViewSchema, InfoViewSchema, LimitViewSchema, MakerViewSchema, OrderViewSchema, PriceViewSchema, RewardViewSchema, StealthViewSchema, TickViewSchema, UserViewSchema
|
||||
from api.oas_schemas import (
|
||||
BookViewSchema,
|
||||
HistoricalViewSchema,
|
||||
InfoViewSchema,
|
||||
LimitViewSchema,
|
||||
MakerViewSchema,
|
||||
OrderViewSchema,
|
||||
PriceViewSchema,
|
||||
RewardViewSchema,
|
||||
StealthViewSchema,
|
||||
TickViewSchema,
|
||||
UserViewSchema,
|
||||
)
|
||||
|
||||
from chat.views import ChatView
|
||||
from api.serializers import InfoSerializer, ListOrderSerializer, MakeOrderSerializer, OrderPublicSerializer, UpdateOrderSerializer, ClaimRewardSerializer, PriceSerializer, UserGenSerializer, TickSerializer, StealthSerializer
|
||||
from api.serializers import (
|
||||
InfoSerializer,
|
||||
ListOrderSerializer,
|
||||
MakeOrderSerializer,
|
||||
OrderPublicSerializer,
|
||||
UpdateOrderSerializer,
|
||||
ClaimRewardSerializer,
|
||||
PriceSerializer,
|
||||
UserGenSerializer,
|
||||
TickSerializer,
|
||||
StealthSerializer,
|
||||
)
|
||||
from api.models import LNPayment, MarketTick, OnchainPayment, Order, Currency, Profile
|
||||
from control.models import AccountingDay, BalanceLog
|
||||
from api.logics import Logics
|
||||
from api.messages import Telegram
|
||||
from secrets import token_urlsafe
|
||||
from api.utils import get_lnd_version, get_robosats_commit, get_robosats_version, compute_premium_percentile, compute_avg_premium
|
||||
from api.utils import (
|
||||
get_lnd_version,
|
||||
get_robosats_commit,
|
||||
get_robosats_version,
|
||||
compute_premium_percentile,
|
||||
compute_avg_premium,
|
||||
)
|
||||
|
||||
from .nick_generator.nick_generator import NickGenerator
|
||||
from robohash import Robohash
|
||||
@ -31,7 +60,7 @@ from decouple import config
|
||||
|
||||
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
|
||||
RETRY_TIME = int(config("RETRY_TIME"))
|
||||
PUBLIC_DURATION = 60*60*int(config("DEFAULT_PUBLIC_ORDER_DURATION"))-1
|
||||
PUBLIC_DURATION = 60 * 60 * int(config("DEFAULT_PUBLIC_ORDER_DURATION")) - 1
|
||||
ESCROW_DURATION = 60 * int(config("INVOICE_AND_ESCROW_DURATION"))
|
||||
BOND_SIZE = int(config("DEFAULT_BOND_SIZE"))
|
||||
|
||||
@ -50,10 +79,7 @@ class MakerView(CreateAPIView):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Woops! It seems you do not have a robot avatar"
|
||||
},
|
||||
{"bad_request": "Woops! It seems you do not have a robot avatar"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -61,11 +87,12 @@ class MakerView(CreateAPIView):
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# In case it gets overwhelming. Limit the number of public orders.
|
||||
if Order.objects.filter(status=Order.Status.PUB).count() >= int(config("MAX_PUBLIC_ORDERS")):
|
||||
if Order.objects.filter(status=Order.Status.PUB).count() >= int(
|
||||
config("MAX_PUBLIC_ORDERS")
|
||||
):
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Woah! RoboSats' book is at full capacity! Try again later"
|
||||
"bad_request": "Woah! RoboSats' book is at full capacity! Try again later"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -90,11 +117,16 @@ class MakerView(CreateAPIView):
|
||||
bondless_taker = serializer.data.get("bondless_taker")
|
||||
|
||||
# Optional params
|
||||
if public_duration == None: public_duration = PUBLIC_DURATION
|
||||
if escrow_duration == None: escrow_duration = ESCROW_DURATION
|
||||
if bond_size == None: bond_size = BOND_SIZE
|
||||
if bondless_taker == None: bondless_taker = False
|
||||
if has_range == None: has_range = False
|
||||
if public_duration == None:
|
||||
public_duration = PUBLIC_DURATION
|
||||
if escrow_duration == None:
|
||||
escrow_duration = ESCROW_DURATION
|
||||
if bond_size == None:
|
||||
bond_size = BOND_SIZE
|
||||
if bondless_taker == None:
|
||||
bondless_taker = False
|
||||
if has_range == None:
|
||||
has_range = False
|
||||
|
||||
# TODO add a check - if `is_explicit` is true then `satoshis` need to be specified
|
||||
|
||||
@ -109,21 +141,16 @@ class MakerView(CreateAPIView):
|
||||
if has_range and (min_amount == None or max_amount == None):
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"You must specify min_amount and max_amount for a range order"
|
||||
"bad_request": "You must specify min_amount and max_amount for a range order"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
elif not has_range and amount == None:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"You must specify an order amount"
|
||||
},
|
||||
{"bad_request": "You must specify an order amount"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
||||
# Creates a new order
|
||||
order = Order(
|
||||
type=type,
|
||||
@ -136,8 +163,7 @@ class MakerView(CreateAPIView):
|
||||
premium=premium,
|
||||
satoshis=satoshis,
|
||||
is_explicit=is_explicit,
|
||||
expires_at=timezone.now() + timedelta(
|
||||
seconds=EXP_MAKER_BOND_INVOICE),
|
||||
expires_at=timezone.now() + timedelta(seconds=EXP_MAKER_BOND_INVOICE),
|
||||
maker=request.user,
|
||||
public_duration=public_duration,
|
||||
escrow_duration=escrow_duration,
|
||||
@ -152,8 +178,7 @@ class MakerView(CreateAPIView):
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
order.save()
|
||||
return Response(ListOrderSerializer(order).data,
|
||||
status=status.HTTP_201_CREATED)
|
||||
return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
class OrderView(viewsets.ViewSet):
|
||||
@ -170,8 +195,7 @@ class OrderView(viewsets.ViewSet):
|
||||
if not request.user.is_authenticated:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"You must have a robot avatar to see the order details"
|
||||
"bad_request": "You must have a robot avatar to see the order details"
|
||||
},
|
||||
status=status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -186,8 +210,9 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
# check if exactly one order is found in the db
|
||||
if len(order) != 1:
|
||||
return Response({"bad_request": "Invalid Order Id"},
|
||||
status.HTTP_404_NOT_FOUND)
|
||||
return Response(
|
||||
{"bad_request": "Invalid Order Id"}, status.HTTP_404_NOT_FOUND
|
||||
)
|
||||
|
||||
# This is our order.
|
||||
order = order[0]
|
||||
@ -200,10 +225,7 @@ class OrderView(viewsets.ViewSet):
|
||||
)
|
||||
if order.status == Order.Status.CCA:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"This order has been cancelled collaborativelly"
|
||||
},
|
||||
{"bad_request": "This order has been cancelled collaborativelly"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -239,11 +261,9 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
# Add activity status of participants based on last_seen
|
||||
if order.taker_last_seen != None:
|
||||
data["taker_status"] = Logics.user_activity_status(
|
||||
order.taker_last_seen)
|
||||
data["taker_status"] = Logics.user_activity_status(order.taker_last_seen)
|
||||
if order.maker_last_seen != None:
|
||||
data["maker_status"] = Logics.user_activity_status(
|
||||
order.maker_last_seen)
|
||||
data["maker_status"] = Logics.user_activity_status(order.maker_last_seen)
|
||||
|
||||
# 3.b) Non participants can view details (but only if PUB)
|
||||
if not data["is_participant"] and order.status == Order.Status.PUB:
|
||||
@ -253,16 +273,21 @@ class OrderView(viewsets.ViewSet):
|
||||
if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
|
||||
data["price_now"], data["premium_now"] = Logics.price_and_premium_now(order)
|
||||
|
||||
# 4. a) If maker and Public/Paused, add premium percentile
|
||||
# 4. a) If maker and Public/Paused, add premium percentile
|
||||
# num similar orders, and maker information to enable telegram notifications.
|
||||
if data["is_maker"] and order.status in [Order.Status.PUB, Order.Status.PAU]:
|
||||
if data["is_maker"] and order.status in [
|
||||
Order.Status.PUB,
|
||||
Order.Status.PAU,
|
||||
]:
|
||||
data["premium_percentile"] = compute_premium_percentile(order)
|
||||
data["num_similar_orders"] = len(
|
||||
Order.objects.filter(currency=order.currency,
|
||||
status=Order.Status.PUB))
|
||||
Order.objects.filter(
|
||||
currency=order.currency, status=Order.Status.PUB
|
||||
)
|
||||
)
|
||||
# Adds/generate telegram token and whether it is enabled
|
||||
# Deprecated
|
||||
data = {**data,**Telegram.get_context(request.user)}
|
||||
data = {**data, **Telegram.get_context(request.user)}
|
||||
|
||||
# For participants add positions, nicks and status as a message and hold invoices status
|
||||
data["is_buyer"] = Logics.is_buyer(order, request.user)
|
||||
@ -294,16 +319,21 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
# If both bonds are locked, participants can see the final trade amount in sats.
|
||||
if order.taker_bond:
|
||||
if (order.maker_bond.status == order.taker_bond.status ==
|
||||
LNPayment.Status.LOCKED):
|
||||
if (
|
||||
order.maker_bond.status
|
||||
== order.taker_bond.status
|
||||
== LNPayment.Status.LOCKED
|
||||
):
|
||||
# Seller sees the amount he sends
|
||||
if data["is_seller"]:
|
||||
data["trade_satoshis"] = Logics.escrow_amount(
|
||||
order, request.user)[1]["escrow_amount"]
|
||||
data["trade_satoshis"] = Logics.escrow_amount(order, request.user)[
|
||||
1
|
||||
]["escrow_amount"]
|
||||
# Buyer sees the amount he receives
|
||||
elif data["is_buyer"]:
|
||||
data["trade_satoshis"] = Logics.payout_amount(
|
||||
order, request.user)[1]["invoice_amount"]
|
||||
data["trade_satoshis"] = Logics.payout_amount(order, request.user)[
|
||||
1
|
||||
]["invoice_amount"]
|
||||
|
||||
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice.
|
||||
if order.status == Order.Status.WFB and data["is_maker"]:
|
||||
@ -322,25 +352,32 @@ class OrderView(viewsets.ViewSet):
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 7 a. ) If seller and status is 'WF2' or 'WFE'
|
||||
elif data["is_seller"] and (order.status == Order.Status.WF2
|
||||
or order.status == Order.Status.WFE):
|
||||
elif data["is_seller"] and (
|
||||
order.status == Order.Status.WF2 or order.status == Order.Status.WFE
|
||||
):
|
||||
# If the two bonds are locked, reply with an ESCROW hold invoice.
|
||||
if (order.maker_bond.status == order.taker_bond.status ==
|
||||
LNPayment.Status.LOCKED):
|
||||
valid, context = Logics.gen_escrow_hold_invoice(
|
||||
order, request.user)
|
||||
if (
|
||||
order.maker_bond.status
|
||||
== order.taker_bond.status
|
||||
== LNPayment.Status.LOCKED
|
||||
):
|
||||
valid, context = Logics.gen_escrow_hold_invoice(order, request.user)
|
||||
if valid:
|
||||
data = {**data, **context}
|
||||
else:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 7.b) If user is Buyer and status is 'WF2' or 'WFI'
|
||||
elif data["is_buyer"] and (order.status == Order.Status.WF2
|
||||
or order.status == Order.Status.WFI):
|
||||
elif data["is_buyer"] and (
|
||||
order.status == Order.Status.WF2 or order.status == Order.Status.WFI
|
||||
):
|
||||
|
||||
# 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 ==
|
||||
LNPayment.Status.LOCKED):
|
||||
if (
|
||||
order.maker_bond.status
|
||||
== order.taker_bond.status
|
||||
== LNPayment.Status.LOCKED
|
||||
):
|
||||
valid, context = Logics.payout_amount(order, request.user)
|
||||
if valid:
|
||||
data = {**data, **context}
|
||||
@ -348,23 +385,27 @@ class OrderView(viewsets.ViewSet):
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED
|
||||
elif order.status in [
|
||||
Order.Status.WFI, Order.Status.CHA, Order.Status.FSE
|
||||
]:
|
||||
elif order.status in [Order.Status.WFI, Order.Status.CHA, Order.Status.FSE]:
|
||||
# If all bonds are locked.
|
||||
if (order.maker_bond.status == order.taker_bond.status ==
|
||||
order.trade_escrow.status == LNPayment.Status.LOCKED):
|
||||
if (
|
||||
order.maker_bond.status
|
||||
== order.taker_bond.status
|
||||
== order.trade_escrow.status
|
||||
== LNPayment.Status.LOCKED
|
||||
):
|
||||
# add whether a collaborative cancel is pending or has been asked
|
||||
if (data["is_maker"] and order.taker_asked_cancel) or (
|
||||
data["is_taker"] and order.maker_asked_cancel):
|
||||
data["is_taker"] and order.maker_asked_cancel
|
||||
):
|
||||
data["pending_cancel"] = True
|
||||
elif (data["is_maker"] and order.maker_asked_cancel) or (
|
||||
data["is_taker"] and order.taker_asked_cancel):
|
||||
data["is_taker"] and order.taker_asked_cancel
|
||||
):
|
||||
data["asked_for_cancel"] = True
|
||||
else:
|
||||
data["asked_for_cancel"] = False
|
||||
|
||||
offset = request.GET.get('offset', None)
|
||||
|
||||
offset = request.GET.get("offset", None)
|
||||
if offset:
|
||||
data["chat"] = ChatView.get(None, request).data
|
||||
|
||||
@ -373,35 +414,47 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
# add whether the dispute statement has been received
|
||||
if data["is_maker"]:
|
||||
data["statement_submitted"] = (order.maker_statement != None
|
||||
and order.maker_statement != "")
|
||||
data["statement_submitted"] = (
|
||||
order.maker_statement != None and order.maker_statement != ""
|
||||
)
|
||||
elif data["is_taker"]:
|
||||
data["statement_submitted"] = (order.taker_statement != None
|
||||
and order.taker_statement != "")
|
||||
data["statement_submitted"] = (
|
||||
order.taker_statement != None and order.taker_statement != ""
|
||||
)
|
||||
|
||||
# 9) If status is 'Failed routing', reply with retry amounts, time of next retry and ask for invoice at third.
|
||||
elif (order.status == Order.Status.FAI
|
||||
and order.payout.receiver == request.user
|
||||
): # might not be the buyer if after a dispute where winner wins
|
||||
elif (
|
||||
order.status == Order.Status.FAI and order.payout.receiver == request.user
|
||||
): # might not be the buyer if after a dispute where winner wins
|
||||
data["retries"] = order.payout.routing_attempts
|
||||
data["next_retry_time"] = order.payout.last_routing_time + timedelta(
|
||||
minutes=RETRY_TIME)
|
||||
minutes=RETRY_TIME
|
||||
)
|
||||
if order.payout.failure_reason:
|
||||
data["failure_reason"] = LNPayment.FailureReason(order.payout.failure_reason).label
|
||||
data["failure_reason"] = LNPayment.FailureReason(
|
||||
order.payout.failure_reason
|
||||
).label
|
||||
|
||||
if order.payout.status == LNPayment.Status.EXPIRE:
|
||||
data["invoice_expired"] = True
|
||||
# Add invoice amount once again if invoice was expired.
|
||||
data["invoice_amount"] = Logics.payout_amount(order,request.user)[1]["invoice_amount"]
|
||||
data["invoice_amount"] = Logics.payout_amount(order, request.user)[1][
|
||||
"invoice_amount"
|
||||
]
|
||||
|
||||
# 10) If status is 'Expired', "Sending", "Finished" or "failed routing", add info for renewal:
|
||||
elif order.status in [Order.Status.EXP, Order.Status.SUC, Order.Status.PAY, Order.Status.FAI]:
|
||||
elif order.status in [
|
||||
Order.Status.EXP,
|
||||
Order.Status.SUC,
|
||||
Order.Status.PAY,
|
||||
Order.Status.FAI,
|
||||
]:
|
||||
data["public_duration"] = order.public_duration
|
||||
data["bond_size"] = order.bond_size
|
||||
data["bondless_taker"] = order.bondless_taker
|
||||
|
||||
# Adds trade summary
|
||||
if order.status in [Order.Status.SUC, Order.Status.PAY, Order.Status.FAI]:
|
||||
if order.status in [Order.Status.SUC, Order.Status.PAY, Order.Status.FAI]:
|
||||
valid, context = Logics.summarize_trade(order, request.user)
|
||||
if valid:
|
||||
data = {**data, **context}
|
||||
@ -414,16 +467,17 @@ class OrderView(viewsets.ViewSet):
|
||||
# If status is 'Succes' add final stats and txid if it is a swap
|
||||
if order.status == Order.Status.SUC:
|
||||
# If buyer and is a swap, add TXID
|
||||
if Logics.is_buyer(order,request.user):
|
||||
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]:
|
||||
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)
|
||||
|
||||
@extend_schema(**OrderViewSchema.take_update_confirm_dispute_cancel)
|
||||
@ -452,8 +506,7 @@ class OrderView(viewsets.ViewSet):
|
||||
# 1) If action is take, it is a taker request!
|
||||
if action == "take":
|
||||
if order.status == Order.Status.PUB:
|
||||
valid, context, _ = Logics.validate_already_maker_or_taker(
|
||||
request.user)
|
||||
valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
|
||||
if not valid:
|
||||
return Response(context, status=status.HTTP_409_CONFLICT)
|
||||
|
||||
@ -487,15 +540,15 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
# 2) If action is 'update invoice'
|
||||
elif action == "update_invoice":
|
||||
valid, context = Logics.update_invoice(order, request.user,
|
||||
invoice)
|
||||
valid, context = Logics.update_invoice(order, request.user, invoice)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
# 2.b) If action is 'update address'
|
||||
elif action == "update_address":
|
||||
valid, context = Logics.update_address(order, request.user,
|
||||
address, mining_fee_rate)
|
||||
valid, context = Logics.update_address(
|
||||
order, request.user, address, mining_fee_rate
|
||||
)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -518,15 +571,13 @@ class OrderView(viewsets.ViewSet):
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
elif action == "submit_statement":
|
||||
valid, context = Logics.dispute_statement(order, request.user,
|
||||
statement)
|
||||
valid, context = Logics.dispute_statement(order, request.user, statement)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# 6) If action is rate
|
||||
elif action == "rate_user" and rating:
|
||||
valid, context = Logics.rate_counterparty(order, request.user,
|
||||
rating)
|
||||
valid, context = Logics.rate_counterparty(order, request.user, rating)
|
||||
if not valid:
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
@ -546,10 +597,8 @@ class OrderView(viewsets.ViewSet):
|
||||
else:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"The Robotic Satoshis working in the warehouse did not understand you. "
|
||||
+
|
||||
"Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues"
|
||||
"bad_request": "The Robotic Satoshis working in the warehouse did not understand you. "
|
||||
+ "Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues"
|
||||
},
|
||||
status.HTTP_501_NOT_IMPLEMENTED,
|
||||
)
|
||||
@ -558,15 +607,12 @@ class OrderView(viewsets.ViewSet):
|
||||
|
||||
|
||||
class UserView(APIView):
|
||||
NickGen = NickGenerator(lang="English",
|
||||
use_adv=False,
|
||||
use_adj=True,
|
||||
use_noun=True,
|
||||
max_num=999)
|
||||
NickGen = NickGenerator(
|
||||
lang="English", use_adv=False, use_adj=True, use_noun=True, max_num=999
|
||||
)
|
||||
|
||||
serializer_class = UserGenSerializer
|
||||
|
||||
|
||||
def post(self, request, format=None):
|
||||
"""
|
||||
Get a new user derived from a high entropy token
|
||||
@ -581,7 +627,7 @@ class UserView(APIView):
|
||||
context = {}
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
|
||||
# Return bad request if serializer is not valid
|
||||
# Return bad request if serializer is not valid
|
||||
if not serializer.is_valid():
|
||||
context = {"bad_request": "Invalid serializer"}
|
||||
return Response(context, status=status.HTTP_400_BAD_REQUEST)
|
||||
@ -590,12 +636,15 @@ class UserView(APIView):
|
||||
if request.user.is_authenticated:
|
||||
context = {"nickname": request.user.username}
|
||||
not_participant, _, order = Logics.validate_already_maker_or_taker(
|
||||
request.user)
|
||||
request.user
|
||||
)
|
||||
|
||||
# Does not allow this 'mistake' if an active order
|
||||
if not not_participant:
|
||||
context["active_order_id"] = order.id
|
||||
context["bad_request"] = f"You are already logged in as {request.user} and have an active order"
|
||||
context[
|
||||
"bad_request"
|
||||
] = f"You are already logged in as {request.user} and have an active order"
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# The new way. The token is never sent. Only its SHA256
|
||||
@ -603,16 +652,21 @@ class UserView(APIView):
|
||||
public_key = serializer.data.get("public_key")
|
||||
encrypted_private_key = serializer.data.get("encrypted_private_key")
|
||||
ref_code = serializer.data.get("ref_code")
|
||||
|
||||
|
||||
if not public_key or not encrypted_private_key:
|
||||
context["bad_request"] = "Must provide valid 'pub' and 'enc_priv' PGP keys"
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
valid, bad_keys_context, public_key, encrypted_private_key = Logics.validate_pgp_keys(public_key, encrypted_private_key)
|
||||
(
|
||||
valid,
|
||||
bad_keys_context,
|
||||
public_key,
|
||||
encrypted_private_key,
|
||||
) = Logics.validate_pgp_keys(public_key, encrypted_private_key)
|
||||
if not valid:
|
||||
return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
# Now the server only receives a hash of the token. So server trusts the client
|
||||
# Now the server only receives a hash of the token. So server trusts the client
|
||||
# with computing length, counts and unique_values to confirm the high entropy of the token
|
||||
# In any case, it is up to the client if they want to create a bad high entropy token.
|
||||
|
||||
@ -640,7 +694,7 @@ class UserView(APIView):
|
||||
pass
|
||||
|
||||
# Hash the token_sha256, only 1 iteration. (this is the second SHA256 of the user token, aka RoboSats ID)
|
||||
hash = hashlib.sha256(token_sha256.encode('utf-8')).hexdigest()
|
||||
hash = hashlib.sha256(token_sha256.encode("utf-8")).hexdigest()
|
||||
|
||||
# Generate nickname deterministically
|
||||
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0]
|
||||
@ -658,17 +712,17 @@ class UserView(APIView):
|
||||
|
||||
# Create new credentials and login if nickname is new
|
||||
if len(User.objects.filter(username=nickname)) == 0:
|
||||
User.objects.create_user(username=nickname,
|
||||
password=token_sha256,
|
||||
is_staff=False)
|
||||
User.objects.create_user(
|
||||
username=nickname, password=token_sha256, is_staff=False
|
||||
)
|
||||
user = authenticate(request, username=nickname, password=token_sha256)
|
||||
login(request, user)
|
||||
|
||||
context['referral_code'] = token_urlsafe(8)
|
||||
user.profile.referral_code = context['referral_code']
|
||||
context["referral_code"] = token_urlsafe(8)
|
||||
user.profile.referral_code = context["referral_code"]
|
||||
user.profile.avatar = "static/assets/avatars/" + nickname + ".png"
|
||||
|
||||
# Noticed some PGP keys replaced at re-login. Should not happen.
|
||||
|
||||
# Noticed some PGP keys replaced at re-login. Should not happen.
|
||||
# Let's implement this sanity check "If profile has not keys..."
|
||||
if not user.profile.public_key:
|
||||
user.profile.public_key = public_key
|
||||
@ -700,17 +754,21 @@ class UserView(APIView):
|
||||
context["wants_stealth"] = user.profile.wants_stealth
|
||||
|
||||
# Adds/generate telegram token and whether it is enabled
|
||||
context = {**context,**Telegram.get_context(user)}
|
||||
context = {**context, **Telegram.get_context(user)}
|
||||
|
||||
# return active order or last made order if any
|
||||
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(request.user)
|
||||
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
|
||||
request.user
|
||||
)
|
||||
if not has_no_active_order:
|
||||
context["active_order_id"] = order.id
|
||||
else:
|
||||
last_order = Order.objects.filter(Q(maker=request.user) | Q(taker=request.user)).last()
|
||||
last_order = Order.objects.filter(
|
||||
Q(maker=request.user) | Q(taker=request.user)
|
||||
).last()
|
||||
if last_order:
|
||||
context["last_order_id"] = last_order.id
|
||||
|
||||
|
||||
# Sends the welcome back message, only if created +3 mins ago
|
||||
if request.user.date_joined < (timezone.now() - timedelta(minutes=3)):
|
||||
context["found"] = "We found your Robot avatar. Welcome back!"
|
||||
@ -737,8 +795,7 @@ class UserView(APIView):
|
||||
if not not_participant:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Maybe a mistake? User cannot be deleted while he is part of an order"
|
||||
"bad_request": "Maybe a mistake? User cannot be deleted while he is part of an order"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -746,8 +803,7 @@ class UserView(APIView):
|
||||
if user.profile.total_contracts > 0:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Maybe a mistake? User cannot be deleted as it has completed trades"
|
||||
"bad_request": "Maybe a mistake? User cannot be deleted as it has completed trades"
|
||||
},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
@ -775,12 +831,11 @@ class BookView(ListAPIView):
|
||||
if int(currency) == 0 and int(type) != 2:
|
||||
queryset = Order.objects.filter(type=type, status=Order.Status.PUB)
|
||||
elif int(type) == 2 and int(currency) != 0:
|
||||
queryset = Order.objects.filter(currency=currency,
|
||||
status=Order.Status.PUB)
|
||||
queryset = Order.objects.filter(currency=currency, status=Order.Status.PUB)
|
||||
elif not (int(currency) == 0 and int(type) == 2):
|
||||
queryset = Order.objects.filter(currency=currency,
|
||||
type=type,
|
||||
status=Order.Status.PUB)
|
||||
queryset = Order.objects.filter(
|
||||
currency=currency, type=type, status=Order.Status.PUB
|
||||
)
|
||||
|
||||
if len(queryset) == 0:
|
||||
return Response(
|
||||
@ -795,11 +850,12 @@ class BookView(ListAPIView):
|
||||
|
||||
data["satoshis_now"] = Logics.satoshis_now(order)
|
||||
# Compute current premium for those orders that are explicitly priced.
|
||||
data["price"], data["premium"] = Logics.price_and_premium_now(
|
||||
order)
|
||||
data["maker_status"] = Logics.user_activity_status(
|
||||
order.maker_last_seen)
|
||||
for key in ("status","taker"): # Non participants should not see the status or who is the taker
|
||||
data["price"], data["premium"] = Logics.price_and_premium_now(order)
|
||||
data["maker_status"] = Logics.user_activity_status(order.maker_last_seen)
|
||||
for key in (
|
||||
"status",
|
||||
"taker",
|
||||
): # Non participants should not see the status or who is the taker
|
||||
del data[key]
|
||||
|
||||
book_data.append(data)
|
||||
@ -816,18 +872,23 @@ class InfoView(ListAPIView):
|
||||
context = {}
|
||||
|
||||
context["num_public_buy_orders"] = len(
|
||||
Order.objects.filter(type=Order.Types.BUY,
|
||||
status=Order.Status.PUB))
|
||||
Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB)
|
||||
)
|
||||
context["num_public_sell_orders"] = len(
|
||||
Order.objects.filter(type=Order.Types.SELL,
|
||||
status=Order.Status.PUB))
|
||||
context["book_liquidity"] = Order.objects.filter(status=Order.Status.PUB).aggregate(Sum('last_satoshis'))['last_satoshis__sum']
|
||||
context["book_liquidity"] = 0 if context["book_liquidity"] == None else context["book_liquidity"]
|
||||
Order.objects.filter(type=Order.Types.SELL, status=Order.Status.PUB)
|
||||
)
|
||||
context["book_liquidity"] = Order.objects.filter(
|
||||
status=Order.Status.PUB
|
||||
).aggregate(Sum("last_satoshis"))["last_satoshis__sum"]
|
||||
context["book_liquidity"] = (
|
||||
0 if context["book_liquidity"] == None else context["book_liquidity"]
|
||||
)
|
||||
|
||||
# Number of active users (logged in in last 30 minutes)
|
||||
today = datetime.today()
|
||||
context["active_robots_today"] = len(
|
||||
User.objects.filter(last_login__day=today.day))
|
||||
User.objects.filter(last_login__day=today.day)
|
||||
)
|
||||
|
||||
# Compute average premium and volume of today
|
||||
last_day = timezone.now() - timedelta(days=1)
|
||||
@ -860,11 +921,15 @@ class InfoView(ListAPIView):
|
||||
context["node_alias"] = config("NODE_ALIAS")
|
||||
context["node_id"] = config("NODE_ID")
|
||||
context["network"] = config("NETWORK")
|
||||
context["maker_fee"] = float(config("FEE"))*float(config("MAKER_FEE_SPLIT"))
|
||||
context["taker_fee"] = float(config("FEE"))*(1 - float(config("MAKER_FEE_SPLIT")))
|
||||
context["maker_fee"] = float(config("FEE")) * 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["current_swap_fee_rate"] = Logics.compute_swap_fee_rate(BalanceLog.objects.latest('time'))
|
||||
context["current_swap_fee_rate"] = Logics.compute_swap_fee_rate(
|
||||
BalanceLog.objects.latest("time")
|
||||
)
|
||||
|
||||
if request.user.is_authenticated:
|
||||
context["nickname"] = request.user.username
|
||||
@ -872,13 +937,16 @@ class InfoView(ListAPIView):
|
||||
context["earned_rewards"] = request.user.profile.earned_rewards
|
||||
context["wants_stealth"] = request.user.profile.wants_stealth
|
||||
# Adds/generate telegram token and whether it is enabled
|
||||
context = {**context,**Telegram.get_context(request.user)}
|
||||
context = {**context, **Telegram.get_context(request.user)}
|
||||
has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
|
||||
request.user)
|
||||
request.user
|
||||
)
|
||||
if not has_no_active_order:
|
||||
context["active_order_id"] = order.id
|
||||
else:
|
||||
last_order = Order.objects.filter(Q(maker=request.user) | Q(taker=request.user)).last()
|
||||
last_order = Order.objects.filter(
|
||||
Q(maker=request.user) | Q(taker=request.user)
|
||||
).last()
|
||||
if last_order:
|
||||
context["last_order_id"] = last_order.id
|
||||
|
||||
@ -894,10 +962,7 @@ class RewardView(CreateAPIView):
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Woops! It seems you do not have a robot avatar"
|
||||
},
|
||||
{"bad_request": "Woops! It seems you do not have a robot avatar"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -909,7 +974,7 @@ class RewardView(CreateAPIView):
|
||||
valid, context = Logics.withdraw_rewards(request.user, invoice)
|
||||
|
||||
if not valid:
|
||||
context['successful_withdrawal'] = False
|
||||
context["successful_withdrawal"] = False
|
||||
return Response(context, status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
return Response({"successful_withdrawal": True}, status.HTTP_200_OK)
|
||||
@ -923,17 +988,19 @@ class PriceView(ListAPIView):
|
||||
def get(self, request):
|
||||
|
||||
payload = {}
|
||||
queryset = Currency.objects.all().order_by('currency')
|
||||
queryset = Currency.objects.all().order_by("currency")
|
||||
|
||||
for currency in queryset:
|
||||
code = Currency.currency_dict[str(currency.currency)]
|
||||
try:
|
||||
last_tick = MarketTick.objects.filter(currency=currency).latest('timestamp')
|
||||
last_tick = MarketTick.objects.filter(currency=currency).latest(
|
||||
"timestamp"
|
||||
)
|
||||
payload[code] = {
|
||||
'price': last_tick.price,
|
||||
'volume': last_tick.volume,
|
||||
'premium': last_tick.premium,
|
||||
'timestamp': last_tick.timestamp,
|
||||
"price": last_tick.price,
|
||||
"volume": last_tick.volume,
|
||||
"premium": last_tick.premium,
|
||||
"timestamp": last_tick.timestamp,
|
||||
}
|
||||
except:
|
||||
payload[code] = None
|
||||
@ -948,48 +1015,48 @@ class TickView(ListAPIView):
|
||||
|
||||
@extend_schema(**TickViewSchema.get)
|
||||
def get(self, request):
|
||||
data = self.serializer_class(self.queryset.all(), many=True, read_only=True).data
|
||||
data = self.serializer_class(
|
||||
self.queryset.all(), many=True, read_only=True
|
||||
).data
|
||||
return Response(data, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
class LimitView(ListAPIView):
|
||||
|
||||
@extend_schema(**LimitViewSchema.get)
|
||||
def get(self, request):
|
||||
|
||||
|
||||
# Trade limits as BTC
|
||||
min_trade = float(config('MIN_TRADE')) / 100000000
|
||||
max_trade = float(config('MAX_TRADE')) / 100000000
|
||||
max_bondless_trade = float(config('MAX_TRADE_BONDLESS_TAKER')) / 100000000
|
||||
min_trade = float(config("MIN_TRADE")) / 100000000
|
||||
max_trade = float(config("MAX_TRADE")) / 100000000
|
||||
max_bondless_trade = float(config("MAX_TRADE_BONDLESS_TAKER")) / 100000000
|
||||
|
||||
payload = {}
|
||||
queryset = Currency.objects.all().order_by('currency')
|
||||
queryset = Currency.objects.all().order_by("currency")
|
||||
|
||||
for currency in queryset:
|
||||
code = Currency.currency_dict[str(currency.currency)]
|
||||
exchange_rate = float(currency.exchange_rate)
|
||||
payload[currency.currency] = {
|
||||
'code': code,
|
||||
'price': exchange_rate,
|
||||
'min_amount': min_trade * exchange_rate,
|
||||
'max_amount': max_trade * exchange_rate,
|
||||
'max_bondless_amount': max_bondless_trade * exchange_rate,
|
||||
"code": code,
|
||||
"price": exchange_rate,
|
||||
"min_amount": min_trade * exchange_rate,
|
||||
"max_amount": max_trade * exchange_rate,
|
||||
"max_bondless_amount": max_bondless_trade * exchange_rate,
|
||||
}
|
||||
|
||||
return Response(payload, status.HTTP_200_OK)
|
||||
|
||||
|
||||
class HistoricalView(ListAPIView):
|
||||
|
||||
@extend_schema(**HistoricalViewSchema.get)
|
||||
def get(self, request):
|
||||
payload = {}
|
||||
queryset = AccountingDay.objects.all().order_by('day')
|
||||
queryset = AccountingDay.objects.all().order_by("day")
|
||||
|
||||
for accounting_day in queryset:
|
||||
payload[str(accounting_day.day)] = {
|
||||
'volume': accounting_day.contracted,
|
||||
'num_contracts': accounting_day.num_contracts,
|
||||
"volume": accounting_day.contracted,
|
||||
"num_contracts": accounting_day.num_contracts,
|
||||
}
|
||||
|
||||
return Response(payload, status.HTTP_200_OK)
|
||||
@ -998,16 +1065,14 @@ class HistoricalView(ListAPIView):
|
||||
class StealthView(UpdateAPIView):
|
||||
|
||||
serializer_class = StealthSerializer
|
||||
|
||||
@extend_schema(**StealthViewSchema.put)
|
||||
def put(self, request):
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
|
||||
if not request.user.is_authenticated:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Woops! It seems you do not have a robot avatar"
|
||||
},
|
||||
{"bad_request": "Woops! It seems you do not have a robot avatar"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
from django.contrib import admin
|
||||
from django_admin_relation_links import AdminChangeLinksMixin
|
||||
from chat.models import ChatRoom, Message
|
||||
|
||||
# Register your models here.
|
||||
|
||||
|
||||
@ -17,9 +18,10 @@ class ChatRoomAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"taker_connect_date",
|
||||
"room_group_name",
|
||||
)
|
||||
change_links = ["order","maker","taker"]
|
||||
change_links = ["order", "maker", "taker"]
|
||||
search_fields = ["id"]
|
||||
|
||||
|
||||
@admin.register(Message)
|
||||
class MessageAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
list_display = (
|
||||
@ -31,7 +33,7 @@ class MessageAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
|
||||
"receiver_link",
|
||||
"created_at",
|
||||
)
|
||||
change_links = ["chatroom","order","sender","receiver"]
|
||||
search_fields = ["id","index"]
|
||||
ordering = ["-chatroom_id","-index"]
|
||||
list_filter = ("chatroom",)
|
||||
change_links = ["chatroom", "order", "sender", "receiver"]
|
||||
search_fields = ["id", "index"]
|
||||
ordering = ["-chatroom_id", "-index"]
|
||||
list_filter = ("chatroom",)
|
||||
|
@ -6,8 +6,8 @@ from asgiref.sync import async_to_sync
|
||||
|
||||
import json
|
||||
|
||||
class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
@database_sync_to_async
|
||||
def allow_in_chatroom(self):
|
||||
order = Order.objects.get(id=self.order_id)
|
||||
@ -23,37 +23,37 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
@database_sync_to_async
|
||||
def save_connect_user(self):
|
||||
'''Creates or updates the ChatRoom object'''
|
||||
"""Creates or updates the ChatRoom object"""
|
||||
|
||||
order = Order.objects.get(id=self.order_id)
|
||||
|
||||
if order.maker == self.user:
|
||||
ChatRoom.objects.update_or_create(
|
||||
id=self.order_id,
|
||||
order=order,
|
||||
id=self.order_id,
|
||||
order=order,
|
||||
room_group_name=self.room_group_name,
|
||||
defaults={
|
||||
"maker": self.user,
|
||||
"maker_connected": True,
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
elif order.taker == self.user:
|
||||
ChatRoom.objects.update_or_create(
|
||||
id=self.order_id,
|
||||
order=order,
|
||||
id=self.order_id,
|
||||
order=order,
|
||||
room_group_name=self.room_group_name,
|
||||
defaults={
|
||||
"taker": self.user,
|
||||
"taker_connected": True,
|
||||
}
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def save_new_PGP_message(self, PGP_message):
|
||||
'''Creates a Message object'''
|
||||
"""Creates a Message object"""
|
||||
|
||||
order = Order.objects.get(id=self.order_id)
|
||||
chatroom = ChatRoom.objects.get(order=order)
|
||||
@ -71,39 +71,33 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
receiver = order.taker
|
||||
|
||||
msg_obj = Message.objects.create(
|
||||
order=order,
|
||||
chatroom=chatroom,
|
||||
index=index,
|
||||
sender=sender,
|
||||
receiver=receiver,
|
||||
PGP_message=PGP_message,
|
||||
)
|
||||
order=order,
|
||||
chatroom=chatroom,
|
||||
index=index,
|
||||
sender=sender,
|
||||
receiver=receiver,
|
||||
PGP_message=PGP_message,
|
||||
)
|
||||
return msg_obj
|
||||
|
||||
@database_sync_to_async
|
||||
def save_disconnect_user(self):
|
||||
'''Creates or updates the ChatRoom object'''
|
||||
|
||||
"""Creates or updates the ChatRoom object"""
|
||||
|
||||
order = Order.objects.get(id=self.order_id)
|
||||
if order.maker == self.user:
|
||||
ChatRoom.objects.update_or_create(
|
||||
id=self.order_id,
|
||||
defaults={
|
||||
"maker_connected": False
|
||||
}
|
||||
)
|
||||
id=self.order_id, defaults={"maker_connected": False}
|
||||
)
|
||||
elif order.taker == self.user:
|
||||
ChatRoom.objects.update_or_create(
|
||||
id=self.order_id,
|
||||
defaults={
|
||||
"taker_connected": False
|
||||
}
|
||||
)
|
||||
id=self.order_id, defaults={"taker_connected": False}
|
||||
)
|
||||
return None
|
||||
|
||||
@database_sync_to_async
|
||||
def is_peer_connected(self):
|
||||
'''Returns whether the consumer's peer is connected'''
|
||||
"""Returns whether the consumer's peer is connected"""
|
||||
|
||||
chatroom = ChatRoom.objects.get(id=self.order_id)
|
||||
|
||||
@ -115,7 +109,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
@database_sync_to_async
|
||||
def get_peer_PGP_public_key(self):
|
||||
'''Returns peer PGP public key'''
|
||||
"""Returns peer PGP public key"""
|
||||
|
||||
order = Order.objects.get(id=self.order_id)
|
||||
|
||||
@ -127,19 +121,21 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
@database_sync_to_async
|
||||
def get_all_PGP_messages(self):
|
||||
'''Returns all PGP messages'''
|
||||
"""Returns all PGP messages"""
|
||||
|
||||
order = Order.objects.get(id=self.order_id)
|
||||
messages = Message.objects.filter(order=order)
|
||||
|
||||
msgs = []
|
||||
for message in messages:
|
||||
msgs.append({
|
||||
"index": message.index,
|
||||
"time": str(message.created_at),
|
||||
"message": message.PGP_message,
|
||||
"nick": str(message.sender),
|
||||
})
|
||||
msgs.append(
|
||||
{
|
||||
"index": message.index,
|
||||
"time": str(message.created_at),
|
||||
"message": message.PGP_message,
|
||||
"nick": str(message.sender),
|
||||
}
|
||||
)
|
||||
|
||||
return msgs
|
||||
|
||||
@ -153,8 +149,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
if allowed:
|
||||
await self.save_connect_user()
|
||||
await self.channel_layer.group_add(self.room_group_name,
|
||||
self.channel_name)
|
||||
await self.channel_layer.group_add(self.room_group_name, self.channel_name)
|
||||
|
||||
await self.accept()
|
||||
|
||||
@ -173,13 +168,12 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
|
||||
async def disconnect(self, close_code):
|
||||
await self.save_disconnect_user()
|
||||
await self.channel_layer.group_discard(self.room_group_name,
|
||||
self.channel_name)
|
||||
await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
|
||||
await self.channel_layer.group_send(
|
||||
self.room_group_name,
|
||||
{
|
||||
"type": "chatroom_message",
|
||||
"message": 'peer-disconnected',
|
||||
"message": "peer-disconnected",
|
||||
"nick": self.scope["user"].username,
|
||||
"peer_connected": False,
|
||||
},
|
||||
@ -189,9 +183,9 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
text_data_json = json.loads(text_data)
|
||||
message = text_data_json["message"]
|
||||
peer_connected = await self.is_peer_connected()
|
||||
|
||||
|
||||
# Encrypted messages are stored. They are served later when a user reconnects.
|
||||
if message[0:27] == '-----BEGIN PGP MESSAGE-----':
|
||||
if message[0:27] == "-----BEGIN PGP MESSAGE-----":
|
||||
# save to database
|
||||
msg_obj = await self.save_new_PGP_message(message)
|
||||
|
||||
@ -210,9 +204,9 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
"peer_connected": peer_connected,
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
# Encrypted messages are served when the user requests them
|
||||
elif message[0:23] == '-----SERVE HISTORY-----':
|
||||
elif message[0:23] == "-----SERVE HISTORY-----":
|
||||
# If there is any stored message, serve them.
|
||||
msgs = await self.get_all_PGP_messages()
|
||||
peer_connected = await self.is_peer_connected()
|
||||
@ -221,10 +215,10 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
self.room_group_name,
|
||||
{
|
||||
"type": "PGP_message",
|
||||
"index": msg['index'],
|
||||
"time": msg['time'],
|
||||
"message": msg['message'],
|
||||
"nick": msg['nick'],
|
||||
"index": msg["index"],
|
||||
"time": msg["time"],
|
||||
"message": msg["message"],
|
||||
"nick": msg["nick"],
|
||||
"peer_connected": peer_connected,
|
||||
},
|
||||
)
|
||||
@ -245,11 +239,15 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
nick = event["nick"]
|
||||
peer_connected = event["peer_connected"]
|
||||
|
||||
await self.send(text_data=json.dumps({
|
||||
"message": message,
|
||||
"user_nick": nick,
|
||||
"peer_connected": peer_connected,
|
||||
}))
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{
|
||||
"message": message,
|
||||
"user_nick": nick,
|
||||
"peer_connected": peer_connected,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
async def PGP_message(self, event):
|
||||
message = event["message"]
|
||||
@ -258,10 +256,14 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
|
||||
peer_connected = event["peer_connected"]
|
||||
time = event["time"]
|
||||
|
||||
await self.send(text_data=json.dumps({
|
||||
"index": index,
|
||||
"message": message,
|
||||
"user_nick": nick,
|
||||
"peer_connected": peer_connected,
|
||||
"time":time,
|
||||
}))
|
||||
await self.send(
|
||||
text_data=json.dumps(
|
||||
{
|
||||
"index": index,
|
||||
"message": message,
|
||||
"user_nick": nick,
|
||||
"peer_connected": peer_connected,
|
||||
"time": time,
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -3,24 +3,29 @@ from api.models import User, Order
|
||||
from django.utils import timezone
|
||||
import uuid
|
||||
|
||||
class ChatRoom(models.Model):
|
||||
'''
|
||||
Simple ChatRoom model. Needed to facilitate communication: Is my counterpart in the room?
|
||||
'''
|
||||
|
||||
id = models.PositiveBigIntegerField(primary_key=True, null=False,default=None, blank=True)
|
||||
class ChatRoom(models.Model):
|
||||
"""
|
||||
Simple ChatRoom model. Needed to facilitate communication: Is my counterpart in the room?
|
||||
"""
|
||||
|
||||
id = models.PositiveBigIntegerField(
|
||||
primary_key=True, null=False, default=None, blank=True
|
||||
)
|
||||
order = models.ForeignKey(
|
||||
Order,
|
||||
related_name="chatroom",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None)
|
||||
default=None,
|
||||
)
|
||||
maker = models.ForeignKey(
|
||||
User,
|
||||
related_name="chat_maker",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None)
|
||||
default=None,
|
||||
)
|
||||
taker = models.ForeignKey(
|
||||
User,
|
||||
related_name="chat_taker",
|
||||
@ -30,8 +35,8 @@ class ChatRoom(models.Model):
|
||||
blank=True,
|
||||
)
|
||||
|
||||
maker_connected = models.BooleanField(default=False, null=False)
|
||||
taker_connected = models.BooleanField(default=False, null=False)
|
||||
maker_connected = models.BooleanField(default=False, null=False)
|
||||
taker_connected = models.BooleanField(default=False, null=False)
|
||||
|
||||
maker_connect_date = models.DateTimeField(auto_now_add=True)
|
||||
taker_connect_date = models.DateTimeField(auto_now_add=True)
|
||||
@ -46,41 +51,39 @@ class ChatRoom(models.Model):
|
||||
def __str__(self):
|
||||
return f"Chat:{str(self.id)}"
|
||||
|
||||
|
||||
class Message(models.Model):
|
||||
class Meta:
|
||||
get_latest_by = 'index'
|
||||
get_latest_by = "index"
|
||||
|
||||
# id = models.PositiveBigIntegerField(primary_key=True, default=uuid.uuid4, editable=False)
|
||||
order = models.ForeignKey(
|
||||
Order,
|
||||
related_name="message",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
default=None)
|
||||
Order, related_name="message", on_delete=models.CASCADE, null=True, default=None
|
||||
)
|
||||
chatroom = models.ForeignKey(
|
||||
ChatRoom,
|
||||
related_name="chatroom",
|
||||
on_delete=models.CASCADE,
|
||||
null=True,
|
||||
default=None)
|
||||
index = models.PositiveIntegerField(null=False,default=None, blank=True)
|
||||
default=None,
|
||||
)
|
||||
index = models.PositiveIntegerField(null=False, default=None, blank=True)
|
||||
sender = models.ForeignKey(
|
||||
User,
|
||||
related_name="message_sender",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None)
|
||||
default=None,
|
||||
)
|
||||
receiver = models.ForeignKey(
|
||||
User,
|
||||
related_name="message_receiver",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
default=None)
|
||||
default=None,
|
||||
)
|
||||
|
||||
PGP_message = models.TextField(max_length=5000,
|
||||
null=True,
|
||||
default=None,
|
||||
blank=True)
|
||||
PGP_message = models.TextField(max_length=5000, null=True, default=None, blank=True)
|
||||
|
||||
created_at = models.DateTimeField(default=timezone.now)
|
||||
|
||||
|
@ -2,6 +2,5 @@ from django.urls import re_path
|
||||
from . import consumers
|
||||
|
||||
websocket_urlpatterns = [
|
||||
re_path(r"ws/chat/(?P<order_id>\w+)/$",
|
||||
consumers.ChatRoomConsumer.as_asgi()),
|
||||
re_path(r"ws/chat/(?P<order_id>\w+)/$", consumers.ChatRoomConsumer.as_asgi()),
|
||||
]
|
||||
|
@ -1,8 +1,8 @@
|
||||
from rest_framework import serializers
|
||||
from chat.models import Message
|
||||
|
||||
class ChatSerializer(serializers.ModelSerializer):
|
||||
|
||||
class ChatSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = (
|
||||
@ -13,14 +13,17 @@ class ChatSerializer(serializers.ModelSerializer):
|
||||
)
|
||||
depth = 0
|
||||
|
||||
|
||||
class PostMessageSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = Message
|
||||
fields = ("PGP_message","order","offset")
|
||||
fields = ("PGP_message", "order", "offset")
|
||||
depth = 0
|
||||
|
||||
offset = serializers.IntegerField(allow_null=True,
|
||||
default=None,
|
||||
required=False,
|
||||
min_value=0,
|
||||
help_text="Offset for message index to get as response")
|
||||
|
||||
offset = serializers.IntegerField(
|
||||
allow_null=True,
|
||||
default=None,
|
||||
required=False,
|
||||
min_value=0,
|
||||
help_text="Offset for message index to get as response",
|
||||
)
|
||||
|
@ -1,9 +1,10 @@
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task(name="chatrooms_cleansing")
|
||||
def chatrooms_cleansing():
|
||||
"""
|
||||
Deletes chatrooms and encrypted messages of orders
|
||||
Deletes chatrooms and encrypted messages of orders
|
||||
that have completely finished more than 3 days ago.
|
||||
"""
|
||||
|
||||
@ -12,24 +13,28 @@ def chatrooms_cleansing():
|
||||
from datetime import timedelta
|
||||
from django.utils import timezone
|
||||
|
||||
finished_states = [Order.Status.SUC,
|
||||
Order.Status.TLD,
|
||||
Order.Status.MLD,
|
||||
Order.Status.CCA,
|
||||
Order.Status.UCA]
|
||||
finished_states = [
|
||||
Order.Status.SUC,
|
||||
Order.Status.TLD,
|
||||
Order.Status.MLD,
|
||||
Order.Status.CCA,
|
||||
Order.Status.UCA,
|
||||
]
|
||||
|
||||
# Orders that have expired more than 3 days ago
|
||||
# Usually expiry takes place 1 day after a finished order. So, ~4 days
|
||||
# Usually expiry takes place 1 day after a finished order. So, ~4 days
|
||||
# until encrypted messages are deleted.
|
||||
finished_time = timezone.now() - timedelta(days=3)
|
||||
queryset = Order.objects.filter(status__in=finished_states, expires_at__lt=finished_time)
|
||||
queryset = Order.objects.filter(
|
||||
status__in=finished_states, expires_at__lt=finished_time
|
||||
)
|
||||
|
||||
# And do not have an active trade, any past contract or any reward.
|
||||
deleted_chatrooms = []
|
||||
for order in queryset:
|
||||
# Try an except. In case some chatroom is already missing.
|
||||
try:
|
||||
chatroom = ChatRoom.objects.get(id = order.id)
|
||||
chatroom = ChatRoom.objects.get(id=order.id)
|
||||
deleted_chatrooms.append(str(chatroom))
|
||||
chatroom.delete()
|
||||
except:
|
||||
@ -39,4 +44,4 @@ def chatrooms_cleansing():
|
||||
"num_deleted": len(deleted_chatrooms),
|
||||
"deleted_chatrooms": deleted_chatrooms,
|
||||
}
|
||||
return results
|
||||
return results
|
||||
|
123
chat/views.py
123
chat/views.py
@ -10,11 +10,14 @@ from django.utils import timezone
|
||||
from asgiref.sync import async_to_sync
|
||||
from channels.layers import get_channel_layer
|
||||
|
||||
|
||||
class ChatView(viewsets.ViewSet):
|
||||
serializer_class = PostMessageSerializer
|
||||
lookup_url_kwarg = ["order_id","offset"]
|
||||
lookup_url_kwarg = ["order_id", "offset"]
|
||||
|
||||
queryset = Message.objects.filter(order__status__in=[Order.Status.CHA, Order.Status.FSE])
|
||||
queryset = Message.objects.filter(
|
||||
order__status__in=[Order.Status.CHA, Order.Status.FSE]
|
||||
)
|
||||
|
||||
def get(self, request, format=None):
|
||||
"""
|
||||
@ -26,63 +29,57 @@ class ChatView(viewsets.ViewSet):
|
||||
|
||||
if order_id is None:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Order ID does not exist"
|
||||
},
|
||||
{"bad_request": "Order ID does not exist"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
)
|
||||
|
||||
order = Order.objects.get(id=order_id)
|
||||
|
||||
if not (request.user == order.maker or request.user == order.taker):
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"You are not participant in this order"
|
||||
},
|
||||
{"bad_request": "You are not participant in this order"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not order.status in [Order.Status.CHA, Order.Status.FSE]:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Order is not in chat status"
|
||||
},
|
||||
{"bad_request": "Order is not in chat status"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
queryset = Message.objects.filter(order=order, index__gt=offset)
|
||||
chatroom = ChatRoom.objects.get(order=order)
|
||||
|
||||
# Poor idea: is_peer_connected() mockup. Update connection status based on last time a GET request was sent
|
||||
|
||||
# Poor idea: is_peer_connected() mockup. Update connection status based on last time a GET request was sent
|
||||
if chatroom.maker == request.user:
|
||||
chatroom.taker_connected = order.taker_last_seen > (timezone.now() - timedelta(minutes=1))
|
||||
chatroom.taker_connected = order.taker_last_seen > (
|
||||
timezone.now() - timedelta(minutes=1)
|
||||
)
|
||||
chatroom.maker_connected = True
|
||||
chatroom.save()
|
||||
peer_connected = chatroom.taker_connected
|
||||
elif chatroom.taker == request.user:
|
||||
chatroom.maker_connected = order.maker_last_seen > (timezone.now() - timedelta(minutes=1))
|
||||
chatroom.maker_connected = order.maker_last_seen > (
|
||||
timezone.now() - timedelta(minutes=1)
|
||||
)
|
||||
chatroom.taker_connected = True
|
||||
chatroom.save()
|
||||
peer_connected = chatroom.maker_connected
|
||||
|
||||
|
||||
|
||||
messages = []
|
||||
for message in queryset:
|
||||
d = ChatSerializer(message).data
|
||||
print(d)
|
||||
# Re-serialize so the response is identical to the consumer message
|
||||
data = {
|
||||
'index':d['index'],
|
||||
'time':d['created_at'],
|
||||
'message':d['PGP_message'],
|
||||
'nick': User.objects.get(id=d['sender']).username
|
||||
}
|
||||
"index": d["index"],
|
||||
"time": d["created_at"],
|
||||
"message": d["PGP_message"],
|
||||
"nick": User.objects.get(id=d["sender"]).username,
|
||||
}
|
||||
messages.append(data)
|
||||
|
||||
response = {'peer_connected': peer_connected, 'messages':messages}
|
||||
response = {"peer_connected": peer_connected, "messages": messages}
|
||||
|
||||
return Response(response, status.HTTP_200_OK)
|
||||
|
||||
@ -92,7 +89,7 @@ class ChatView(viewsets.ViewSet):
|
||||
"""
|
||||
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
# Return bad request if serializer is not valid
|
||||
# Return bad request if serializer is not valid
|
||||
if not serializer.is_valid():
|
||||
context = {"bad_request": "Invalid serializer"}
|
||||
return Response(context, status=status.HTTP_400_BAD_REQUEST)
|
||||
@ -102,30 +99,21 @@ class ChatView(viewsets.ViewSet):
|
||||
|
||||
if order_id is None:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Order ID does not exist"
|
||||
},
|
||||
{"bad_request": "Order ID does not exist"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
)
|
||||
|
||||
order = Order.objects.get(id=order_id)
|
||||
|
||||
if not (request.user == order.maker or request.user == order.taker):
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"You are not participant in this order"
|
||||
},
|
||||
{"bad_request": "You are not participant in this order"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
if not order.status in [Order.Status.CHA, Order.Status.FSE]:
|
||||
return Response(
|
||||
{
|
||||
"bad_request":
|
||||
"Order is not in chat status"
|
||||
},
|
||||
{"bad_request": "Order is not in chat status"},
|
||||
status.HTTP_400_BAD_REQUEST,
|
||||
)
|
||||
|
||||
@ -137,26 +125,26 @@ class ChatView(viewsets.ViewSet):
|
||||
receiver = order.maker
|
||||
|
||||
chatroom, _ = ChatRoom.objects.get_or_create(
|
||||
id=order_id,
|
||||
order=order,
|
||||
room_group_name=f"chat_order_{order_id}",
|
||||
defaults={
|
||||
"maker": order.maker,
|
||||
"maker_connected": order.maker == request.user,
|
||||
"taker": order.taker,
|
||||
"taker_connected": order.taker == request.user,
|
||||
}
|
||||
)
|
||||
id=order_id,
|
||||
order=order,
|
||||
room_group_name=f"chat_order_{order_id}",
|
||||
defaults={
|
||||
"maker": order.maker,
|
||||
"maker_connected": order.maker == request.user,
|
||||
"taker": order.taker,
|
||||
"taker_connected": order.taker == request.user,
|
||||
},
|
||||
)
|
||||
|
||||
last_index = Message.objects.filter(order=order, chatroom=chatroom).count()
|
||||
new_message = Message.objects.create(
|
||||
index=last_index+1,
|
||||
index=last_index + 1,
|
||||
PGP_message=serializer.data.get("PGP_message"),
|
||||
order=order,
|
||||
chatroom=chatroom,
|
||||
sender=sender,
|
||||
receiver=receiver,
|
||||
)
|
||||
)
|
||||
|
||||
# Send websocket message
|
||||
if chatroom.maker == request.user:
|
||||
@ -168,13 +156,13 @@ class ChatView(viewsets.ViewSet):
|
||||
async_to_sync(channel_layer.group_send)(
|
||||
f"chat_order_{order_id}",
|
||||
{
|
||||
"type": "PGP_message",
|
||||
"index": new_message.index,
|
||||
"message": new_message.PGP_message,
|
||||
"time": str(new_message.created_at),
|
||||
"nick": new_message.sender.username,
|
||||
"peer_connected": peer_connected,
|
||||
}
|
||||
"type": "PGP_message",
|
||||
"index": new_message.index,
|
||||
"message": new_message.PGP_message,
|
||||
"time": str(new_message.created_at),
|
||||
"nick": new_message.sender.username,
|
||||
"peer_connected": peer_connected,
|
||||
},
|
||||
)
|
||||
|
||||
# if offset is given, reply with messages
|
||||
@ -187,16 +175,15 @@ class ChatView(viewsets.ViewSet):
|
||||
print(d)
|
||||
# Re-serialize so the response is identical to the consumer message
|
||||
data = {
|
||||
'index':d['index'],
|
||||
'time':d['created_at'],
|
||||
'message':d['PGP_message'],
|
||||
'nick': User.objects.get(id=d['sender']).username
|
||||
}
|
||||
"index": d["index"],
|
||||
"time": d["created_at"],
|
||||
"message": d["PGP_message"],
|
||||
"nick": User.objects.get(id=d["sender"]).username,
|
||||
}
|
||||
messages.append(data)
|
||||
|
||||
response = {'peer_connected': peer_connected, 'messages':messages}
|
||||
response = {"peer_connected": peer_connected, "messages": messages}
|
||||
else:
|
||||
response = {}
|
||||
|
||||
return Response(response, status.HTTP_200_OK)
|
||||
|
@ -4,6 +4,7 @@ from import_export.admin import ImportExportModelAdmin
|
||||
|
||||
# Register your models here.
|
||||
|
||||
|
||||
@admin.register(AccountingDay)
|
||||
class AccountingDayAdmin(ImportExportModelAdmin):
|
||||
|
||||
@ -29,6 +30,7 @@ class AccountingDayAdmin(ImportExportModelAdmin):
|
||||
change_links = ["day"]
|
||||
search_fields = ["day"]
|
||||
|
||||
|
||||
@admin.register(BalanceLog)
|
||||
class BalanceLogAdmin(ImportExportModelAdmin):
|
||||
|
||||
@ -57,4 +59,4 @@ class BalanceLogAdmin(ImportExportModelAdmin):
|
||||
"ln_remote_unsettled",
|
||||
]
|
||||
change_links = ["time"]
|
||||
search_fields = ["time"]
|
||||
search_fields = ["time"]
|
||||
|
@ -2,5 +2,5 @@ from django.apps import AppConfig
|
||||
|
||||
|
||||
class ControlConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'control'
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "control"
|
||||
|
@ -3,79 +3,129 @@ from django.utils import timezone
|
||||
|
||||
from api.lightning.node import LNNode
|
||||
|
||||
|
||||
class AccountingDay(models.Model):
|
||||
day = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False)
|
||||
|
||||
# Every field is denominated in Sats with (3 decimals for millisats)
|
||||
# Total volume contracted
|
||||
contracted = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
||||
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_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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
mining_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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
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)
|
||||
earned_rewards = models.DecimalField(
|
||||
max_digits=15, decimal_places=3, default=0, null=False, blank=False
|
||||
)
|
||||
# Balance change on pending disputes (not resolved yet)
|
||||
disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
|
||||
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)
|
||||
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']
|
||||
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']
|
||||
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']
|
||||
return LNNode.wallet_balance()["unconfirmed_balance"]
|
||||
|
||||
def get_ln_local():
|
||||
return LNNode.channel_balance()['local_balance']
|
||||
return LNNode.channel_balance()["local_balance"]
|
||||
|
||||
def get_ln_remote():
|
||||
return LNNode.channel_balance()['remote_balance']
|
||||
return LNNode.channel_balance()["remote_balance"]
|
||||
|
||||
def get_ln_local_unsettled():
|
||||
return LNNode.channel_balance()['unsettled_local_balance']
|
||||
return LNNode.channel_balance()["unsettled_local_balance"]
|
||||
|
||||
def get_ln_remote_unsettled():
|
||||
return LNNode.channel_balance()['unsettled_remote_balance']
|
||||
return LNNode.channel_balance()["unsettled_remote_balance"]
|
||||
|
||||
time = models.DateTimeField(primary_key=True, default=timezone.now)
|
||||
|
||||
# Every field is denominated in Sats
|
||||
total = models.PositiveBigIntegerField(default=get_total)
|
||||
onchain_fraction = models.DecimalField(max_digits=6, decimal_places=5, default=get_frac)
|
||||
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)
|
||||
ln_remote_unsettled = models.PositiveBigIntegerField(
|
||||
default=get_ln_remote_unsettled
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"Balance at {self.time.strftime('%d/%m/%Y %H:%M:%S')}"
|
||||
|
||||
|
||||
class Dispute(models.Model):
|
||||
pass
|
||||
pass
|
||||
|
143
control/tasks.py
143
control/tasks.py
@ -1,10 +1,11 @@
|
||||
from celery import shared_task
|
||||
|
||||
|
||||
@shared_task(name="do_accounting")
|
||||
def do_accounting():
|
||||
'''
|
||||
"""
|
||||
Does all accounting from the beginning of time
|
||||
'''
|
||||
"""
|
||||
|
||||
from api.models import Order, LNPayment, OnchainPayment, Profile, MarketTick
|
||||
from control.models import AccountingDay
|
||||
@ -18,61 +19,84 @@ def do_accounting():
|
||||
today = timezone.now().date()
|
||||
|
||||
try:
|
||||
last_accounted_day = AccountingDay.objects.latest('day').day.date()
|
||||
accounted_yesterday = AccountingDay.objects.latest('day')
|
||||
last_accounted_day = AccountingDay.objects.latest("day").day.date()
|
||||
accounted_yesterday = AccountingDay.objects.latest("day")
|
||||
except:
|
||||
last_accounted_day = None
|
||||
accounted_yesterday = None
|
||||
|
||||
if last_accounted_day == today:
|
||||
return {'message':'no days to account for'}
|
||||
return {"message": "no days to account for"}
|
||||
elif last_accounted_day != None:
|
||||
initial_day = last_accounted_day + timedelta(days=1)
|
||||
elif last_accounted_day == None:
|
||||
initial_day = all_payments.earliest('created_at').created_at.date()
|
||||
initial_day = all_payments.earliest("created_at").created_at.date()
|
||||
|
||||
|
||||
day = initial_day
|
||||
result = {}
|
||||
while day <= today:
|
||||
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_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)
|
||||
)
|
||||
|
||||
# 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()
|
||||
inflow = day_payments.filter(type=LNPayment.Types.HOLD,status=LNPayment.Status.SETLED).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
|
||||
onchain_outflow = day_onchain_payments.filter(status__in=[OnchainPayment.Status.MEMPO,OnchainPayment.Status.CONFI]).aggregate(Sum('sent_satoshis'))['sent_satoshis__sum']
|
||||
inflow = day_payments.filter(
|
||||
type=LNPayment.Types.HOLD, status=LNPayment.Status.SETLED
|
||||
).aggregate(Sum("num_satoshis"))["num_satoshis__sum"]
|
||||
onchain_outflow = day_onchain_payments.filter(
|
||||
status__in=[OnchainPayment.Status.MEMPO, OnchainPayment.Status.CONFI]
|
||||
).aggregate(Sum("sent_satoshis"))["sent_satoshis__sum"]
|
||||
onchain_outflow = 0 if onchain_outflow == None else int(onchain_outflow)
|
||||
offchain_outflow = day_payments.filter(type=LNPayment.Types.NORM,status=LNPayment.Status.SUCCED).aggregate(Sum('num_satoshis'))['num_satoshis__sum']
|
||||
offchain_outflow = day_payments.filter(
|
||||
type=LNPayment.Types.NORM, status=LNPayment.Status.SUCCED
|
||||
).aggregate(Sum("num_satoshis"))["num_satoshis__sum"]
|
||||
offchain_outflow = 0 if offchain_outflow == None else int(offchain_outflow)
|
||||
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']
|
||||
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"]
|
||||
|
||||
contracted = 0 if contracted == None else contracted
|
||||
inflow = 0 if inflow == None else inflow
|
||||
outflow = offchain_outflow + onchain_outflow
|
||||
routing_fees = 0 if routing_fees == None else routing_fees
|
||||
routing_fees = 0 if routing_fees == None else routing_fees
|
||||
rewards_claimed = 0 if rewards_claimed == None else rewards_claimed
|
||||
mining_fees = 0 if mining_fees == None else mining_fees
|
||||
|
||||
accounted_day = AccountingDay.objects.create(
|
||||
day = day,
|
||||
contracted = contracted,
|
||||
num_contracts = num_contracts,
|
||||
inflow = inflow,
|
||||
outflow = outflow,
|
||||
routing_fees = routing_fees,
|
||||
mining_fees = mining_fees,
|
||||
cashflow = inflow - outflow - routing_fees,
|
||||
rewards_claimed = rewards_claimed,
|
||||
)
|
||||
|
||||
day=day,
|
||||
contracted=contracted,
|
||||
num_contracts=num_contracts,
|
||||
inflow=inflow,
|
||||
outflow=outflow,
|
||||
routing_fees=routing_fees,
|
||||
mining_fees=mining_fees,
|
||||
cashflow=inflow - outflow - routing_fees,
|
||||
rewards_claimed=rewards_claimed,
|
||||
)
|
||||
|
||||
# Fine Net Daily accounting based on orders
|
||||
# Only account for orders where everything worked out right
|
||||
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
|
||||
payouts_paid = 0
|
||||
costs = 0
|
||||
@ -80,31 +104,42 @@ def do_accounting():
|
||||
escrows_settled += int(payout.order_paid_LN.trade_escrow.num_satoshis)
|
||||
payouts_paid += int(payout.num_satoshis)
|
||||
costs += int(payout.fee)
|
||||
|
||||
|
||||
# Same for orders that use onchain payments.
|
||||
payouts_tx = day_onchain_payments.filter(status__in=[OnchainPayment.Status.MEMPO,OnchainPayment.Status.CONFI])
|
||||
payouts_tx = day_onchain_payments.filter(
|
||||
status__in=[OnchainPayment.Status.MEMPO, OnchainPayment.Status.CONFI]
|
||||
)
|
||||
for payout_tx in payouts_tx:
|
||||
escrows_settled += int(payout_tx.order_paid_TX.trade_escrow.num_satoshis)
|
||||
payouts_paid += int(payout_tx.sent_satoshis)
|
||||
costs += int(payout_tx.mining_fee_sats)
|
||||
|
||||
|
||||
# account for those orders where bonds were lost
|
||||
# + Settled bonds / bond_split
|
||||
bonds_settled = day_payments.filter(type=LNPayment.Types.HOLD,concept__in=[LNPayment.Concepts.TAKEBOND,LNPayment.Concepts.MAKEBOND], status=LNPayment.Status.SETLED)
|
||||
bonds_settled = day_payments.filter(
|
||||
type=LNPayment.Types.HOLD,
|
||||
concept__in=[LNPayment.Concepts.TAKEBOND, LNPayment.Concepts.MAKEBOND],
|
||||
status=LNPayment.Status.SETLED,
|
||||
)
|
||||
|
||||
if len(bonds_settled) > 0:
|
||||
collected_slashed_bonds = (bonds_settled.aggregate(Sum('num_satoshis'))['num_satoshis__sum'])* float(config('SLASHED_BOND_REWARD_SPLIT'))
|
||||
collected_slashed_bonds = (
|
||||
bonds_settled.aggregate(Sum("num_satoshis"))["num_satoshis__sum"]
|
||||
) * float(config("SLASHED_BOND_REWARD_SPLIT"))
|
||||
else:
|
||||
collected_slashed_bonds = 0
|
||||
|
||||
|
||||
accounted_day.net_settled = escrows_settled + collected_slashed_bonds
|
||||
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
|
||||
if day == today:
|
||||
pending_disputes = Order.objects.filter(status__in=[Order.Status.DIS,Order.Status.WFR])
|
||||
pending_disputes = Order.objects.filter(
|
||||
status__in=[Order.Status.DIS, Order.Status.WFR]
|
||||
)
|
||||
if len(pending_disputes) > 0:
|
||||
outstanding_pending_disputes = 0
|
||||
for order in pending_disputes:
|
||||
@ -112,28 +147,44 @@ def do_accounting():
|
||||
else:
|
||||
outstanding_pending_disputes = 0
|
||||
|
||||
accounted_day.outstanding_earned_rewards = Profile.objects.all().aggregate(Sum('earned_rewards'))['earned_rewards__sum']
|
||||
accounted_day.outstanding_earned_rewards = Profile.objects.all().aggregate(
|
||||
Sum("earned_rewards")
|
||||
)["earned_rewards__sum"]
|
||||
accounted_day.outstanding_pending_disputes = outstanding_pending_disputes
|
||||
accounted_day.lifetime_rewards_claimed = Profile.objects.all().aggregate(Sum('claimed_rewards'))['claimed_rewards__sum']
|
||||
accounted_day.lifetime_rewards_claimed = Profile.objects.all().aggregate(
|
||||
Sum("claimed_rewards")
|
||||
)["claimed_rewards__sum"]
|
||||
if accounted_yesterday != None:
|
||||
accounted_day.earned_rewards = accounted_day.outstanding_earned_rewards - accounted_yesterday.outstanding_earned_rewards
|
||||
accounted_day.disputes = outstanding_pending_disputes - accounted_yesterday.outstanding_earned_rewards
|
||||
accounted_day.earned_rewards = (
|
||||
accounted_day.outstanding_earned_rewards
|
||||
- accounted_yesterday.outstanding_earned_rewards
|
||||
)
|
||||
accounted_day.disputes = (
|
||||
outstanding_pending_disputes
|
||||
- accounted_yesterday.outstanding_earned_rewards
|
||||
)
|
||||
|
||||
# Close the loop
|
||||
accounted_day.save()
|
||||
accounted_yesterday = accounted_day
|
||||
result[str(day)]={'contracted':contracted,'inflow':inflow,'outflow':outflow}
|
||||
result[str(day)] = {
|
||||
"contracted": contracted,
|
||||
"inflow": inflow,
|
||||
"outflow": outflow,
|
||||
}
|
||||
day = day + timedelta(days=1)
|
||||
|
||||
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
|
||||
|
@ -4,4 +4,4 @@ from __future__ import absolute_import, unicode_literals
|
||||
# Django starts so that shared_task will use this app.
|
||||
from .celery import app as celery_app
|
||||
|
||||
__all__ = ("celery_app", )
|
||||
__all__ = ("celery_app",)
|
||||
|
@ -35,11 +35,11 @@ app.conf.beat_schedule = {
|
||||
"task": "users_cleansing",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"chatrooms-cleansing": { # Cleans 3+ days old encrypted messages and chatrooms at midnight
|
||||
"chatrooms-cleansing": { # Cleans 3+ days old encrypted messages and chatrooms at midnight
|
||||
"task": "chatrooms_cleansing",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
"lnpayments-cleansing": { # Cleans 3+ days old unlocked hodl invoices
|
||||
"lnpayments-cleansing": { # Cleans 3+ days old unlocked hodl invoices
|
||||
"task": "payments_cleansing",
|
||||
"schedule": crontab(hour=0, minute=0),
|
||||
},
|
||||
@ -55,10 +55,10 @@ app.conf.beat_schedule = {
|
||||
"task": "cache_external_market_prices",
|
||||
"schedule": timedelta(seconds=60),
|
||||
},
|
||||
"compute-node-balance": { # Logs LND channel and wallet balance
|
||||
"task":"compute_node_balance",
|
||||
"compute-node-balance": { # Logs LND channel and wallet balance
|
||||
"task": "compute_node_balance",
|
||||
"schedule": timedelta(minutes=60),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
app.conf.timezone = "UTC"
|
||||
|
@ -2,11 +2,13 @@ from channels.auth import AuthMiddlewareStack
|
||||
from channels.routing import ProtocolTypeRouter, URLRouter
|
||||
import chat.routing
|
||||
|
||||
application = ProtocolTypeRouter({
|
||||
"websocket":
|
||||
AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
chat.routing.websocket_urlpatterns,
|
||||
# TODO add api.routing.websocket_urlpatterns when Order page works with websocket
|
||||
)),
|
||||
})
|
||||
application = ProtocolTypeRouter(
|
||||
{
|
||||
"websocket": AuthMiddlewareStack(
|
||||
URLRouter(
|
||||
chat.routing.websocket_urlpatterns,
|
||||
# TODO add api.routing.websocket_urlpatterns when Order page works with websocket
|
||||
)
|
||||
),
|
||||
}
|
||||
)
|
||||
|
@ -53,21 +53,21 @@ SESSION_COOKIE_HTTPONLY = False
|
||||
# Logging settings
|
||||
if os.environ.get("LOG_TO_CONSOLE"):
|
||||
LOGGING = {
|
||||
'version': 1,
|
||||
'disable_existing_loggers': False,
|
||||
'handlers': {
|
||||
'console': {
|
||||
'class': 'logging.StreamHandler',
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
},
|
||||
},
|
||||
'root': {
|
||||
'handlers': ['console'],
|
||||
'level': 'WARNING',
|
||||
"root": {
|
||||
"handlers": ["console"],
|
||||
"level": "WARNING",
|
||||
},
|
||||
'loggers': {
|
||||
'api.utils': {
|
||||
'handlers': ['console'],
|
||||
'level': 'WARNING',
|
||||
"loggers": {
|
||||
"api.utils": {
|
||||
"handlers": ["console"],
|
||||
"level": "WARNING",
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -95,12 +95,12 @@ INSTALLED_APPS = [
|
||||
]
|
||||
|
||||
REST_FRAMEWORK = {
|
||||
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema',
|
||||
"DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
|
||||
}
|
||||
|
||||
SPECTACULAR_SETTINGS = {
|
||||
'TITLE': 'RoboSats REST API v0',
|
||||
'DESCRIPTION': textwrap.dedent(
|
||||
"TITLE": "RoboSats REST API v0",
|
||||
"DESCRIPTION": textwrap.dedent(
|
||||
"""
|
||||
REST API Documentation for [RoboSats](https://learn.robosats.com) - A Simple and Private LN P2P Exchange
|
||||
|
||||
@ -114,21 +114,21 @@ SPECTACULAR_SETTINGS = {
|
||||
|
||||
"""
|
||||
),
|
||||
'VERSION': '0.1.0',
|
||||
'SERVE_INCLUDE_SCHEMA': False,
|
||||
'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead
|
||||
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR',
|
||||
'REDOC_UI_SETTINGS': {
|
||||
'expandResponses': '200,201',
|
||||
"VERSION": "0.1.0",
|
||||
"SERVE_INCLUDE_SCHEMA": False,
|
||||
"SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead
|
||||
"SWAGGER_UI_FAVICON_HREF": "SIDECAR",
|
||||
"REDOC_UI_SETTINGS": {
|
||||
"expandResponses": "200,201",
|
||||
},
|
||||
'EXTENSIONS_INFO': {
|
||||
'x-logo': {
|
||||
'url': 'https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats-0.1.1-banner.png',
|
||||
'backgroundColor': '#FFFFFF',
|
||||
'altText': 'RoboSats logo'
|
||||
"EXTENSIONS_INFO": {
|
||||
"x-logo": {
|
||||
"url": "https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats-0.1.1-banner.png",
|
||||
"backgroundColor": "#FFFFFF",
|
||||
"altText": "RoboSats logo",
|
||||
}
|
||||
},
|
||||
'REDOC_DIST': 'SIDECAR',
|
||||
"REDOC_DIST": "SIDECAR",
|
||||
}
|
||||
|
||||
from .celery.conf import *
|
||||
@ -173,7 +173,7 @@ DATABASES = {
|
||||
"NAME": config("POSTGRES_DB"),
|
||||
"USER": config("POSTGRES_USER"),
|
||||
"PASSWORD": config("POSTGRES_PASSWORD"),
|
||||
'HOST': config("POSTGRES_HOST"),
|
||||
"HOST": config("POSTGRES_HOST"),
|
||||
"PORT": config("POSTGRES_PORT"),
|
||||
}
|
||||
}
|
||||
@ -183,20 +183,16 @@ DATABASES = {
|
||||
|
||||
AUTH_PASSWORD_VALIDATORS = [
|
||||
{
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
|
||||
},
|
||||
{
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
|
||||
},
|
||||
{
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
|
||||
},
|
||||
{
|
||||
"NAME":
|
||||
"django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
|
||||
},
|
||||
]
|
||||
|
||||
@ -230,9 +226,7 @@ CACHES = {
|
||||
"default": {
|
||||
"BACKEND": "django_redis.cache.RedisCache",
|
||||
"LOCATION": config("REDIS_URL"),
|
||||
"OPTIONS": {
|
||||
"CLIENT_CLASS": "django_redis.client.DefaultClient"
|
||||
},
|
||||
"OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
|
||||
}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user