Fix code style issues with Black

This commit is contained in:
Lint Action 2022-10-20 09:56:10 +00:00
parent c32c07eaa6
commit 3d3da78f8a
31 changed files with 1916 additions and 1578 deletions

View File

@ -5,6 +5,7 @@ from django.contrib.auth.admin import UserAdmin
from api.models import OnchainPayment, Order, LNPayment, Profile, MarketTick, Currency from api.models import OnchainPayment, Order, LNPayment, Profile, MarketTick, Currency
from api.logics import Logics from api.logics import Logics
from statistics import median from statistics import median
admin.site.unregister(Group) admin.site.unregister(Group)
admin.site.unregister(User) admin.site.unregister(User)
@ -12,10 +13,11 @@ admin.site.unregister(User)
class ProfileInline(admin.StackedInline): class ProfileInline(admin.StackedInline):
model = Profile model = Profile
can_delete = False can_delete = False
fields = ("avatar_tag", ) fields = ("avatar_tag",)
readonly_fields = ["avatar_tag"] readonly_fields = ["avatar_tag"]
show_change_link = True show_change_link = True
# extended users with avatars # extended users with avatars
@admin.register(User) @admin.register(User)
class EUserAdmin(AdminChangeLinksMixin, UserAdmin): class EUserAdmin(AdminChangeLinksMixin, UserAdmin):
@ -30,14 +32,13 @@ class EUserAdmin(AdminChangeLinksMixin, UserAdmin):
"is_staff", "is_staff",
) )
list_display_links = ("id", "username") list_display_links = ("id", "username")
change_links = ( change_links = ("profile",)
"profile", ordering = ("-id",)
)
ordering = ("-id", )
def avatar_tag(self, obj): def avatar_tag(self, obj):
return obj.profile.avatar_tag() return obj.profile.avatar_tag()
@admin.register(Order) @admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ( list_display = (
@ -79,19 +80,34 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"taker_bond", "taker_bond",
"trade_escrow", "trade_escrow",
) )
list_filter = ("is_disputed", "is_fiat_sent", "is_swap","type", "currency", "status") list_filter = (
search_fields = ["id","amount","min_amount","max_amount"] "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): def maker_wins(self, request, queryset):
''' """
Solves a dispute on favor of the maker. Solves a dispute on favor of the maker.
Adds Sats to compensations (earned_rewards) of the maker profile. Adds Sats to compensations (earned_rewards) of the maker profile.
''' """
for order in queryset: 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 own_bond_sats = order.maker_bond.num_satoshis
if Logics.is_buyer(order, order.maker): if Logics.is_buyer(order, order.maker):
if order.is_swap: 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.earned_rewards = own_bond_sats + trade_sats
order.maker.profile.save() order.maker.profile.save()
order.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: 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): def taker_wins(self, request, queryset):
''' """
Solves a dispute on favor of the taker. Solves a dispute on favor of the taker.
Adds Sats to compensations (earned_rewards) of the taker profile. Adds Sats to compensations (earned_rewards) of the taker profile.
''' """
for order in queryset: 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 own_bond_sats = order.maker_bond.num_satoshis
if Logics.is_buyer(order, order.taker): if Logics.is_buyer(order, order.taker):
if order.is_swap: 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.earned_rewards = own_bond_sats + trade_sats
order.taker.profile.save() order.taker.profile.save()
order.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: 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): def return_everything(self, request, queryset):
''' """
Solves a dispute by pushing back every bond and escrow to their sender. Solves a dispute by pushing back every bond and escrow to their sender.
''' """
for order in queryset: for order in queryset:
if order.status in [Order.Status.DIS, Order.Status.WFR] and order.is_disputed: if (
order.maker_bond.sender.profile.earned_rewards += order.maker_bond.num_satoshis 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.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.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.trade_escrow.sender.profile.save()
order.status = Order.Status.CCA order.status = Order.Status.CCA
order.save() 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: 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): 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. successfully for the set of selected orders.
''' """
times = [] times = []
for order in queryset: for order in queryset:
if order.contract_finalization_time: if order.contract_finalization_time:
timedelta = order.contract_finalization_time - order.last_satoshis_time timedelta = order.contract_finalization_time - order.last_satoshis_time
times.append(timedelta.total_seconds()) times.append(timedelta.total_seconds())
if len(times) > 0: if len(times) > 0:
median_time_secs = median(times) median_time_secs = median(times)
mins = int(median_time_secs/60) mins = int(median_time_secs / 60)
secs = int(median_time_secs - mins*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) self.message_user(
request,
f"The median time to complete the trades is {mins}m {secs}s",
messages.SUCCESS,
)
else: 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): def amt(self, obj):
if obj.has_range and obj.amount == None: 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: else:
return float(obj.amount) return float(obj.amount)
@admin.register(LNPayment) @admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@ -210,8 +271,15 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"order_paid_LN", "order_paid_LN",
) )
list_filter = ("type", "concept", "status") list_filter = ("type", "concept", "status")
ordering = ("-expires_at", ) ordering = ("-expires_at",)
search_fields = ["payment_hash","num_satoshis","sender__username","receiver__username","description"] search_fields = [
"payment_hash",
"num_satoshis",
"sender__username",
"receiver__username",
"description",
]
@admin.register(OnchainPayment) @admin.register(OnchainPayment)
class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@ -231,9 +299,10 @@ class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"balance", "balance",
"order_paid_TX", "order_paid_TX",
) )
list_display_links = ("id","address", "concept") list_display_links = ("id", "address", "concept")
list_filter = ("concept", "status") list_filter = ("concept", "status")
search_fields = ["address","num_satoshis","receiver__username","txid"] search_fields = ["address", "num_satoshis", "receiver__username", "txid"]
@admin.register(Profile) @admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@ -261,7 +330,7 @@ class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display_links = ("avatar_tag", "id") list_display_links = ("avatar_tag", "id")
change_links = ["user"] change_links = ["user"]
readonly_fields = ["avatar_tag"] readonly_fields = ["avatar_tag"]
search_fields = ["user__username","id"] search_fields = ["user__username", "id"]
readonly_fields = ("public_key", "encrypted_private_key") readonly_fields = ("public_key", "encrypted_private_key")
@ -270,13 +339,12 @@ class CurrencieAdmin(admin.ModelAdmin):
list_display = ("id", "currency", "exchange_rate", "timestamp") list_display = ("id", "currency", "exchange_rate", "timestamp")
list_display_links = ("id", "currency") list_display_links = ("id", "currency")
readonly_fields = ("currency", "exchange_rate", "timestamp") readonly_fields = ("currency", "exchange_rate", "timestamp")
ordering = ("id", ) ordering = ("id",)
@admin.register(MarketTick) @admin.register(MarketTick)
class MarketTickAdmin(admin.ModelAdmin): class MarketTickAdmin(admin.ModelAdmin):
list_display = ("timestamp", "price", "volume", "premium", "currency", list_display = ("timestamp", "price", "volume", "premium", "currency", "fee")
"fee") readonly_fields = ("timestamp", "price", "volume", "premium", "currency", "fee")
readonly_fields = ("timestamp", "price", "volume", "premium", "currency",
"fee")
list_filter = ["currency"] list_filter = ["currency"]
ordering = ("-timestamp", ) ordering = ("-timestamp",)

View File

@ -24,8 +24,9 @@ except:
# Read macaroon from file or .env variable string encoded as base64 # Read macaroon from file or .env variable string encoded as base64
try: try:
MACAROON = open(os.path.join(config("LND_DIR"), config("MACAROON_path")), MACAROON = open(
"rb").read() os.path.join(config("LND_DIR"), config("MACAROON_path")), "rb"
).read()
except: except:
MACAROON = b64decode(config("LND_MACAROON_BASE64")) MACAROON = b64decode(config("LND_MACAROON_BASE64"))
@ -49,13 +50,10 @@ class LNNode:
payment_failure_context = { payment_failure_context = {
0: "Payment isn't failed (yet)", 0: "Payment isn't failed (yet)",
1: 1: "There are more routes to try, but the payment timeout was exceeded.",
"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.",
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.", 3: "A non-recoverable error has occured.",
4: 4: "Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
"Payment details incorrect (unknown hash, invalid amt or invalid final cltv delta)",
5: "Insufficient local balance.", 5: "Insufficient local balance.",
} }
@ -63,9 +61,9 @@ class LNNode:
def decode_payreq(cls, invoice): def decode_payreq(cls, invoice):
"""Decodes a lightning payment request (invoice)""" """Decodes a lightning payment request (invoice)"""
request = lnrpc.PayReqString(pay_req=invoice) request = lnrpc.PayReqString(pay_req=invoice)
response = cls.lightningstub.DecodePayReq(request, response = cls.lightningstub.DecodePayReq(
metadata=[("macaroon", request, metadata=[("macaroon", MACAROON.hex())]
MACAROON.hex())]) )
return response return response
@classmethod @classmethod
@ -73,46 +71,56 @@ class LNNode:
"""Returns estimated fee for onchain payouts""" """Returns estimated fee for onchain payouts"""
# We assume segwit. Use robosats donation address as shortcut so there is no need of user inputs # We assume segwit. Use robosats donation address as shortcut so there is no need of user inputs
request = lnrpc.EstimateFeeRequest(AddrToAmount={'bc1q3cpp7ww92n6zp04hv40kd3eyy5avgughx6xqnx':amount_sats}, request = lnrpc.EstimateFeeRequest(
target_conf=target_conf, AddrToAmount={"bc1q3cpp7ww92n6zp04hv40kd3eyy5avgughx6xqnx": amount_sats},
min_confs=min_confs, target_conf=target_conf,
spend_unconfirmed=False) min_confs=min_confs,
spend_unconfirmed=False,
)
response = cls.lightningstub.EstimateFee(request, response = cls.lightningstub.EstimateFee(
metadata=[("macaroon", request, metadata=[("macaroon", MACAROON.hex())]
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 = {} wallet_balance_cache = {}
@ring.dict(wallet_balance_cache, expire=10) # keeps in cache for 10 seconds @ring.dict(wallet_balance_cache, expire=10) # keeps in cache for 10 seconds
@classmethod @classmethod
def wallet_balance(cls): def wallet_balance(cls):
"""Returns onchain balance""" """Returns onchain balance"""
request = lnrpc.WalletBalanceRequest() request = lnrpc.WalletBalanceRequest()
response = cls.lightningstub.WalletBalance(request, response = cls.lightningstub.WalletBalance(
metadata=[("macaroon", request, metadata=[("macaroon", MACAROON.hex())]
MACAROON.hex())]) )
return {'total_balance': response.total_balance, return {
'confirmed_balance': response.confirmed_balance, "total_balance": response.total_balance,
'unconfirmed_balance': response.unconfirmed_balance} "confirmed_balance": response.confirmed_balance,
"unconfirmed_balance": response.unconfirmed_balance,
}
channel_balance_cache = {} channel_balance_cache = {}
@ring.dict(channel_balance_cache, expire=10) # keeps in cache for 10 seconds @ring.dict(channel_balance_cache, expire=10) # keeps in cache for 10 seconds
@classmethod @classmethod
def channel_balance(cls): def channel_balance(cls):
"""Returns channels balance""" """Returns channels balance"""
request = lnrpc.ChannelBalanceRequest() request = lnrpc.ChannelBalanceRequest()
response = cls.lightningstub.ChannelBalance(request, response = cls.lightningstub.ChannelBalance(
metadata=[("macaroon", request, metadata=[("macaroon", MACAROON.hex())]
MACAROON.hex())]) )
return {
return {'local_balance': response.local_balance.sat, "local_balance": response.local_balance.sat,
'remote_balance': response.remote_balance.sat, "remote_balance": response.remote_balance.sat,
'unsettled_local_balance': response.unsettled_local_balance.sat, "unsettled_local_balance": response.unsettled_local_balance.sat,
'unsettled_remote_balance': response.unsettled_remote_balance.sat} "unsettled_remote_balance": response.unsettled_remote_balance.sat,
}
@classmethod @classmethod
def pay_onchain(cls, onchainpayment): def pay_onchain(cls, onchainpayment):
@ -121,15 +129,17 @@ class LNNode:
if config("DISABLE_ONCHAIN", cast=bool): if config("DISABLE_ONCHAIN", cast=bool):
return False return False
request = lnrpc.SendCoinsRequest(addr=onchainpayment.address, request = lnrpc.SendCoinsRequest(
amount=int(onchainpayment.sent_satoshis), addr=onchainpayment.address,
sat_per_vbyte=int(onchainpayment.mining_fee_rate), amount=int(onchainpayment.sent_satoshis),
label=str("Payout order #" + str(onchainpayment.order_paid_TX.id)), sat_per_vbyte=int(onchainpayment.mining_fee_rate),
spend_unconfirmed=True) label=str("Payout order #" + str(onchainpayment.order_paid_TX.id)),
spend_unconfirmed=True,
)
response = cls.lightningstub.SendCoins(request, response = cls.lightningstub.SendCoins(
metadata=[("macaroon", request, metadata=[("macaroon", MACAROON.hex())]
MACAROON.hex())]) )
onchainpayment.txid = response.txid onchainpayment.txid = response.txid
onchainpayment.save() onchainpayment.save()
@ -139,28 +149,27 @@ class LNNode:
@classmethod @classmethod
def cancel_return_hold_invoice(cls, payment_hash): def cancel_return_hold_invoice(cls, payment_hash):
"""Cancels or returns a hold invoice""" """Cancels or returns a hold invoice"""
request = invoicesrpc.CancelInvoiceMsg( request = invoicesrpc.CancelInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
payment_hash=bytes.fromhex(payment_hash)) response = cls.invoicesstub.CancelInvoice(
response = cls.invoicesstub.CancelInvoice(request, request, metadata=[("macaroon", MACAROON.hex())]
metadata=[("macaroon", )
MACAROON.hex())])
# Fix this: tricky because canceling sucessfully an invoice has no response. TODO # Fix this: tricky because canceling sucessfully an invoice has no response. TODO
return str(response) == "" # True if no response, false otherwise. return str(response) == "" # True if no response, false otherwise.
@classmethod @classmethod
def settle_hold_invoice(cls, preimage): def settle_hold_invoice(cls, preimage):
"""settles a hold invoice""" """settles a hold invoice"""
request = invoicesrpc.SettleInvoiceMsg( request = invoicesrpc.SettleInvoiceMsg(preimage=bytes.fromhex(preimage))
preimage=bytes.fromhex(preimage)) response = cls.invoicesstub.SettleInvoice(
response = cls.invoicesstub.SettleInvoice(request, request, metadata=[("macaroon", MACAROON.hex())]
metadata=[("macaroon", )
MACAROON.hex())])
# Fix this: tricky because settling sucessfully an invoice has None response. TODO # Fix this: tricky because settling sucessfully an invoice has None response. TODO
return str(response) == "" # True if no response, false otherwise. return str(response) == "" # True if no response, false otherwise.
@classmethod @classmethod
def gen_hold_invoice(cls, num_satoshis, description, invoice_expiry, def gen_hold_invoice(
cltv_expiry_blocks): cls, num_satoshis, description, invoice_expiry, cltv_expiry_blocks
):
"""Generates hold invoice""" """Generates hold invoice"""
hold_payment = {} 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. ), # actual expiry is padded by 50%, if tight, wrong client system clock will say invoice is expired.
cltv_expiry=cltv_expiry_blocks, cltv_expiry=cltv_expiry_blocks,
) )
response = cls.invoicesstub.AddHoldInvoice(request, response = cls.invoicesstub.AddHoldInvoice(
metadata=[("macaroon", request, metadata=[("macaroon", MACAROON.hex())]
MACAROON.hex())]) )
hold_payment["invoice"] = response.payment_request hold_payment["invoice"] = response.payment_request
payreq_decoded = cls.decode_payreq(hold_payment["invoice"]) payreq_decoded = cls.decode_payreq(hold_payment["invoice"])
hold_payment["preimage"] = preimage.hex() hold_payment["preimage"] = preimage.hex()
hold_payment["payment_hash"] = payreq_decoded.payment_hash hold_payment["payment_hash"] = payreq_decoded.payment_hash
hold_payment["created_at"] = timezone.make_aware( 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( hold_payment["expires_at"] = hold_payment["created_at"] + timedelta(
seconds=payreq_decoded.expiry) seconds=payreq_decoded.expiry
)
hold_payment["cltv_expiry"] = cltv_expiry_blocks hold_payment["cltv_expiry"] = cltv_expiry_blocks
return hold_payment return hold_payment
@ -201,11 +212,11 @@ class LNNode:
from api.models import LNPayment from api.models import LNPayment
request = invoicesrpc.LookupInvoiceMsg( request = invoicesrpc.LookupInvoiceMsg(
payment_hash=bytes.fromhex(lnpayment.payment_hash)) payment_hash=bytes.fromhex(lnpayment.payment_hash)
response = cls.invoicesstub.LookupInvoiceV2(request, )
metadata=[("macaroon", response = cls.invoicesstub.LookupInvoiceV2(
MACAROON.hex()) request, metadata=[("macaroon", MACAROON.hex())]
]) )
# Will fail if 'unable to locate invoice'. Happens if invoice expiry # Will fail if 'unable to locate invoice'. Happens if invoice expiry
# time has passed (but these are 15% padded at the moment). Should catch it # time has passed (but these are 15% padded at the moment). Should catch it
@ -225,11 +236,9 @@ class LNNode:
@classmethod @classmethod
def resetmc(cls): def resetmc(cls):
request = routerrpc.ResetMissionControlRequest() request = routerrpc.ResetMissionControlRequest()
response = cls.routerstub.ResetMissionControl(request, response = cls.routerstub.ResetMissionControl(
metadata=[ request, metadata=[("macaroon", MACAROON.hex())]
("macaroon", )
MACAROON.hex())
])
return True return True
@classmethod @classmethod
@ -258,7 +267,10 @@ class LNNode:
route_hints = payreq_decoded.route_hints route_hints = payreq_decoded.route_hints
# Max amount RoboSats will pay for routing # 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: if route_hints:
routes_cost = [] routes_cost = []
@ -268,15 +280,17 @@ class LNNode:
# ...add up the cost of every hinted hop... # ...add up the cost of every hinted hop...
for hop_hint in hinted_route.hop_hints: for hop_hint in hinted_route.hop_hints:
route_cost += hop_hint.fee_base_msat / 1000 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 # ...and store the cost of the route to the array
routes_cost.append(route_cost) routes_cost.append(route_cost)
# If the cheapest possible private route is more expensive than what RoboSats is willing to pay # 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"] = { 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 return payout
@ -288,16 +302,18 @@ class LNNode:
if not payreq_decoded.num_satoshis == num_satoshis: if not payreq_decoded.num_satoshis == num_satoshis:
payout["context"] = { payout["context"] = {
"bad_invoice": "bad_invoice": "The invoice provided is not for "
"The invoice provided is not for " + + "{:,}".format(num_satoshis)
"{:,}".format(num_satoshis) + " Sats" + " Sats"
} }
return payout return payout
payout["created_at"] = timezone.make_aware( payout["created_at"] = timezone.make_aware(
datetime.fromtimestamp(payreq_decoded.timestamp)) datetime.fromtimestamp(payreq_decoded.timestamp)
)
payout["expires_at"] = payout["created_at"] + timedelta( payout["expires_at"] = payout["created_at"] + timedelta(
seconds=payreq_decoded.expiry) seconds=payreq_decoded.expiry
)
if payout["expires_at"] < timezone.now(): if payout["expires_at"] < timezone.now():
payout["context"] = { payout["context"] = {
@ -315,21 +331,24 @@ class LNNode:
def pay_invoice(cls, lnpayment): def pay_invoice(cls, lnpayment):
"""Sends sats. Used for rewards payouts""" """Sends sats. Used for rewards payouts"""
from api.models import LNPayment from api.models import LNPayment
fee_limit_sat = int( fee_limit_sat = int(
max( max(
lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")), lnpayment.num_satoshis
* float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT_REWARD")),
)) # 200 ppm or 10 sats )
) # 200 ppm or 10 sats
timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS")) timeout_seconds = int(config("REWARDS_TIMEOUT_SECONDS"))
request = routerrpc.SendPaymentRequest(payment_request=lnpayment.invoice, request = routerrpc.SendPaymentRequest(
fee_limit_sat=fee_limit_sat, payment_request=lnpayment.invoice,
timeout_seconds=timeout_seconds) fee_limit_sat=fee_limit_sat,
timeout_seconds=timeout_seconds,
)
for response in cls.routerstub.SendPaymentV2(request, for response in cls.routerstub.SendPaymentV2(
metadata=[("macaroon", request, metadata=[("macaroon", MACAROON.hex())]
MACAROON.hex()) ):
]):
if response.status == 0: # Status 0 'UNKNOWN' if response.status == 0: # Status 0 'UNKNOWN'
# Not sure when this status happens # Not sure when this status happens
@ -354,7 +373,7 @@ class LNNode:
if response.status == 2: # STATUS 'SUCCEEDED' if response.status == 2: # STATUS 'SUCCEEDED'
lnpayment.status = LNPayment.Status.SUCCED 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.preimage = response.payment_preimage
lnpayment.save() lnpayment.save()
return True, None return True, None
@ -364,12 +383,10 @@ class LNNode:
@classmethod @classmethod
def double_check_htlc_is_settled(cls, payment_hash): def double_check_htlc_is_settled(cls, payment_hash):
"""Just as it sounds. Better safe than sorry!""" """Just as it sounds. Better safe than sorry!"""
request = invoicesrpc.LookupInvoiceMsg( request = invoicesrpc.LookupInvoiceMsg(payment_hash=bytes.fromhex(payment_hash))
payment_hash=bytes.fromhex(payment_hash)) response = cls.invoicesstub.LookupInvoiceV2(
response = cls.invoicesstub.LookupInvoiceV2(request, request, metadata=[("macaroon", MACAROON.hex())]
metadata=[("macaroon", )
MACAROON.hex())
])
return ( return (
response.state == 1 response.state == 1

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,8 @@ class Command(BaseCommand):
queryset = Order.objects.exclude(status__in=do_nothing) queryset = Order.objects.exclude(status__in=do_nothing)
queryset = queryset.filter( 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 = {}
debug["num_expired_orders"] = len(queryset) debug["num_expired_orders"] = len(queryset)
@ -45,11 +46,9 @@ class Command(BaseCommand):
debug["reason_failure"] = [] debug["reason_failure"] = []
for idx, order in enumerate(queryset): for idx, order in enumerate(queryset):
context = str(order) + " was " + Order.Status( context = str(order) + " was " + Order.Status(order.status).label
order.status).label
try: try:
if Logics.order_expires( if Logics.order_expires(order): # Order send to expire here
order): # Order send to expire here
debug["expired_orders"].append({idx: context}) debug["expired_orders"].append({idx: context})
# It should not happen, but if it cannot locate the hold invoice # It should not happen, but if it cannot locate the hold invoice
@ -57,7 +56,7 @@ class Command(BaseCommand):
except Exception as e: except Exception as e:
debug["failed_order_expiry"].append({idx: context}) debug["failed_order_expiry"].append({idx: context})
debug["reason_failure"].append({idx: str(e)}) debug["reason_failure"].append({idx: str(e)})
if "unable to locate invoice" in str(e): if "unable to locate invoice" in str(e):
self.stdout.write(str(e)) self.stdout.write(str(e))
order.status = Order.Status.EXP order.status = Order.Status.EXP

View File

@ -73,18 +73,17 @@ class Command(BaseCommand):
try: try:
# this is similar to LNNnode.validate_hold_invoice_locked # this is similar to LNNnode.validate_hold_invoice_locked
request = LNNode.invoicesrpc.LookupInvoiceMsg( request = LNNode.invoicesrpc.LookupInvoiceMsg(
payment_hash=bytes.fromhex(hold_lnpayment.payment_hash)) payment_hash=bytes.fromhex(hold_lnpayment.payment_hash)
response = stub.LookupInvoiceV2(request, )
metadata=[("macaroon", response = stub.LookupInvoiceV2(
MACAROON.hex())]) request, metadata=[("macaroon", MACAROON.hex())]
hold_lnpayment.status = lnd_state_to_lnpayment_status[ )
response.state] hold_lnpayment.status = lnd_state_to_lnpayment_status[response.state]
# try saving expiry height # try saving expiry height
if hasattr(response, "htlcs"): if hasattr(response, "htlcs"):
try: try:
hold_lnpayment.expiry_height = response.htlcs[ hold_lnpayment.expiry_height = response.htlcs[0].expiry_height
0].expiry_height
except: except:
pass pass
@ -97,8 +96,7 @@ class Command(BaseCommand):
# LND restarted. # LND restarted.
if "wallet locked, unlock it" in str(e): if "wallet locked, unlock it" in str(e):
self.stdout.write( self.stdout.write(str(timezone.now()) + " :: Wallet Locked")
str(timezone.now()) + " :: Wallet Locked")
# Other write to logs # Other write to logs
else: else:
self.stdout.write(str(e)) self.stdout.write(str(e))
@ -114,13 +112,15 @@ class Command(BaseCommand):
# Report for debugging # Report for debugging
new_status = LNPayment.Status(hold_lnpayment.status).label new_status = LNPayment.Status(hold_lnpayment.status).label
debug["invoices"].append({ debug["invoices"].append(
idx: { {
"payment_hash": str(hold_lnpayment.payment_hash), idx: {
"old_status": old_status, "payment_hash": str(hold_lnpayment.payment_hash),
"new_status": new_status, "old_status": old_status,
"new_status": new_status,
}
} }
}) )
at_least_one_changed = at_least_one_changed or changed 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], status__in=[LNPayment.Status.VALIDI, LNPayment.Status.FAILRO],
in_flight=False, in_flight=False,
last_routing_time__lt=( 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) queryset = queryset.union(queryset_retries)
@ -167,7 +168,7 @@ class Command(BaseCommand):
# It is a maker bond => Publish order. # It is a maker bond => Publish order.
if hasattr(lnpayment, "order_made"): if hasattr(lnpayment, "order_made"):
Logics.publish_order(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 return
# It is a taker bond => close contract. # It is a taker bond => close contract.

View File

@ -7,50 +7,54 @@ from decouple import config
import requests import requests
import time import time
class Command(BaseCommand): class Command(BaseCommand):
help = "Polls telegram /getUpdates method" help = "Polls telegram /getUpdates method"
rest = 3 # seconds between consecutive polls rest = 3 # seconds between consecutive polls
bot_token = config('TELEGRAM_TOKEN') bot_token = config("TELEGRAM_TOKEN")
updates_url = f'https://api.telegram.org/bot{bot_token}/getUpdates' updates_url = f"https://api.telegram.org/bot{bot_token}/getUpdates"
session = get_session() session = get_session()
telegram = Telegram() telegram = Telegram()
def handle(self, *args, **options): def handle(self, *args, **options):
"""Infinite loop to check for telegram updates. """Infinite loop to check for telegram updates.
If it finds a new user (/start), enables it's taker found If it finds a new user (/start), enables it's taker found
notification and sends a 'Hey {username} {order_id}' message back""" notification and sends a 'Hey {username} {order_id}' message back"""
offset = 0 offset = 0
while True: while True:
time.sleep(self.rest) 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() response = self.session.get(self.updates_url, params=params).json()
if len(list(response['result'])) == 0: if len(list(response["result"])) == 0:
continue continue
for result in response['result']: for result in response["result"]:
try: # if there is no key message, skips this result. try: # if there is no key message, skips this result.
text = result['message']['text'] text = result["message"]["text"]
except: except:
continue continue
splitted_text = text.split(' ') splitted_text = text.split(" ")
if splitted_text[0] == '/start': if splitted_text[0] == "/start":
token = splitted_text[-1] token = splitted_text[-1]
try : try:
profile = Profile.objects.get(telegram_token=token) profile = Profile.objects.get(telegram_token=token)
except: except:
print(f'No profile with token {token}') print(f"No profile with token {token}")
continue continue
attempts = 5 attempts = 5
while attempts >= 0: while attempts >= 0:
try: try:
profile.telegram_chat_id = result['message']['from']['id'] profile.telegram_chat_id = result["message"]["from"]["id"]
profile.telegram_lang_code = result['message']['from']['language_code'] profile.telegram_lang_code = result["message"]["from"][
"language_code"
]
self.telegram.welcome(profile.user) self.telegram.welcome(profile.user)
profile.telegram_enabled = True profile.telegram_enabled = True
profile.save() profile.save()
@ -59,6 +63,4 @@ class Command(BaseCommand):
time.sleep(5) time.sleep(5)
attempts = attempts - 1 attempts = attempts - 1
offset = response['result'][-1]['update_id'] offset = response["result"][-1]["update_id"]

View File

@ -3,37 +3,38 @@ from secrets import token_urlsafe
from api.models import Order from api.models import Order
from api.utils import get_session 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() session = get_session()
site = config('HOST_NAME') site = config("HOST_NAME")
def get_context(user): def get_context(user):
"""returns context needed to enable TG notifications""" """returns context needed to enable TG notifications"""
context = {} context = {}
if user.profile.telegram_enabled : if user.profile.telegram_enabled:
context['tg_enabled'] = True context["tg_enabled"] = True
else: else:
context['tg_enabled'] = False context["tg_enabled"] = False
if user.profile.telegram_token == None: if user.profile.telegram_token == None:
user.profile.telegram_token = token_urlsafe(15) user.profile.telegram_token = token_urlsafe(15)
user.profile.save() user.profile.save()
context['tg_token'] = user.profile.telegram_token context["tg_token"] = user.profile.telegram_token
context['tg_bot_name'] = config("TELEGRAM_BOT_NAME") context["tg_bot_name"] = config("TELEGRAM_BOT_NAME")
return context return context
def send_message(self, user, text): 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 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 # if it fails, it should keep trying
while True: while True:
try: try:
@ -41,13 +42,13 @@ class Telegram():
return return
except: except:
pass pass
def welcome(self, user): def welcome(self, user):
''' User enabled Telegram Notifications''' """User enabled Telegram Notifications"""
lang = user.profile.telegram_lang_code lang = user.profile.telegram_lang_code
if lang == 'es': if lang == "es":
text = f'Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats.' text = f"Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats."
else: else:
text = f"Hey {user.username}, I will send you notifications about your RoboSats orders." text = f"Hey {user.username}, I will send you notifications about your RoboSats orders."
self.send_message(user, text) self.send_message(user, text)
@ -75,18 +76,18 @@ class Telegram():
def order_taken_confirmed(self, order): def order_taken_confirmed(self, order):
if order.maker.profile.telegram_enabled: if order.maker.profile.telegram_enabled:
lang = order.maker.profile.telegram_lang_code lang = order.maker.profile.telegram_lang_code
if lang == 'es': 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.' 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: 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) self.send_message(order.maker, text)
if order.taker.profile.telegram_enabled: if order.taker.profile.telegram_enabled:
lang = order.taker.profile.telegram_lang_code lang = order.taker.profile.telegram_lang_code
if lang == 'es': if lang == "es":
text = f'Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}.' text = f"Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}."
else: 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) self.send_message(order.taker, text)
return return
@ -95,20 +96,20 @@ class Telegram():
for user in [order.maker, order.taker]: for user in [order.maker, order.taker]:
if user.profile.telegram_enabled: if user.profile.telegram_enabled:
lang = user.profile.telegram_lang_code lang = user.profile.telegram_lang_code
if lang == 'es': 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.' 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: 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) self.send_message(user, text)
return return
def order_expired_untaken(self, order): def order_expired_untaken(self, order):
if order.maker.profile.telegram_enabled: if order.maker.profile.telegram_enabled:
lang = order.maker.profile.telegram_lang_code lang = order.maker.profile.telegram_lang_code
if lang == 'es': 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.' 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: 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) self.send_message(order.maker, text)
return return
@ -116,42 +117,42 @@ class Telegram():
for user in [order.maker, order.taker]: for user in [order.maker, order.taker]:
if user.profile.telegram_enabled: if user.profile.telegram_enabled:
lang = user.profile.telegram_lang_code lang = user.profile.telegram_lang_code
if lang == 'es': 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.' text = f"¡Tu orden con ID {order.id} ha finalizado exitosamente!⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar."
else: 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) self.send_message(user, text)
return return
def public_order_cancelled(self, order): def public_order_cancelled(self, order):
if order.maker.profile.telegram_enabled: if order.maker.profile.telegram_enabled:
lang = order.maker.profile.telegram_lang_code lang = order.maker.profile.telegram_lang_code
if lang == 'es': if lang == "es":
text = f'Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}.' text = f"Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}."
else: 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) self.send_message(order.maker, text)
return return
def collaborative_cancelled(self, order): def collaborative_cancelled(self, order):
for user in [order.maker, order.taker]: for user in [order.maker, order.taker]:
if user.profile.telegram_enabled: if user.profile.telegram_enabled:
lang = user.profile.telegram_lang_code lang = user.profile.telegram_lang_code
if lang == 'es': if lang == "es":
text = f'Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente.' text = f"Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente."
else: 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) self.send_message(user, text)
return return
def dispute_opened(self, order): def dispute_opened(self, order):
for user in [order.maker, order.taker]: for user in [order.maker, order.taker]:
if user.profile.telegram_enabled: if user.profile.telegram_enabled:
lang = user.profile.telegram_lang_code lang = user.profile.telegram_lang_code
if lang == 'es': if lang == "es":
text = f'Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa.' text = f"Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa."
else: 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) self.send_message(user, text)
return return
@ -163,8 +164,8 @@ class Telegram():
if len(queryset) == 0: if len(queryset) == 0:
return return
order = queryset.last() order = queryset.last()
if lang == 'es': if lang == "es":
text = f'Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes.' text = f"Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes."
else: else:
text = f"Hey {order.maker.username}, your order with ID {str(order.id)} is public in the order book." 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) self.send_message(order.maker, text)

View File

@ -29,12 +29,11 @@ DEFAULT_BOND_SIZE = float(config("DEFAULT_BOND_SIZE"))
class Currency(models.Model): class Currency(models.Model):
currency_dict = json.load(open("frontend/static/assets/currencies.json")) currency_dict = json.load(open("frontend/static/assets/currencies.json"))
currency_choices = [(int(val), label) currency_choices = [(int(val), label) for val, label in list(currency_dict.items())]
for val, label in list(currency_dict.items())]
currency = models.PositiveSmallIntegerField(choices=currency_choices, currency = models.PositiveSmallIntegerField(
null=False, choices=currency_choices, null=False, unique=True
unique=True) )
exchange_rate = models.DecimalField( exchange_rate = models.DecimalField(
max_digits=14, max_digits=14,
decimal_places=4, decimal_places=4,
@ -54,7 +53,6 @@ class Currency(models.Model):
class LNPayment(models.Model): class LNPayment(models.Model):
class Types(models.IntegerChoices): class Types(models.IntegerChoices):
NORM = 0, "Regular invoice" NORM = 0, "Regular invoice"
HOLD = 1, "hold invoice" HOLD = 1, "hold invoice"
@ -80,77 +78,78 @@ class LNPayment(models.Model):
class FailureReason(models.IntegerChoices): class FailureReason(models.IntegerChoices):
NOTYETF = 0, "Payment isn't failed (yet)" NOTYETF = 0, "Payment isn't failed (yet)"
TIMEOUT = 1, "There are more routes to try, but the payment timeout was exceeded." TIMEOUT = (
NOROUTE = 2, "All possible routes were tried and failed permanently. Or there were no routes to the destination at all." 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." 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." NOBALAN = 5, "Insufficient unlocked balance in RoboSats' node."
# payment use details # payment use details
type = models.PositiveSmallIntegerField(choices=Types.choices, type = models.PositiveSmallIntegerField(
null=False, choices=Types.choices, null=False, default=Types.HOLD
default=Types.HOLD) )
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, concept = models.PositiveSmallIntegerField(
null=False, choices=Concepts.choices, null=False, default=Concepts.MAKEBOND
default=Concepts.MAKEBOND) )
status = models.PositiveSmallIntegerField(choices=Status.choices, status = models.PositiveSmallIntegerField(
null=False, choices=Status.choices, null=False, default=Status.INVGEN
default=Status.INVGEN) )
failure_reason = models.PositiveSmallIntegerField(choices=FailureReason.choices, failure_reason = models.PositiveSmallIntegerField(
null=True, choices=FailureReason.choices, null=True, default=None
default=None) )
# payment info # payment info
payment_hash = models.CharField(max_length=100, payment_hash = models.CharField(
unique=True, max_length=100, unique=True, default=None, blank=True, primary_key=True
default=None, )
blank=True,
primary_key=True)
invoice = models.CharField( invoice = models.CharField(
max_length=1200, unique=True, null=True, default=None, max_length=1200, unique=True, null=True, default=None, blank=True
blank=True) # Some invoices with lots of routing hints might be long ) # Some invoices with lots of routing hints might be long
preimage = models.CharField(max_length=64, preimage = models.CharField(
unique=True, max_length=64, unique=True, null=True, default=None, blank=True
null=True, )
default=None, description = models.CharField(
blank=True) max_length=500, unique=False, null=True, default=None, blank=True
description = models.CharField(max_length=500, )
unique=False, num_satoshis = models.PositiveBigIntegerField(
null=True, validators=[
default=None, MinValueValidator(100),
blank=True) MaxValueValidator(1.5 * MAX_TRADE),
num_satoshis = models.PositiveBigIntegerField(validators=[ ]
MinValueValidator(100), )
MaxValueValidator(1.5 * MAX_TRADE),
])
# Fee in sats with mSats decimals fee_msat # Fee in sats with mSats decimals fee_msat
fee = models.DecimalField(max_digits=10, decimal_places=3, default=0, null=False, blank=False) fee = models.DecimalField(
max_digits=10, decimal_places=3, default=0, null=False, blank=False
)
created_at = models.DateTimeField() created_at = models.DateTimeField()
expires_at = models.DateTimeField() expires_at = models.DateTimeField()
cltv_expiry = models.PositiveSmallIntegerField(null=True, cltv_expiry = models.PositiveSmallIntegerField(null=True, default=None, blank=True)
default=None, expiry_height = models.PositiveBigIntegerField(null=True, default=None, blank=True)
blank=True)
expiry_height = models.PositiveBigIntegerField(null=True,
default=None,
blank=True)
# routing # routing
routing_attempts = models.PositiveSmallIntegerField(null=False, default=0) routing_attempts = models.PositiveSmallIntegerField(null=False, default=0)
last_routing_time = models.DateTimeField(null=True, last_routing_time = models.DateTimeField(null=True, default=None, blank=True)
default=None,
blank=True)
in_flight = models.BooleanField(default=False, null=False, blank=False) in_flight = models.BooleanField(default=False, null=False, blank=False)
# involved parties # involved parties
sender = models.ForeignKey(User, sender = models.ForeignKey(
related_name="sender", User, related_name="sender", on_delete=models.SET_NULL, null=True, default=None
on_delete=models.SET_NULL, )
null=True, receiver = models.ForeignKey(
default=None) User,
receiver = models.ForeignKey(User, related_name="receiver",
related_name="receiver", on_delete=models.SET_NULL,
on_delete=models.SET_NULL, null=True,
null=True, default=None,
default=None) )
def __str__(self): def __str__(self):
return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}" 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' # We created a truncated property for display 'hash'
return truncatechars(self.payment_hash, 10) return truncatechars(self.payment_hash, 10)
class OnchainPayment(models.Model):
class OnchainPayment(models.Model):
class Concepts(models.IntegerChoices): class Concepts(models.IntegerChoices):
PAYBUYER = 3, "Payment to buyer" PAYBUYER = 3, "Payment to buyer"
class Status(models.IntegerChoices): class Status(models.IntegerChoices):
CREAT = 0, "Created" # User was given platform fees and suggested mining fees CREAT = 0, "Created" # User was given platform fees and suggested mining fees
VALID = 1, "Valid" # Valid onchain address submitted VALID = 1, "Valid" # Valid onchain address submitted
MEMPO = 2, "In mempool" # Tx is sent to mempool MEMPO = 2, "In mempool" # Tx is sent to mempool
CONFI = 3, "Confirmed" # Tx is confirme +2 blocks CONFI = 3, "Confirmed" # Tx is confirme +2 blocks
CANCE = 4, "Cancelled" # Cancelled tx CANCE = 4, "Cancelled" # Cancelled tx
def get_balance(): def get_balance():
balance = BalanceLog.objects.create() balance = BalanceLog.objects.create()
return balance.time return balance.time
# payment use details # payment use details
concept = models.PositiveSmallIntegerField(choices=Concepts.choices, concept = models.PositiveSmallIntegerField(
null=False, choices=Concepts.choices, null=False, default=Concepts.PAYBUYER
default=Concepts.PAYBUYER) )
status = models.PositiveSmallIntegerField(choices=Status.choices, status = models.PositiveSmallIntegerField(
null=False, choices=Status.choices, null=False, default=Status.CREAT
default=Status.CREAT) )
# payment info # payment info
address = models.CharField(max_length=100, address = models.CharField(
unique=False, max_length=100, unique=False, default=None, null=True, blank=True
default=None, )
null=True,
blank=True)
txid = models.CharField(max_length=64,
unique=True,
null=True,
default=None,
blank=True)
num_satoshis = models.PositiveBigIntegerField(null=True, txid = models.CharField(
validators=[ max_length=64, unique=True, null=True, default=None, blank=True
MinValueValidator(0.5 * MIN_SWAP_AMOUNT), )
MaxValueValidator(1.5 * MAX_TRADE),
]) num_satoshis = models.PositiveBigIntegerField(
sent_satoshis = models.PositiveBigIntegerField(null=True, null=True,
validators=[ validators=[
MinValueValidator(0.5 * MIN_SWAP_AMOUNT), MinValueValidator(0.5 * MIN_SWAP_AMOUNT),
MaxValueValidator(1.5 * MAX_TRADE), 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 # fee in sats/vbyte with mSats decimals fee_msat
suggested_mining_fee_rate = models.DecimalField(max_digits=6, suggested_mining_fee_rate = models.DecimalField(
decimal_places=3, max_digits=6, decimal_places=3, default=1.05, null=False, blank=False
default=1.05, )
null=False, mining_fee_rate = models.DecimalField(
blank=False) max_digits=6, decimal_places=3, default=1.05, null=False, blank=False
mining_fee_rate = models.DecimalField(max_digits=6, )
decimal_places=3, mining_fee_sats = models.PositiveBigIntegerField(default=0, null=False, blank=False)
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 # platform onchain/channels balance at creation, swap fee rate as percent of total volume
balance = models.ForeignKey(BalanceLog, balance = models.ForeignKey(
related_name="balance", BalanceLog,
on_delete=models.SET_NULL, related_name="balance",
null=True, on_delete=models.SET_NULL,
default=get_balance) null=True,
default=get_balance,
)
swap_fee_rate = models.DecimalField(max_digits=4, swap_fee_rate = models.DecimalField(
decimal_places=2, max_digits=4,
default=float(config("MIN_SWAP_FEE"))*100, decimal_places=2,
null=False, default=float(config("MIN_SWAP_FEE")) * 100,
blank=False) null=False,
blank=False,
)
created_at = models.DateTimeField(default=timezone.now) created_at = models.DateTimeField(default=timezone.now)
# involved parties # involved parties
receiver = models.ForeignKey(User, receiver = models.ForeignKey(
related_name="tx_receiver", User,
on_delete=models.SET_NULL, related_name="tx_receiver",
null=True, on_delete=models.SET_NULL,
default=None) null=True,
default=None,
)
def __str__(self): def __str__(self):
return f"TX-{str(self.id)}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}" 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 # Display txid as 'hash' truncated
return truncatechars(self.txid, 10) return truncatechars(self.txid, 10)
class Order(models.Model):
class Order(models.Model):
class Types(models.IntegerChoices): class Types(models.IntegerChoices):
BUY = 0, "BUY" BUY = 0, "BUY"
SELL = 1, "SELL" SELL = 1, "SELL"
@ -297,30 +296,30 @@ class Order(models.Model):
NESINV = 4, "Neither escrow locked or invoice submitted" NESINV = 4, "Neither escrow locked or invoice submitted"
# order info # order info
reference = models.UUIDField(default = uuid.uuid4, editable = False) reference = models.UUIDField(default=uuid.uuid4, editable=False)
status = models.PositiveSmallIntegerField(choices=Status.choices, status = models.PositiveSmallIntegerField(
null=False, choices=Status.choices, null=False, default=Status.WFB
default=Status.WFB) )
created_at = models.DateTimeField(default=timezone.now) created_at = models.DateTimeField(default=timezone.now)
expires_at = models.DateTimeField() expires_at = models.DateTimeField()
expiry_reason = models.PositiveSmallIntegerField(choices=ExpiryReasons.choices, expiry_reason = models.PositiveSmallIntegerField(
null=True, choices=ExpiryReasons.choices, null=True, blank=True, default=None
blank=True, )
default=None)
# order details # order details
type = models.PositiveSmallIntegerField(choices=Types.choices, null=False) type = models.PositiveSmallIntegerField(choices=Types.choices, null=False)
currency = models.ForeignKey(Currency, currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
null=True,
on_delete=models.SET_NULL)
amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True) amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True)
has_range = models.BooleanField(default=False, null=False, blank=False) has_range = models.BooleanField(default=False, null=False, blank=False)
min_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True) min_amount = models.DecimalField(
max_amount = models.DecimalField(max_digits=18, decimal_places=8, null=True, blank=True) max_digits=18, decimal_places=8, null=True, blank=True
payment_method = models.CharField(max_length=70, )
null=False, max_amount = models.DecimalField(
default="not specified", max_digits=18, decimal_places=8, null=True, blank=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) 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. # order pricing method. A explicit amount of sats, or a relative premium above/below market.
is_explicit = models.BooleanField(default=False, null=False) is_explicit = models.BooleanField(default=False, null=False)
@ -330,37 +329,37 @@ class Order(models.Model):
decimal_places=2, decimal_places=2,
default=0, default=0,
null=True, null=True,
validators=[MinValueValidator(-100), validators=[MinValueValidator(-100), MaxValueValidator(999)],
MaxValueValidator(999)],
blank=True, blank=True,
) )
# explicit # explicit
satoshis = models.PositiveBigIntegerField( satoshis = models.PositiveBigIntegerField(
null=True, null=True,
validators=[ validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)],
MinValueValidator(MIN_TRADE),
MaxValueValidator(MAX_TRADE)
],
blank=True, blank=True,
) )
# optionally makers can choose the public order duration length (seconds) # optionally makers can choose the public order duration length (seconds)
public_duration = models.PositiveBigIntegerField( 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, null=False,
validators=[ validators=[
MinValueValidator(60*60*float(config("MIN_PUBLIC_ORDER_DURATION"))), # Min is 10 minutes MinValueValidator(
MaxValueValidator(60*60*float(config("MAX_PUBLIC_ORDER_DURATION"))), # Max is 24 Hours 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, blank=False,
) )
# optionally makers can choose the escrow lock / invoice submission step length (seconds) # optionally makers can choose the escrow lock / invoice submission step length (seconds)
escrow_duration = models.PositiveBigIntegerField( escrow_duration = models.PositiveBigIntegerField(
default=60 * int(config("INVOICE_AND_ESCROW_DURATION"))-1, default=60 * int(config("INVOICE_AND_ESCROW_DURATION")) - 1,
null=False, null=False,
validators=[ validators=[
MinValueValidator(60*30), # Min is 30 minutes MinValueValidator(60 * 30), # Min is 30 minutes
MaxValueValidator(60*60*8), # Max is 8 Hours MaxValueValidator(60 * 60 * 8), # Max is 8 Hours
], ],
blank=False, blank=False,
) )
@ -372,8 +371,8 @@ class Order(models.Model):
default=DEFAULT_BOND_SIZE, default=DEFAULT_BOND_SIZE,
null=False, null=False,
validators=[ validators=[
MinValueValidator(float(config("MIN_BOND_SIZE"))), # 1 % MinValueValidator(float(config("MIN_BOND_SIZE"))), # 1 %
MaxValueValidator(float(config("MAX_BOND_SIZE"))), # 15 % MaxValueValidator(float(config("MAX_BOND_SIZE"))), # 15 %
], ],
blank=False, blank=False,
) )
@ -381,29 +380,24 @@ class Order(models.Model):
# how many sats at creation and at last check (relevant for marked to market) # how many sats at creation and at last check (relevant for marked to market)
t0_satoshis = models.PositiveBigIntegerField( t0_satoshis = models.PositiveBigIntegerField(
null=True, null=True,
validators=[ validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)],
MinValueValidator(MIN_TRADE),
MaxValueValidator(MAX_TRADE)
],
blank=True, blank=True,
) # sats at creation ) # sats at creation
last_satoshis = models.PositiveBigIntegerField( last_satoshis = models.PositiveBigIntegerField(
null=True, null=True,
validators=[MinValueValidator(0), validators=[MinValueValidator(0), MaxValueValidator(MAX_TRADE * 2)],
MaxValueValidator(MAX_TRADE * 2)],
blank=True, blank=True,
) # sats last time checked. Weird if 2* trade max... ) # sats last time checked. Weird if 2* trade max...
# timestamp of last_satoshis # timestamp of last_satoshis
last_satoshis_time = models.DateTimeField(null=True, default=None, blank=True) last_satoshis_time = models.DateTimeField(null=True, default=None, blank=True)
# time the fiat exchange is confirmed and Sats released to buyer # 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 # order participants
maker = models.ForeignKey( maker = models.ForeignKey(
User, User, related_name="maker", on_delete=models.SET_NULL, null=True, default=None
related_name="maker", ) # unique = True, a maker can only make one order
on_delete=models.SET_NULL,
null=True,
default=None) # unique = True, a maker can only make one order
taker = models.ForeignKey( taker = models.ForeignKey(
User, User,
related_name="taker", related_name="taker",
@ -416,21 +410,19 @@ class Order(models.Model):
taker_last_seen = models.DateTimeField(null=True, default=None, blank=True) taker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
# When collaborative cancel is needed and one partner has cancelled. # 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) taker_asked_cancel = models.BooleanField(default=False, null=False)
is_fiat_sent = models.BooleanField(default=False, null=False) is_fiat_sent = models.BooleanField(default=False, null=False)
# in dispute # in dispute
is_disputed = models.BooleanField(default=False, null=False) is_disputed = models.BooleanField(default=False, null=False)
maker_statement = models.TextField(max_length=5000, maker_statement = models.TextField(
null=True, max_length=5000, null=True, default=None, blank=True
default=None, )
blank=True) taker_statement = models.TextField(
taker_statement = models.TextField(max_length=5000, max_length=5000, null=True, default=None, blank=True
null=True, )
default=None,
blank=True)
# LNpayments # LNpayments
# Order collateral # Order collateral
@ -487,7 +479,7 @@ class Order(models.Model):
def __str__(self): def __str__(self):
if self.has_range and self.amount == None: 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: else:
amt = float(self.amount) amt = float(self.amount)
return f"Order {self.id}: {self.Types(self.type).label} BTC for {amt} {self.currency}" 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): def t_to_expire(self, status):
t_to_expire = { t_to_expire = {
0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond' 0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
1: self.public_duration, # 'Public' 1: self.public_duration, # 'Public'
2: 0, # 'Deleted' 2: 0, # 'Deleted'
3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond' 3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
4: 0, # 'Cancelled' 4: 0, # 'Cancelled'
5: 0, # 'Expired' 5: 0, # 'Expired'
6: int(self.escrow_duration), # 'Waiting for trade collateral and buyer invoice' 6: int(
7: int(self.escrow_duration), # 'Waiting only for seller trade collateral' self.escrow_duration
8: int(self.escrow_duration), # 'Waiting only for buyer invoice' ), # 'Waiting for trade collateral and buyer invoice'
9: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom' 7: int(self.escrow_duration), # 'Waiting only for seller trade collateral'
10: 60 * 60 * int(config("FIAT_EXCHANGE_DURATION")),# 'Fiat sent - In chatroom' 8: int(self.escrow_duration), # 'Waiting only for buyer invoice'
11: 1 * 24 * 60 * 60, # 'In dispute' 9: 60
12: 0, # 'Collaboratively cancelled' * 60
13: 100 * 24 * 60 * 60, # 'Sending satoshis to buyer' * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
14: 100 * 24 * 60 * 60, # 'Sucessful trade' 10: 60
15: 100 * 24 * 60 * 60, # 'Failed lightning network routing' * 60
16: 100 * 24 * 60 * 60, # 'Wait for dispute resolution' * int(config("FIAT_EXCHANGE_DURATION")), # 'Fiat sent - In chatroom'
17: 100 * 24 * 60 * 60, # 'Maker lost dispute' 11: 1 * 24 * 60 * 60, # 'In dispute'
18: 100 * 24 * 60 * 60, # 'Taker lost 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] return t_to_expire[status]
@ -570,53 +568,27 @@ class Profile(models.Model):
decimal_places=1, decimal_places=1,
default=None, default=None,
null=True, null=True,
validators=[MinValueValidator(0), validators=[MinValueValidator(0), MaxValueValidator(100)],
MaxValueValidator(100)],
blank=True, blank=True,
) )
# Used to deep link telegram chat in case telegram notifications are enabled # Used to deep link telegram chat in case telegram notifications are enabled
telegram_token = models.CharField( telegram_token = models.CharField(max_length=20, null=True, blank=True)
max_length=20, telegram_chat_id = models.BigIntegerField(null=True, default=None, blank=True)
null=True, telegram_enabled = models.BooleanField(default=False, null=False)
blank=True telegram_lang_code = models.CharField(max_length=10, null=True, blank=True)
) telegram_welcomed = models.BooleanField(default=False, null=False)
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 # Referral program
is_referred = models.BooleanField( is_referred = models.BooleanField(default=False, null=False)
default=False,
null=False
)
referred_by = models.ForeignKey( referred_by = models.ForeignKey(
'self', "self",
related_name="referee", related_name="referee",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
default=None, default=None,
blank=True, blank=True,
) )
referral_code = models.CharField( referral_code = models.CharField(max_length=15, null=True, blank=True)
max_length=15,
null=True,
blank=True
)
# Recent rewards from referred trades that will be "earned" at a later point to difficult spionage. # Recent rewards from referred trades that will be "earned" at a later point to difficult spionage.
pending_rewards = models.PositiveIntegerField(null=False, default=0) pending_rewards = models.PositiveIntegerField(null=False, default=0)
# Claimable rewards # 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 (only used then taking/cancelling repeatedly orders in the book before comitting bond)
penalty_expiration = models.DateTimeField(null=True, penalty_expiration = models.DateTimeField(null=True, default=None, blank=True)
default=None,
blank=True)
# Platform rate # Platform rate
platform_rating = models.PositiveIntegerField(null=True, platform_rating = models.PositiveIntegerField(null=True, default=None, blank=True)
default=None,
blank=True)
# Stealth invoices # Stealth invoices
wants_stealth = models.BooleanField(default=True, wants_stealth = models.BooleanField(default=True, null=False)
null=False)
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs): def create_user_profile(sender, instance, created, **kwargs):
@ -669,8 +636,9 @@ class Profile(models.Model):
@receiver(pre_delete, sender=User) @receiver(pre_delete, sender=User)
def del_avatar_from_disk(sender, instance, **kwargs): def del_avatar_from_disk(sender, instance, **kwargs):
try: try:
avatar_file = Path(settings.AVATAR_ROOT + avatar_file = Path(
instance.profile.avatar.url.split("/")[-1]) settings.AVATAR_ROOT + instance.profile.avatar.url.split("/")[-1]
)
avatar_file.unlink() avatar_file.unlink()
except: except:
pass pass
@ -686,8 +654,7 @@ class Profile(models.Model):
# method to create a fake table field in read only mode # method to create a fake table field in read only mode
def avatar_tag(self): def avatar_tag(self):
return mark_safe('<img src="%s" width="50" height="50" />' % return mark_safe('<img src="%s" width="50" height="50" />' % self.get_avatar())
self.get_avatar())
class MarketTick(models.Model): class MarketTick(models.Model):
@ -723,13 +690,10 @@ class MarketTick(models.Model):
decimal_places=2, decimal_places=2,
default=None, default=None,
null=True, null=True,
validators=[MinValueValidator(-100), validators=[MinValueValidator(-100), MaxValueValidator(999)],
MaxValueValidator(999)],
blank=True, blank=True,
) )
currency = models.ForeignKey(Currency, currency = models.ForeignKey(Currency, null=True, on_delete=models.SET_NULL)
null=True,
on_delete=models.SET_NULL)
timestamp = models.DateTimeField(default=timezone.now) timestamp = models.DateTimeField(default=timezone.now)
# Relevant to keep record of the historical fee, so the insight on the premium can be better analyzed # 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, max_digits=4,
decimal_places=4, decimal_places=4,
default=FEE, default=FEE,
validators=[MinValueValidator(0), validators=[MinValueValidator(0), MaxValueValidator(1)],
MaxValueValidator(1)],
) )
def log_a_tick(order): def log_a_tick(order):
@ -755,10 +718,9 @@ class MarketTick(models.Model):
market_exchange_rate = float(order.currency.exchange_rate) market_exchange_rate = float(order.currency.exchange_rate)
premium = 100 * (price / market_exchange_rate - 1) premium = 100 * (price / market_exchange_rate - 1)
tick = MarketTick.objects.create(price=price, tick = MarketTick.objects.create(
volume=volume, price=price, volume=volume, premium=premium, currency=order.currency
premium=premium, )
currency=order.currency)
tick.save() tick.save()
@ -767,4 +729,4 @@ class MarketTick(models.Model):
class Meta: class Meta:
verbose_name = "Market tick" verbose_name = "Market tick"
verbose_name_plural = "Market ticks" verbose_name_plural = "Market ticks"

View File

@ -4823,8 +4823,7 @@ adjectives = [
"Vindictive", "Vindictive",
"Chatting", "Chatting",
"Nightmarish", "Nightmarish",
"Niggardly" "Niggardly" "Hated",
"Hated",
"Satiric", "Satiric",
"Shattering", "Shattering",
"Fabled", "Fabled",

View File

@ -2,6 +2,7 @@ from .utils import human_format
import hashlib import hashlib
import time import time
""" """
Deterministic nick generator from SHA256 hash. Deterministic nick generator from SHA256 hash.
@ -15,7 +16,6 @@ is a total of to 450*4800*12500*1000 =
class NickGenerator: class NickGenerator:
def __init__( def __init__(
self, self,
lang="English", lang="English",
@ -43,11 +43,13 @@ class NickGenerator:
raise ValueError("Language not implemented.") raise ValueError("Language not implemented.")
if verbose: if verbose:
print(f"{lang} SHA256 Nick Generator initialized with:" + print(
f"\nUp to {len(adverbs)} adverbs." + f"{lang} SHA256 Nick Generator initialized with:"
f"\nUp to {len(adjectives)} adjectives." + + f"\nUp to {len(adverbs)} adverbs."
f"\nUp to {len(nouns)} nouns." + + f"\nUp to {len(adjectives)} adjectives."
f"\nUp to {max_num+1} numerics.\n") + f"\nUp to {len(nouns)} nouns."
+ f"\nUp to {max_num+1} numerics.\n"
)
self.use_adv = use_adv self.use_adv = use_adv
self.use_adj = use_adj self.use_adj = use_adj
@ -147,10 +149,7 @@ class NickGenerator:
i = i + 1 i = i + 1
return "", 0, 0, i return "", 0, 0, i
def compute_pool_size_loss(self, def compute_pool_size_loss(self, max_length=22, max_iter=1000000, num_runs=5000):
max_length=22,
max_iter=1000000,
num_runs=5000):
""" """
Computes median an average loss of Computes median an average loss of
nick pool diversity due to max_lenght nick pool diversity due to max_lenght
@ -196,13 +195,16 @@ if __name__ == "__main__":
# Generates a short nick with length limit from SHA256 # Generates a short nick with length limit from SHA256
nick, nick_id, pool_size, iterations = GenNick.short_from_SHA256( nick, nick_id, pool_size, iterations = GenNick.short_from_SHA256(
hash, max_length, max_iter) hash, max_length, max_iter
)
# Output # Output
print(f"Nick number {nick_id} has been selected among" + print(
f" {human_format(pool_size)} possible nicks.\n" + f"Nick number {nick_id} has been selected among"
f"Needed {iterations} iterations to find one " + + f" {human_format(pool_size)} possible nicks.\n"
f"this short.\nYour nick is {nick} !\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 lenght is {len(nick)} characters.")
print(f"Nick landed at height {nick_id/(pool_size+1)} on the pool.") print(f"Nick landed at height {nick_id/(pool_size+1)} on the pool.")
print(f"Took {time.time()-t0} secs.\n") print(f"Took {time.time()-t0} secs.\n")
@ -216,9 +218,8 @@ if __name__ == "__main__":
string = str(random.uniform(0, 1000000)) string = str(random.uniform(0, 1000000))
hash = hashlib.sha256(str.encode(string)).hexdigest() hash = hashlib.sha256(str.encode(string)).hexdigest()
print( print(
GenNick.short_from_SHA256(hash, GenNick.short_from_SHA256(hash, max_length=max_length, max_iter=max_iter)[0]
max_length=max_length, )
max_iter=max_iter)[0])
# Other analysis # Other analysis
GenNick.compute_pool_size_loss(max_length, max_iter, 200) GenNick.compute_pool_size_loss(max_length, max_iter, 200)

View File

@ -2,9 +2,7 @@ from math import log, floor
def human_format(number): def human_format(number):
units = [ units = ["", " Thousand", " Million", " Billion", " Trillion", " Quatrillion"]
"", " Thousand", " Million", " Billion", " Trillion", " Quatrillion"
]
k = 1000.0 k = 1000.0
magnitude = int(floor(log(number, k))) magnitude = int(floor(log(number, k)))
return "%.2f%s" % (number / k**magnitude, units[magnitude]) return "%.2f%s" % (number / k**magnitude, units[magnitude])

View File

@ -3,17 +3,26 @@ from .models import MarketTick, Order
from decouple import config from decouple import config
RETRY_TIME = int(config("RETRY_TIME")) RETRY_TIME = int(config("RETRY_TIME"))
MIN_PUBLIC_ORDER_DURATION_SECS=60*60*float(config("MIN_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")) MAX_PUBLIC_ORDER_DURATION_SECS = 60 * 60 * float(config("MAX_PUBLIC_ORDER_DURATION"))
class InfoSerializer(serializers.Serializer): class InfoSerializer(serializers.Serializer):
num_public_buy_orders = serializers.IntegerField() num_public_buy_orders = serializers.IntegerField()
num_public_sell_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() 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_nonkyc_btc_premium = serializers.FloatField(
last_day_volume = serializers.FloatField(help_text='Total volume in BTC in the last 24h') help_text="Average premium (weighted by volume) of the orders in the last 24h"
lifetime_volume = serializers.FloatField(help_text='Total volume in BTC since exchange\'s inception') )
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() lnd_version = serializers.CharField()
robosats_running_commit_hash = serializers.CharField() robosats_running_commit_hash = serializers.CharField()
alternative_site = serializers.CharField() alternative_site = serializers.CharField()
@ -21,17 +30,20 @@ class InfoSerializer(serializers.Serializer):
node_alias = serializers.CharField() node_alias = serializers.CharField()
node_id = serializers.CharField() node_id = serializers.CharField()
network = serializers.CharField() network = serializers.CharField()
maker_fee = serializers.FloatField(help_text='Exchange\'s set maker fee') maker_fee = serializers.FloatField(help_text="Exchange's set maker fee")
taker_fee = serializers.FloatField(help_text='Exchange\'s set taker fee ') taker_fee = serializers.FloatField(help_text="Exchange's set taker fee ")
bond_size = serializers.FloatField(help_text='Default bond size (percent)') 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)') current_swap_fee_rate = serializers.FloatField(
nickname = serializers.CharField(help_text='Currenlty logged in Robot name') help_text="Swap fees to perform on-chain transaction (percent)"
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') 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 ListOrderSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Order model = Order
fields = ( fields = (
@ -53,51 +65,46 @@ class ListOrderSerializer(serializers.ModelSerializer):
"maker", "maker",
"taker", "taker",
"escrow_duration", "escrow_duration",
"bond_size" "bond_size",
) )
# Only used in oas_schemas # Only used in oas_schemas
class SummarySerializer(serializers.Serializer): class SummarySerializer(serializers.Serializer):
sent_fiat = serializers.IntegerField( sent_fiat = serializers.IntegerField(
required=False, required=False, help_text="same as `amount` (only for buyer)"
help_text="same as `amount` (only for buyer)"
) )
received_sats = serializers.IntegerField( received_sats = serializers.IntegerField(
required=False, required=False, help_text="same as `trade_satoshis` (only for buyer)"
help_text="same as `trade_satoshis` (only for buyer)"
) )
is_swap = serializers.BooleanField( is_swap = serializers.BooleanField(
required=False, required=False, help_text="True if the payout was on-chain (only for buyer)"
help_text="True if the payout was on-chain (only for buyer)"
) )
received_onchain_sats = serializers.IntegerField( received_onchain_sats = serializers.IntegerField(
required=False, 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( mining_fee_sats = serializers.IntegerField(
required=False, 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( swap_fee_sats = serializers.IntegerField(
required=False, 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( swap_fee_percent = serializers.FloatField(
required=False, 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( sent_sats = serializers.IntegerField(
required=False, required=False, help_text="The total sats you sent (only for seller)"
help_text="The total sats you sent (only for seller)"
) )
received_fiat = serializers.IntegerField( received_fiat = serializers.IntegerField(
required=False, required=False, help_text="same as `amount` (only for seller)"
help_text="same as `amount` (only for seller)"
) )
trade_fee_sats = serializers.IntegerField( trade_fee_sats = serializers.IntegerField(
required=False, 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): class PlatformSummarySerializer(serializers.Serializer):
contract_timestamp = serializers.DateTimeField( contract_timestamp = serializers.DateTimeField(
required=False, 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( contract_total_time = serializers.FloatField(
required=False, 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( routing_fee_sats = serializers.IntegerField(
required=False, 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( trade_revenue_sats = serializers.IntegerField(
required=False, required=False, help_text="The sats the exchange earned from the trade"
help_text="The sats the exchange earned from the trade"
) )
# Only used in oas_schemas # Only used in oas_schemas
class OrderDetailSerializer(serializers.ModelSerializer): class OrderDetailSerializer(serializers.ModelSerializer):
total_secs_exp = serializers.IntegerField( total_secs_exp = serializers.IntegerField(
required=False, required=False,
help_text="Duration of time (in seconds) to expire, according to the current status of order." 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 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 value changes according to which stage the order is in",
) )
penalty = serializers.DateTimeField( penalty = serializers.DateTimeField(
required=False, 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( is_maker = serializers.BooleanField(
required=False, required=False, help_text="Whether you are the maker or not"
help_text="Whether you are the maker or not"
) )
is_taker = serializers.BooleanField( is_taker = serializers.BooleanField(
required=False, required=False, help_text="Whether you are the taker or not"
help_text="Whether you are the taker or not"
) )
is_participant = serializers.BooleanField( is_participant = serializers.BooleanField(
required=False, 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( maker_status = serializers.CharField(
required=False, required=False,
@ -151,193 +155,170 @@ class OrderDetailSerializer(serializers.ModelSerializer):
"- **'Active'** (seen within last 2 min)\n" "- **'Active'** (seen within last 2 min)\n"
"- **'Seen Recently'** (seen within last 10 min)\n" "- **'Seen Recently'** (seen within last 10 min)\n"
"- **'Inactive'** (seen more than 10 min ago)\n\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( taker_status = serializers.BooleanField(
required=False, 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( price_now = serializers.IntegerField(
required=False, 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( premium = serializers.IntegerField(
required=False, required=False, help_text="Premium over the CEX price at the current time"
help_text="Premium over the CEX price at the current time"
) )
premium_percentile = serializers.IntegerField( premium_percentile = serializers.IntegerField(
required=False, 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( num_similar_orders = serializers.IntegerField(
required=False, 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( tg_enabled = serializers.BooleanField(
required=False, 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( tg_token = serializers.CharField(
required=False, 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( tg_bot_name = serializers.CharField(
required=False, 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( is_buyer = serializers.BooleanField(
required=False, 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( is_seller = serializers.BooleanField(
required=False, 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( maker_nick = serializers.CharField(
required=False, required=False, help_text="Nickname (Robot name) of the maker"
help_text="Nickname (Robot name) of the maker"
) )
taker_nick = serializers.CharField( taker_nick = serializers.CharField(
required=False, required=False, help_text="Nickname (Robot name) of the taker"
help_text="Nickname (Robot name) of the taker"
) )
status_message = serializers.CharField( status_message = serializers.CharField(
required=False, 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( is_fiat_sent = serializers.BooleanField(
required=False, required=False, help_text="Whether or not the fiat amount is sent by the buyer"
help_text="Whether or not the fiat amount is sent by the buyer"
) )
is_disputed = serializers.BooleanField( is_disputed = serializers.BooleanField(
required=False, required=False, help_text="Whether or not the counterparty raised a dispute"
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"
) )
ur_nick = serializers.CharField(required=False, help_text="Your Nickname")
ur_nick = serializers.CharField(required=False, help_text="Your Nick")
maker_locked = serializers.BooleanField( maker_locked = serializers.BooleanField(
required=False, required=False, help_text="True if maker bond is locked, False otherwise"
help_text="True if maker bond is locked, False otherwise"
) )
taker_locked = serializers.BooleanField( taker_locked = serializers.BooleanField(
required=False, required=False, help_text="True if taker bond is locked, False otherwise"
help_text="True if taker bond is locked, False otherwise"
) )
escrow_locked = serializers.BooleanField( escrow_locked = serializers.BooleanField(
required=False, 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( trade_satoshis = serializers.IntegerField(
required=False, 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( bond_invoice = serializers.CharField(
required=False, required=False, help_text="When `status` = `0`, `3`. Bond invoice to be paid"
help_text="When `status` = `0`, `3`. Bond invoice to be paid"
) )
bond_satoshis = serializers.IntegerField( bond_satoshis = serializers.IntegerField(
required=False, required=False, help_text="The bond amount in satoshis"
help_text="The bond amount in satoshis"
) )
escrow_invoice = serializers.CharField( escrow_invoice = serializers.CharField(
required=False, 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( escrow_satoshis = serializers.IntegerField(
required=False, required=False, help_text="The escrow amount in satoshis"
help_text="The escrow amount in satoshis"
) )
invoice_amount = serializers.IntegerField( invoice_amount = serializers.IntegerField(
required=False, 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( swap_allowed = serializers.BooleanField(
required=False, required=False, help_text="Whether on-chain swap is allowed"
help_text="Whether on-chain swap is allowed"
) )
swap_failure_reason = serializers.CharField( swap_failure_reason = serializers.CharField(
required=False, required=False, help_text="Reason for why on-chain swap is not available"
help_text="Reason for why on-chain swap is not available"
) )
suggested_mining_fee_rate = serializers.IntegerField( suggested_mining_fee_rate = serializers.IntegerField(
required=False, required=False, help_text="fee in sats/vbyte for the on-chain swap"
help_text="fee in sats/vbyte for the on-chain swap"
) )
swap_fee_rate = serializers.FloatField( swap_fee_rate = serializers.FloatField(
required=False, 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( pending_cancel = serializers.BooleanField(
required=False, 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( asked_for_cancel = serializers.BooleanField(
required=False, 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( statement_submitted = serializers.BooleanField(
required=False, 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( retries = serializers.IntegerField(
required=False, 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( next_retry_time = serializers.DateTimeField(
required=False, 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( failure_reason = serializers.CharField(
required=False, required=False, help_text="The reason the payout failed"
help_text="The reason the payout failed"
) )
invoice_expired = serializers.BooleanField( invoice_expired = serializers.BooleanField(
required=False, 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( trade_fee_percent = serializers.IntegerField(
required=False, 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( bond_size_sats = serializers.IntegerField(
required=False, required=False, help_text="The size of the bond in sats"
help_text="The size of the bond in sats"
) )
bond_size_percent = serializers.IntegerField( bond_size_percent = serializers.IntegerField(
required=False, required=False, help_text="same as `bond_size`"
help_text="same as `bond_size`"
) )
maker_summary = SummarySerializer(required=False) maker_summary = SummarySerializer(required=False)
taker_summary = SummarySerializer(required=False) taker_summary = SummarySerializer(required=False)
platform_summary = PlatformSummarySerializer(required=True) platform_summary = PlatformSummarySerializer(required=True)
expiry_message = serializers.CharField( expiry_message = serializers.CharField(
required=False, 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( num_satoshis = serializers.IntegerField(
required=False, 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( sent_satoshis = serializers.IntegerField(
required=False, 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( txid = serializers.CharField(
required=False, 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( network = serializers.CharField(
required=False, 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: class Meta:
model = Order model = Order
fields = ( fields = (
@ -392,7 +373,7 @@ class OrderDetailSerializer(serializers.ModelSerializer):
"escrow_satoshis", "escrow_satoshis",
"invoice_amount", "invoice_amount",
"swap_allowed", "swap_allowed",
'swap_failure_reason', "swap_failure_reason",
"suggested_mining_fee_rate", "suggested_mining_fee_rate",
"swap_fee_rate", "swap_fee_rate",
"pending_cancel", "pending_cancel",
@ -421,9 +402,16 @@ class OrderDetailSerializer(serializers.ModelSerializer):
class OrderPublicSerializer(serializers.ModelSerializer): class OrderPublicSerializer(serializers.ModelSerializer):
maker_nick = serializers.CharField(required=False) maker_nick = serializers.CharField(required=False)
maker_status = serializers.CharField(help_text='Status of the nick - "Active" or "Inactive"', required=False) maker_status = serializers.CharField(
price = serializers.FloatField(help_text="Price in order's fiat currency", required=False) help_text='Status of the nick - "Active" or "Inactive"', 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) )
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: class Meta:
model = Order model = Order
@ -448,7 +436,7 @@ class OrderPublicSerializer(serializers.ModelSerializer):
"price", "price",
"escrow_duration", "escrow_duration",
"satoshis_now", "satoshis_now",
"bond_size" "bond_size",
) )
@ -461,19 +449,19 @@ class MakeOrderSerializer(serializers.ModelSerializer):
max_length=70, max_length=70,
default="not specified", default="not specified",
required=False, 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( is_explicit = serializers.BooleanField(
default=False, 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( has_range = serializers.BooleanField(
default=False, 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( bondless_taker = serializers.BooleanField(
default=False, 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: class Meta:
@ -495,19 +483,17 @@ class MakeOrderSerializer(serializers.ModelSerializer):
"bondless_taker", "bondless_taker",
) )
class UpdateOrderSerializer(serializers.Serializer): class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, invoice = serializers.CharField(
allow_null=True, max_length=2000, allow_null=True, allow_blank=True, default=None
allow_blank=True, )
default=None) address = serializers.CharField(
address = serializers.CharField(max_length=100, max_length=100, allow_null=True, allow_blank=True, default=None
allow_null=True, )
allow_blank=True, statement = serializers.CharField(
default=None) max_length=10000, 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( action = serializers.ChoiceField(
choices=( choices=(
"pause", "pause",
@ -529,64 +515,86 @@ class UpdateOrderSerializer(serializers.Serializer):
allow_blank=True, allow_blank=True,
default=None, default=None,
) )
amount = serializers.DecimalField(max_digits=18, decimal_places=8, allow_null=True, required=False, default=None) amount = serializers.DecimalField(
mining_fee_rate = serializers.DecimalField(max_digits=6, decimal_places=3, allow_null=True, required=False, default=None) max_digits=18, decimal_places=8, allow_null=True, required=False, default=None
)
mining_fee_rate = serializers.DecimalField(
max_digits=6, decimal_places=3, allow_null=True, required=False, default=None
)
class UserGenSerializer(serializers.Serializer): class UserGenSerializer(serializers.Serializer):
# Mandatory fields # Mandatory fields
token_sha256 = serializers.CharField( token_sha256 = serializers.CharField(
min_length=64, min_length=64,
max_length=64, max_length=64,
allow_null=False, allow_null=False,
allow_blank=False, allow_blank=False,
required=True, required=True,
help_text="SHA256 of user secret") help_text="SHA256 of user secret",
public_key = serializers.CharField(max_length=2000, )
allow_null=False, public_key = serializers.CharField(
allow_blank=False, max_length=2000,
required=True, allow_null=False,
help_text="Armored ASCII PGP public key block") allow_blank=False,
encrypted_private_key = serializers.CharField(max_length=2000, required=True,
allow_null=False, help_text="Armored ASCII PGP public key block",
allow_blank=False, )
required=True, encrypted_private_key = serializers.CharField(
help_text="Armored ASCII PGP encrypted private key block") max_length=2000,
allow_null=False,
allow_blank=False,
required=True,
help_text="Armored ASCII PGP encrypted private key block",
)
# Optional fields # Optional fields
ref_code = serializers.CharField(max_length=30, ref_code = serializers.CharField(
allow_null=True, max_length=30,
allow_blank=True, allow_null=True,
required=False, allow_blank=True,
default=None, required=False,
help_text="Referal code") default=None,
counts = serializers.ListField(child=serializers.IntegerField(), help_text="Referal code",
allow_null=True, )
required=False, counts = serializers.ListField(
default=None, child=serializers.IntegerField(),
help_text="Counts of the unique characters in the token") allow_null=True,
length = serializers.IntegerField(allow_null=True, required=False,
default=None, default=None,
required=False, help_text="Counts of the unique characters in the token",
min_value=1, )
help_text="Length of the token") length = serializers.IntegerField(
unique_values = serializers.IntegerField(allow_null=True, allow_null=True,
default=None, default=None,
required=False, required=False,
min_value=1, min_value=1,
help_text="Number of unique values in the token") 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): class ClaimRewardSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000, invoice = serializers.CharField(
allow_null=True, max_length=2000,
allow_blank=True, allow_null=True,
default=None, allow_blank=True,
help_text="A valid LN invoice with the reward amount to withdraw") default=None,
help_text="A valid LN invoice with the reward amount to withdraw",
)
class PriceSerializer(serializers.Serializer): class PriceSerializer(serializers.Serializer):
pass pass
class TickSerializer(serializers.ModelSerializer):
class TickSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = MarketTick model = MarketTick
fields = ( fields = (
@ -599,5 +607,6 @@ class TickSerializer(serializers.ModelSerializer):
) )
depth = 1 depth = 1
class StealthSerializer(serializers.Serializer): class StealthSerializer(serializers.Serializer):
wantsStealth = serializers.BooleanField() wantsStealth = serializers.BooleanField()

View File

@ -1,5 +1,6 @@
from celery import shared_task from celery import shared_task
@shared_task(name="users_cleansing") @shared_task(name="users_cleansing")
def users_cleansing(): def users_cleansing():
""" """
@ -21,7 +22,11 @@ def users_cleansing():
for user in queryset: for user in queryset:
# Try an except, due to unknown cause for users lacking profiles. # Try an except, due to unknown cause for users lacking profiles.
try: 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 continue
if not user.profile.total_contracts == 0: if not user.profile.total_contracts == 0:
continue continue
@ -38,6 +43,7 @@ def users_cleansing():
} }
return results return results
@shared_task(name="give_rewards") @shared_task(name="give_rewards")
def give_rewards(): def give_rewards():
""" """
@ -57,10 +63,14 @@ def give_rewards():
profile.pending_rewards = 0 profile.pending_rewards = 0
profile.save() 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 return results
@shared_task(name="follow_send_payment") @shared_task(name="follow_send_payment")
def follow_send_payment(hash): def follow_send_payment(hash):
"""Sends sats to buyer, continuous update""" """Sends sats to buyer, continuous update"""
@ -75,10 +85,10 @@ def follow_send_payment(hash):
lnpayment = LNPayment.objects.get(payment_hash=hash) lnpayment = LNPayment.objects.get(payment_hash=hash)
fee_limit_sat = int( fee_limit_sat = int(
max( max(
lnpayment.num_satoshis * lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")), float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
)) # 1000 ppm or 10 sats )
) # 1000 ppm or 10 sats
timeout_seconds = int(config("PAYOUT_TIMEOUT_SECONDS")) timeout_seconds = int(config("PAYOUT_TIMEOUT_SECONDS"))
request = LNNode.routerrpc.SendPaymentRequest( request = LNNode.routerrpc.SendPaymentRequest(
@ -89,15 +99,13 @@ def follow_send_payment(hash):
order = lnpayment.order_paid_LN order = lnpayment.order_paid_LN
try: try:
for response in LNNode.routerstub.SendPaymentV2(request, for response in LNNode.routerstub.SendPaymentV2(
metadata=[ request, metadata=[("macaroon", MACAROON.hex())]
("macaroon", ):
MACAROON.hex())
]):
lnpayment.in_flight = True lnpayment.in_flight = True
lnpayment.save() lnpayment.save()
if response.status == 0: # Status 0 'UNKNOWN' if response.status == 0: # Status 0 'UNKNOWN'
# Not sure when this status happens # Not sure when this status happens
lnpayment.in_flight = False lnpayment.in_flight = False
@ -125,18 +133,20 @@ def follow_send_payment(hash):
order.status = Order.Status.FAI order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.FAI)) seconds=order.t_to_expire(Order.Status.FAI)
)
order.save() order.save()
context = { context = {
"routing_failed": "routing_failed": LNNode.payment_failure_context[
LNNode.payment_failure_context[response.failure_reason], response.failure_reason
"IN_FLIGHT":False, ],
"IN_FLIGHT": False,
} }
print(context) print(context)
# If failed due to not route, reset mission control. (This won't scale well, just a temporary fix) # If failed due to not route, reset mission control. (This won't scale well, just a temporary fix)
# ResetMC deactivate temporary for tests # ResetMC deactivate temporary for tests
#if response.failure_reason==2: # if response.failure_reason==2:
# LNNode.resetmc() # LNNode.resetmc()
return False, context return False, context
@ -144,12 +154,13 @@ def follow_send_payment(hash):
if response.status == 2: # Status 2 'SUCCEEDED' if response.status == 2: # Status 2 'SUCCEEDED'
print("SUCCEEDED") print("SUCCEEDED")
lnpayment.status = LNPayment.Status.SUCCED 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.preimage = response.payment_preimage
lnpayment.save() lnpayment.save()
order.status = Order.Status.SUC order.status = Order.Status.SUC
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.SUC)) seconds=order.t_to_expire(Order.Status.SUC)
)
order.save() order.save()
return True, None return True, None
@ -162,17 +173,19 @@ def follow_send_payment(hash):
lnpayment.save() lnpayment.save()
order.status = Order.Status.FAI order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta( order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.FAI)) seconds=order.t_to_expire(Order.Status.FAI)
)
order.save() order.save()
context = {"routing_failed": "The payout invoice has expired"} context = {"routing_failed": "The payout invoice has expired"}
return False, context return False, context
@shared_task(name="payments_cleansing") @shared_task(name="payments_cleansing")
def 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. 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 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 # Usually expiry is 1 day for every finished order. So ~4 days until
# a never locked hodl invoice is removed. # a never locked hodl invoice is removed.
finished_time = timezone.now() - timedelta(days=3) finished_time = timezone.now() - timedelta(days=3)
queryset = LNPayment.objects.filter(Q(status=LNPayment.Status.CANCEL), queryset = LNPayment.objects.filter(
Q(order_made__expires_at__lt=finished_time)| Q(status=LNPayment.Status.CANCEL),
Q(order_taken__expires_at__lt=finished_time)) 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. # And do not have an active trade, any past contract or any reward.
deleted_lnpayments = [] deleted_lnpayments = []
@ -200,10 +214,12 @@ def payments_cleansing():
deleted_lnpayments.append(name) deleted_lnpayments.append(name)
except: except:
pass pass
# same for onchain payments # same for onchain payments
queryset = OnchainPayment.objects.filter(Q(status__in=[OnchainPayment.Status.CANCE, OnchainPayment.Status.CREAT]), queryset = OnchainPayment.objects.filter(
Q(order_paid_TX__expires_at__lt=finished_time)|Q(order_paid_TX__isnull=True)) 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. # And do not have an active trade, any past contract or any reward.
deleted_onchainpayments = [] deleted_onchainpayments = []
@ -224,6 +240,7 @@ def payments_cleansing():
} }
return results return results
@shared_task(name="cache_external_market_prices", ignore_result=True) @shared_task(name="cache_external_market_prices", ignore_result=True)
def cache_market(): def cache_market():
@ -236,7 +253,9 @@ def cache_market():
exchange_rates = get_exchange_rates(currency_codes) exchange_rates = get_exchange_rates(currency_codes)
results = {} 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] rate = exchange_rates[i]
results[i] = {currency_codes[i], rate} results[i] = {currency_codes[i], rate}
@ -259,45 +278,48 @@ def cache_market():
return results return results
@shared_task(name="send_message", ignore_result=True) @shared_task(name="send_message", ignore_result=True)
def send_message(order_id, message): def send_message(order_id, message):
from api.models import Order from api.models import Order
order = Order.objects.get(id=order_id) order = Order.objects.get(id=order_id)
if not order.maker.profile.telegram_enabled: if not order.maker.profile.telegram_enabled:
return return
from api.messages import Telegram from api.messages import Telegram
telegram = Telegram() telegram = Telegram()
if message == 'welcome': if message == "welcome":
telegram.welcome(order) telegram.welcome(order)
elif message == 'order_expired_untaken': elif message == "order_expired_untaken":
telegram.order_expired_untaken(order) telegram.order_expired_untaken(order)
elif message == 'trade_successful': elif message == "trade_successful":
telegram.trade_successful(order) telegram.trade_successful(order)
elif message == 'public_order_cancelled': elif message == "public_order_cancelled":
telegram.public_order_cancelled(order) telegram.public_order_cancelled(order)
elif message == 'taker_expired_b4bond': elif message == "taker_expired_b4bond":
telegram.taker_expired_b4bond(order) telegram.taker_expired_b4bond(order)
elif message == 'order_published': elif message == "order_published":
telegram.order_published(order) telegram.order_published(order)
elif message == 'order_taken_confirmed': elif message == "order_taken_confirmed":
telegram.order_taken_confirmed(order) telegram.order_taken_confirmed(order)
elif message == 'fiat_exchange_starts': elif message == "fiat_exchange_starts":
telegram.fiat_exchange_starts(order) telegram.fiat_exchange_starts(order)
elif message == 'dispute_opened': elif message == "dispute_opened":
telegram.dispute_opened(order) telegram.dispute_opened(order)
elif message == 'collaborative_cancelled': elif message == "collaborative_cancelled":
telegram.collaborative_cancelled(order) telegram.collaborative_cancelled(order)
return return

View File

@ -1,16 +1,27 @@
from django.urls import path 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 drf_spectacular.views import SpectacularAPIView, SpectacularRedocView
from chat.views import ChatView from chat.views import ChatView
urlpatterns = [ urlpatterns = [
path('schema/', SpectacularAPIView.as_view(), name='schema'), path("schema/", SpectacularAPIView.as_view(), name="schema"),
path('', SpectacularRedocView.as_view(url_name='schema'), name='redoc'), path("", SpectacularRedocView.as_view(url_name="schema"), name="redoc"),
path("make/", MakerView.as_view()), path("make/", MakerView.as_view()),
path("order/",OrderView.as_view({ path(
"get": "get", "order/",
"post": "take_update_confirm_dispute_cancel" OrderView.as_view({"get": "get", "post": "take_update_confirm_dispute_cancel"}),
}),
), ),
path("user/", UserView.as_view()), path("user/", UserView.as_view()),
path("book/", BookView.as_view()), path("book/", BookView.as_view()),
@ -21,5 +32,5 @@ urlpatterns = [
path("historical/", HistoricalView.as_view()), path("historical/", HistoricalView.as_view()),
path("ticks/", TickView.as_view()), path("ticks/", TickView.as_view()),
path("stealth/", StealthView.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"})),
] ]

View File

@ -7,17 +7,20 @@ from decouple import config
from api.models import Order 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(): def get_session():
session = requests.session() session = requests.session()
# Tor uses the 9050 port as the default socks port # Tor uses the 9050 port as the default socks port
if USE_TOR: if USE_TOR:
session.proxies = {'http': 'socks5://' + TOR_PROXY, session.proxies = {
'https': 'socks5://' + TOR_PROXY} "http": "socks5://" + TOR_PROXY,
"https": "socks5://" + TOR_PROXY,
}
return session return session
@ -29,22 +32,19 @@ def bitcoind_rpc(method, params=None):
:return: :return:
""" """
BITCOIND_RPCURL = config('BITCOIND_RPCURL') BITCOIND_RPCURL = config("BITCOIND_RPCURL")
BITCOIND_RPCUSER = config('BITCOIND_RPCUSER') BITCOIND_RPCUSER = config("BITCOIND_RPCUSER")
BITCOIND_RPCPASSWORD = config('BITCOIND_RPCPASSWORD') BITCOIND_RPCPASSWORD = config("BITCOIND_RPCPASSWORD")
if params is None: if params is None:
params = [] params = []
payload = json.dumps( 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): def validate_onchain_address(address):
@ -53,17 +53,21 @@ def validate_onchain_address(address):
""" """
try: try:
validation = bitcoind_rpc('validateaddress', [address]) validation = bitcoind_rpc("validateaddress", [address])
if not validation['isvalid']: if not validation["isvalid"]:
return False, {"bad_address": "Invalid address"} return False, {"bad_address": "Invalid address"}
except Exception as e: except Exception as e:
logger.error(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 return True, None
market_cache = {} market_cache = {}
@ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds @ring.dict(market_cache, expire=3) # keeps in cache for 3 seconds
def get_exchange_rates(currencies): def get_exchange_rates(currencies):
""" """
@ -74,8 +78,7 @@ def get_exchange_rates(currencies):
session = get_session() session = get_session()
APIS = config("MARKET_PRICE_APIS", APIS = config("MARKET_PRICE_APIS", cast=lambda v: [s.strip() for s in v.split(",")])
cast=lambda v: [s.strip() for s in v.split(",")])
api_rates = [] api_rates = []
for api_url in APIS: for api_url in APIS:
@ -86,7 +89,8 @@ def get_exchange_rates(currencies):
for currency in currencies: for currency in currencies:
try: # If a currency is missing place a None try: # If a currency is missing place a None
blockchain_rates.append( blockchain_rates.append(
float(blockchain_prices[currency]["last"])) float(blockchain_prices[currency]["last"])
)
except: except:
blockchain_rates.append(np.nan) blockchain_rates.append(np.nan)
api_rates.append(blockchain_rates) api_rates.append(blockchain_rates)
@ -96,8 +100,7 @@ def get_exchange_rates(currencies):
yadio_rates = [] yadio_rates = []
for currency in currencies: for currency in currencies:
try: try:
yadio_rates.append(float( yadio_rates.append(float(yadio_prices["BTC"][currency]))
yadio_prices["BTC"][currency]))
except: except:
yadio_rates.append(np.nan) yadio_rates.append(np.nan)
api_rates.append(yadio_rates) api_rates.append(yadio_rates)
@ -133,6 +136,8 @@ def get_lnd_version():
robosats_commit_cache = {} robosats_commit_cache = {}
@ring.dict(robosats_commit_cache, expire=3600) @ring.dict(robosats_commit_cache, expire=3600)
def get_robosats_commit(): def get_robosats_commit():
@ -140,13 +145,16 @@ def get_robosats_commit():
commit_hash = commit.read() commit_hash = commit.read()
# .git folder is included in .dockerignore. But automatic build will drop in a commit_sha.txt file on root # .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: with open("commit_sha.txt") as f:
commit_hash = f.read() commit_hash = f.read()
return commit_hash return commit_hash
robosats_version_cache = {} robosats_version_cache = {}
@ring.dict(robosats_commit_cache, expire=99999) @ring.dict(robosats_commit_cache, expire=99999)
def get_robosats_version(): def get_robosats_version():
@ -156,12 +164,16 @@ def get_robosats_version():
print(version_dict) print(version_dict)
return version_dict return version_dict
premium_percentile = {} premium_percentile = {}
@ring.dict(premium_percentile, expire=300) @ring.dict(premium_percentile, expire=300)
def compute_premium_percentile(order): def compute_premium_percentile(order):
queryset = Order.objects.filter( 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)) print(len(queryset))
if len(queryset) <= 1: if len(queryset) <= 1:
@ -171,15 +183,18 @@ def compute_premium_percentile(order):
order_rate = float(order.last_satoshis) / float(amount) order_rate = float(order.last_satoshis) / float(amount)
rates = [] rates = []
for similar_order in queryset: for similar_order in queryset:
similar_order_amount = similar_order.amount if not similar_order.has_range else similar_order.max_amount similar_order_amount = (
rates.append( similar_order.amount
float(similar_order.last_satoshis) / float(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) rates = np.array(rates)
return round(np.sum(rates < order_rate) / len(rates), 2) 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. """Very close to numpy.percentile, but it supports weights.
NOTE: quantiles should be in [0, 1]! NOTE: quantiles should be in [0, 1]!
:param values: numpy.array with data :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: if sample_weight is None:
sample_weight = np.ones(len(values)) sample_weight = np.ones(len(values))
sample_weight = np.array(sample_weight) sample_weight = np.array(sample_weight)
assert np.all(quantiles >= 0) and np.all(quantiles <= 1), \ assert np.all(quantiles >= 0) and np.all(
'quantiles should be in [0, 1]' quantiles <= 1
), "quantiles should be in [0, 1]"
if not values_sorted: if not values_sorted:
sorter = np.argsort(values) 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) return np.interp(quantiles, weighted_quantiles, values)
def compute_avg_premium(queryset): def compute_avg_premium(queryset):
premiums = [] premiums = []
volumes = [] volumes = []
@ -221,11 +238,10 @@ def compute_avg_premium(queryset):
total_volume = sum(volumes) total_volume = sum(volumes)
# weighted_median_premium is the weighted median of the premiums by volume # weighted_median_premium is the weighted median of the premiums by volume
if len(premiums) > 0 and len(volumes)>0: if len(premiums) > 0 and len(volumes) > 0:
weighted_median_premium = weighted_median(values=premiums, weighted_median_premium = weighted_median(
sample_weight=volumes, values=premiums, sample_weight=volumes, quantiles=0.5, values_sorted=False
quantiles=0.5, )
values_sorted=False)
else: else:
weighted_median_premium = 0.0 weighted_median_premium = 0.0
return weighted_median_premium, total_volume return weighted_median_premium, total_volume

View File

@ -7,16 +7,45 @@ from rest_framework.response import Response
from django.contrib.auth import authenticate, login, logout from django.contrib.auth import authenticate, login, logout
from django.contrib.auth.models import User 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 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 api.models import LNPayment, MarketTick, OnchainPayment, Order, Currency, Profile
from control.models import AccountingDay, BalanceLog from control.models import AccountingDay, BalanceLog
from api.logics import Logics from api.logics import Logics
from api.messages import Telegram from api.messages import Telegram
from secrets import token_urlsafe from secrets import token_urlsafe
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 .nick_generator.nick_generator import NickGenerator
from robohash import Robohash from robohash import Robohash
@ -31,7 +60,7 @@ from decouple import config
EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE")) EXP_MAKER_BOND_INVOICE = int(config("EXP_MAKER_BOND_INVOICE"))
RETRY_TIME = int(config("RETRY_TIME")) 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")) ESCROW_DURATION = 60 * int(config("INVOICE_AND_ESCROW_DURATION"))
BOND_SIZE = int(config("DEFAULT_BOND_SIZE")) BOND_SIZE = int(config("DEFAULT_BOND_SIZE"))
@ -50,10 +79,7 @@ class MakerView(CreateAPIView):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return Response( 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, status.HTTP_400_BAD_REQUEST,
) )
@ -61,11 +87,12 @@ class MakerView(CreateAPIView):
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)
# In case it gets overwhelming. Limit the number of public orders. # 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( return Response(
{ {
"bad_request": "bad_request": "Woah! RoboSats' book is at full capacity! Try again later"
"Woah! RoboSats' book is at full capacity! Try again later"
}, },
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
@ -90,11 +117,16 @@ class MakerView(CreateAPIView):
bondless_taker = serializer.data.get("bondless_taker") bondless_taker = serializer.data.get("bondless_taker")
# Optional params # Optional params
if public_duration == None: public_duration = PUBLIC_DURATION if public_duration == None:
if escrow_duration == None: escrow_duration = ESCROW_DURATION public_duration = PUBLIC_DURATION
if bond_size == None: bond_size = BOND_SIZE if escrow_duration == None:
if bondless_taker == None: bondless_taker = False escrow_duration = ESCROW_DURATION
if has_range == None: has_range = False 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 # 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): if has_range and (min_amount == None or max_amount == None):
return Response( return Response(
{ {
"bad_request": "bad_request": "You must specify min_amount and max_amount for a range order"
"You must specify min_amount and max_amount for a range order"
}, },
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
elif not has_range and amount == None: elif not has_range and amount == None:
return Response( return Response(
{ {"bad_request": "You must specify an order amount"},
"bad_request":
"You must specify an order amount"
},
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
# Creates a new order # Creates a new order
order = Order( order = Order(
type=type, type=type,
@ -136,8 +163,7 @@ class MakerView(CreateAPIView):
premium=premium, premium=premium,
satoshis=satoshis, satoshis=satoshis,
is_explicit=is_explicit, is_explicit=is_explicit,
expires_at=timezone.now() + timedelta( expires_at=timezone.now() + timedelta(seconds=EXP_MAKER_BOND_INVOICE),
seconds=EXP_MAKER_BOND_INVOICE),
maker=request.user, maker=request.user,
public_duration=public_duration, public_duration=public_duration,
escrow_duration=escrow_duration, escrow_duration=escrow_duration,
@ -152,8 +178,7 @@ class MakerView(CreateAPIView):
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
order.save() order.save()
return Response(ListOrderSerializer(order).data, return Response(ListOrderSerializer(order).data, status=status.HTTP_201_CREATED)
status=status.HTTP_201_CREATED)
class OrderView(viewsets.ViewSet): class OrderView(viewsets.ViewSet):
@ -170,8 +195,7 @@ class OrderView(viewsets.ViewSet):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return Response( return Response(
{ {
"bad_request": "bad_request": "You must have a robot avatar to see the order details"
"You must have a robot avatar to see the order details"
}, },
status=status.HTTP_400_BAD_REQUEST, status=status.HTTP_400_BAD_REQUEST,
) )
@ -186,8 +210,9 @@ class OrderView(viewsets.ViewSet):
# check if exactly one order is found in the db # check if exactly one order is found in the db
if len(order) != 1: if len(order) != 1:
return Response({"bad_request": "Invalid Order Id"}, return Response(
status.HTTP_404_NOT_FOUND) {"bad_request": "Invalid Order Id"}, status.HTTP_404_NOT_FOUND
)
# This is our order. # This is our order.
order = order[0] order = order[0]
@ -200,10 +225,7 @@ class OrderView(viewsets.ViewSet):
) )
if order.status == Order.Status.CCA: if order.status == Order.Status.CCA:
return Response( return Response(
{ {"bad_request": "This order has been cancelled collaborativelly"},
"bad_request":
"This order has been cancelled collaborativelly"
},
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
@ -239,11 +261,9 @@ class OrderView(viewsets.ViewSet):
# Add activity status of participants based on last_seen # Add activity status of participants based on last_seen
if order.taker_last_seen != None: if order.taker_last_seen != None:
data["taker_status"] = Logics.user_activity_status( data["taker_status"] = Logics.user_activity_status(order.taker_last_seen)
order.taker_last_seen)
if order.maker_last_seen != None: if order.maker_last_seen != None:
data["maker_status"] = Logics.user_activity_status( data["maker_status"] = Logics.user_activity_status(order.maker_last_seen)
order.maker_last_seen)
# 3.b) Non participants can view details (but only if PUB) # 3.b) Non participants can view details (but only if PUB)
if not data["is_participant"] and order.status == Order.Status.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: if order.status >= Order.Status.PUB and order.status < Order.Status.WF2:
data["price_now"], data["premium_now"] = Logics.price_and_premium_now(order) 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. # 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["premium_percentile"] = compute_premium_percentile(order)
data["num_similar_orders"] = len( data["num_similar_orders"] = len(
Order.objects.filter(currency=order.currency, Order.objects.filter(
status=Order.Status.PUB)) currency=order.currency, status=Order.Status.PUB
)
)
# Adds/generate telegram token and whether it is enabled # Adds/generate telegram token and whether it is enabled
# Deprecated # 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 # For participants add positions, nicks and status as a message and hold invoices status
data["is_buyer"] = Logics.is_buyer(order, request.user) 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 both bonds are locked, participants can see the final trade amount in sats.
if order.taker_bond: if order.taker_bond:
if (order.maker_bond.status == order.taker_bond.status == if (
LNPayment.Status.LOCKED): order.maker_bond.status
== order.taker_bond.status
== LNPayment.Status.LOCKED
):
# Seller sees the amount he sends # Seller sees the amount he sends
if data["is_seller"]: if data["is_seller"]:
data["trade_satoshis"] = Logics.escrow_amount( data["trade_satoshis"] = Logics.escrow_amount(order, request.user)[
order, request.user)[1]["escrow_amount"] 1
]["escrow_amount"]
# Buyer sees the amount he receives # Buyer sees the amount he receives
elif data["is_buyer"]: elif data["is_buyer"]:
data["trade_satoshis"] = Logics.payout_amount( data["trade_satoshis"] = Logics.payout_amount(order, request.user)[
order, request.user)[1]["invoice_amount"] 1
]["invoice_amount"]
# 5) If status is 'waiting for maker bond' and user is MAKER, reply with a MAKER hold invoice. # 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"]: 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) return Response(context, status.HTTP_400_BAD_REQUEST)
# 7 a. ) If seller and status is 'WF2' or 'WFE' # 7 a. ) If seller and status is 'WF2' or 'WFE'
elif data["is_seller"] and (order.status == Order.Status.WF2 elif data["is_seller"] and (
or order.status == Order.Status.WFE): order.status == Order.Status.WF2 or order.status == Order.Status.WFE
):
# If the two bonds are locked, reply with an ESCROW hold invoice. # If the two bonds are locked, reply with an ESCROW hold invoice.
if (order.maker_bond.status == order.taker_bond.status == if (
LNPayment.Status.LOCKED): order.maker_bond.status
valid, context = Logics.gen_escrow_hold_invoice( == order.taker_bond.status
order, request.user) == LNPayment.Status.LOCKED
):
valid, context = Logics.gen_escrow_hold_invoice(order, request.user)
if valid: if valid:
data = {**data, **context} data = {**data, **context}
else: else:
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
# 7.b) If user is Buyer and status is 'WF2' or 'WFI' # 7.b) If user is Buyer and status is 'WF2' or 'WFI'
elif data["is_buyer"] and (order.status == Order.Status.WF2 elif data["is_buyer"] and (
or order.status == Order.Status.WFI): 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 the two bonds are locked, reply with an AMOUNT and onchain swap cost so he can send the buyer invoice/address.
if (order.maker_bond.status == order.taker_bond.status == if (
LNPayment.Status.LOCKED): order.maker_bond.status
== order.taker_bond.status
== LNPayment.Status.LOCKED
):
valid, context = Logics.payout_amount(order, request.user) valid, context = Logics.payout_amount(order, request.user)
if valid: if valid:
data = {**data, **context} data = {**data, **context}
@ -348,23 +385,27 @@ class OrderView(viewsets.ViewSet):
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
# 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED # 8) If status is 'CHA' or 'FSE' and all HTLCS are in LOCKED
elif order.status in [ elif order.status in [Order.Status.WFI, Order.Status.CHA, Order.Status.FSE]:
Order.Status.WFI, Order.Status.CHA, Order.Status.FSE
]:
# If all bonds are locked. # If all bonds are locked.
if (order.maker_bond.status == order.taker_bond.status == if (
order.trade_escrow.status == LNPayment.Status.LOCKED): 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 # add whether a collaborative cancel is pending or has been asked
if (data["is_maker"] and order.taker_asked_cancel) or ( 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 data["pending_cancel"] = True
elif (data["is_maker"] and order.maker_asked_cancel) or ( 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 data["asked_for_cancel"] = True
else: else:
data["asked_for_cancel"] = False data["asked_for_cancel"] = False
offset = request.GET.get('offset', None) offset = request.GET.get("offset", None)
if offset: if offset:
data["chat"] = ChatView.get(None, request).data data["chat"] = ChatView.get(None, request).data
@ -373,35 +414,47 @@ class OrderView(viewsets.ViewSet):
# add whether the dispute statement has been received # add whether the dispute statement has been received
if data["is_maker"]: if data["is_maker"]:
data["statement_submitted"] = (order.maker_statement != None data["statement_submitted"] = (
and order.maker_statement != "") order.maker_statement != None and order.maker_statement != ""
)
elif data["is_taker"]: elif data["is_taker"]:
data["statement_submitted"] = (order.taker_statement != None data["statement_submitted"] = (
and order.taker_statement != "") 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. # 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 elif (
and order.payout.receiver == request.user order.status == Order.Status.FAI and order.payout.receiver == request.user
): # might not be the buyer if after a dispute where winner wins ): # might not be the buyer if after a dispute where winner wins
data["retries"] = order.payout.routing_attempts data["retries"] = order.payout.routing_attempts
data["next_retry_time"] = order.payout.last_routing_time + timedelta( data["next_retry_time"] = order.payout.last_routing_time + timedelta(
minutes=RETRY_TIME) minutes=RETRY_TIME
)
if order.payout.failure_reason: 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: if order.payout.status == LNPayment.Status.EXPIRE:
data["invoice_expired"] = True data["invoice_expired"] = True
# Add invoice amount once again if invoice was expired. # 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: # 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["public_duration"] = order.public_duration
data["bond_size"] = order.bond_size data["bond_size"] = order.bond_size
data["bondless_taker"] = order.bondless_taker data["bondless_taker"] = order.bondless_taker
# Adds trade summary # 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) valid, context = Logics.summarize_trade(order, request.user)
if valid: if valid:
data = {**data, **context} 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 status is 'Succes' add final stats and txid if it is a swap
if order.status == Order.Status.SUC: if order.status == Order.Status.SUC:
# If buyer and is a swap, add TXID # 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: if order.is_swap:
data["num_satoshis"] = order.payout_tx.num_satoshis data["num_satoshis"] = order.payout_tx.num_satoshis
data["sent_satoshis"] = order.payout_tx.sent_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["txid"] = order.payout_tx.txid
data["network"] = str(config("NETWORK")) data["network"] = str(config("NETWORK"))
return Response(data, status.HTTP_200_OK) return Response(data, status.HTTP_200_OK)
@extend_schema(**OrderViewSchema.take_update_confirm_dispute_cancel) @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! # 1) If action is take, it is a taker request!
if action == "take": if action == "take":
if order.status == Order.Status.PUB: if order.status == Order.Status.PUB:
valid, context, _ = Logics.validate_already_maker_or_taker( valid, context, _ = Logics.validate_already_maker_or_taker(request.user)
request.user)
if not valid: if not valid:
return Response(context, status=status.HTTP_409_CONFLICT) return Response(context, status=status.HTTP_409_CONFLICT)
@ -487,15 +540,15 @@ class OrderView(viewsets.ViewSet):
# 2) If action is 'update invoice' # 2) If action is 'update invoice'
elif action == "update_invoice": elif action == "update_invoice":
valid, context = Logics.update_invoice(order, request.user, valid, context = Logics.update_invoice(order, request.user, invoice)
invoice)
if not valid: if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
# 2.b) If action is 'update address' # 2.b) If action is 'update address'
elif action == "update_address": elif action == "update_address":
valid, context = Logics.update_address(order, request.user, valid, context = Logics.update_address(
address, mining_fee_rate) order, request.user, address, mining_fee_rate
)
if not valid: if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
@ -518,15 +571,13 @@ class OrderView(viewsets.ViewSet):
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
elif action == "submit_statement": elif action == "submit_statement":
valid, context = Logics.dispute_statement(order, request.user, valid, context = Logics.dispute_statement(order, request.user, statement)
statement)
if not valid: if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
# 6) If action is rate # 6) If action is rate
elif action == "rate_user" and rating: elif action == "rate_user" and rating:
valid, context = Logics.rate_counterparty(order, request.user, valid, context = Logics.rate_counterparty(order, request.user, rating)
rating)
if not valid: if not valid:
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
@ -546,10 +597,8 @@ class OrderView(viewsets.ViewSet):
else: else:
return Response( return Response(
{ {
"bad_request": "bad_request": "The Robotic Satoshis working in the warehouse did not understand you. "
"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"
+
"Please, fill a Bug Issue in Github https://github.com/reckless-satoshi/robosats/issues"
}, },
status.HTTP_501_NOT_IMPLEMENTED, status.HTTP_501_NOT_IMPLEMENTED,
) )
@ -558,15 +607,12 @@ class OrderView(viewsets.ViewSet):
class UserView(APIView): class UserView(APIView):
NickGen = NickGenerator(lang="English", NickGen = NickGenerator(
use_adv=False, lang="English", use_adv=False, use_adj=True, use_noun=True, max_num=999
use_adj=True, )
use_noun=True,
max_num=999)
serializer_class = UserGenSerializer serializer_class = UserGenSerializer
def post(self, request, format=None): def post(self, request, format=None):
""" """
Get a new user derived from a high entropy token Get a new user derived from a high entropy token
@ -581,7 +627,7 @@ class UserView(APIView):
context = {} context = {}
serializer = self.serializer_class(data=request.data) 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(): if not serializer.is_valid():
context = {"bad_request": "Invalid serializer"} context = {"bad_request": "Invalid serializer"}
return Response(context, status=status.HTTP_400_BAD_REQUEST) return Response(context, status=status.HTTP_400_BAD_REQUEST)
@ -590,12 +636,15 @@ class UserView(APIView):
if request.user.is_authenticated: if request.user.is_authenticated:
context = {"nickname": request.user.username} context = {"nickname": request.user.username}
not_participant, _, order = Logics.validate_already_maker_or_taker( not_participant, _, order = Logics.validate_already_maker_or_taker(
request.user) request.user
)
# Does not allow this 'mistake' if an active order # Does not allow this 'mistake' if an active order
if not not_participant: if not not_participant:
context["active_order_id"] = order.id 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) return Response(context, status.HTTP_400_BAD_REQUEST)
# The new way. The token is never sent. Only its SHA256 # 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") public_key = serializer.data.get("public_key")
encrypted_private_key = serializer.data.get("encrypted_private_key") encrypted_private_key = serializer.data.get("encrypted_private_key")
ref_code = serializer.data.get("ref_code") ref_code = serializer.data.get("ref_code")
if not public_key or not encrypted_private_key: if not public_key or not encrypted_private_key:
context["bad_request"] = "Must provide valid 'pub' and 'enc_priv' PGP keys" context["bad_request"] = "Must provide valid 'pub' and 'enc_priv' PGP keys"
return Response(context, status.HTTP_400_BAD_REQUEST) 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: if not valid:
return Response(bad_keys_context, status.HTTP_400_BAD_REQUEST) 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 # 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. # 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 pass
# Hash the token_sha256, only 1 iteration. (this is the second SHA256 of the user token, aka RoboSats ID) # 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 # Generate nickname deterministically
nickname = self.NickGen.short_from_SHA256(hash, max_length=18)[0] 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 # Create new credentials and login if nickname is new
if len(User.objects.filter(username=nickname)) == 0: if len(User.objects.filter(username=nickname)) == 0:
User.objects.create_user(username=nickname, User.objects.create_user(
password=token_sha256, username=nickname, password=token_sha256, is_staff=False
is_staff=False) )
user = authenticate(request, username=nickname, password=token_sha256) user = authenticate(request, username=nickname, password=token_sha256)
login(request, user) login(request, user)
context['referral_code'] = token_urlsafe(8) context["referral_code"] = token_urlsafe(8)
user.profile.referral_code = context['referral_code'] user.profile.referral_code = context["referral_code"]
user.profile.avatar = "static/assets/avatars/" + nickname + ".png" 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..." # Let's implement this sanity check "If profile has not keys..."
if not user.profile.public_key: if not user.profile.public_key:
user.profile.public_key = public_key user.profile.public_key = public_key
@ -700,17 +754,21 @@ class UserView(APIView):
context["wants_stealth"] = user.profile.wants_stealth context["wants_stealth"] = user.profile.wants_stealth
# Adds/generate telegram token and whether it is enabled # 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 # 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: if not has_no_active_order:
context["active_order_id"] = order.id context["active_order_id"] = order.id
else: 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: if last_order:
context["last_order_id"] = last_order.id context["last_order_id"] = last_order.id
# Sends the welcome back message, only if created +3 mins ago # Sends the welcome back message, only if created +3 mins ago
if request.user.date_joined < (timezone.now() - timedelta(minutes=3)): if request.user.date_joined < (timezone.now() - timedelta(minutes=3)):
context["found"] = "We found your Robot avatar. Welcome back!" context["found"] = "We found your Robot avatar. Welcome back!"
@ -737,8 +795,7 @@ class UserView(APIView):
if not not_participant: if not not_participant:
return Response( return Response(
{ {
"bad_request": "bad_request": "Maybe a mistake? User cannot be deleted while he is part of an order"
"Maybe a mistake? User cannot be deleted while he is part of an order"
}, },
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
@ -746,8 +803,7 @@ class UserView(APIView):
if user.profile.total_contracts > 0: if user.profile.total_contracts > 0:
return Response( return Response(
{ {
"bad_request": "bad_request": "Maybe a mistake? User cannot be deleted as it has completed trades"
"Maybe a mistake? User cannot be deleted as it has completed trades"
}, },
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
@ -775,12 +831,11 @@ class BookView(ListAPIView):
if int(currency) == 0 and int(type) != 2: if int(currency) == 0 and int(type) != 2:
queryset = Order.objects.filter(type=type, status=Order.Status.PUB) queryset = Order.objects.filter(type=type, status=Order.Status.PUB)
elif int(type) == 2 and int(currency) != 0: elif int(type) == 2 and int(currency) != 0:
queryset = Order.objects.filter(currency=currency, queryset = Order.objects.filter(currency=currency, status=Order.Status.PUB)
status=Order.Status.PUB)
elif not (int(currency) == 0 and int(type) == 2): elif not (int(currency) == 0 and int(type) == 2):
queryset = Order.objects.filter(currency=currency, queryset = Order.objects.filter(
type=type, currency=currency, type=type, status=Order.Status.PUB
status=Order.Status.PUB) )
if len(queryset) == 0: if len(queryset) == 0:
return Response( return Response(
@ -795,11 +850,12 @@ class BookView(ListAPIView):
data["satoshis_now"] = Logics.satoshis_now(order) data["satoshis_now"] = Logics.satoshis_now(order)
# Compute current premium for those orders that are explicitly priced. # Compute current premium for those orders that are explicitly priced.
data["price"], data["premium"] = Logics.price_and_premium_now( data["price"], data["premium"] = Logics.price_and_premium_now(order)
order) data["maker_status"] = Logics.user_activity_status(order.maker_last_seen)
data["maker_status"] = Logics.user_activity_status( for key in (
order.maker_last_seen) "status",
for key in ("status","taker"): # Non participants should not see the status or who is the taker "taker",
): # Non participants should not see the status or who is the taker
del data[key] del data[key]
book_data.append(data) book_data.append(data)
@ -816,18 +872,23 @@ class InfoView(ListAPIView):
context = {} context = {}
context["num_public_buy_orders"] = len( context["num_public_buy_orders"] = len(
Order.objects.filter(type=Order.Types.BUY, Order.objects.filter(type=Order.Types.BUY, status=Order.Status.PUB)
status=Order.Status.PUB)) )
context["num_public_sell_orders"] = len( context["num_public_sell_orders"] = len(
Order.objects.filter(type=Order.Types.SELL, Order.objects.filter(type=Order.Types.SELL, status=Order.Status.PUB)
status=Order.Status.PUB)) )
context["book_liquidity"] = Order.objects.filter(status=Order.Status.PUB).aggregate(Sum('last_satoshis'))['last_satoshis__sum'] context["book_liquidity"] = Order.objects.filter(
context["book_liquidity"] = 0 if context["book_liquidity"] == None else context["book_liquidity"] 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) # Number of active users (logged in in last 30 minutes)
today = datetime.today() today = datetime.today()
context["active_robots_today"] = len( 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 # Compute average premium and volume of today
last_day = timezone.now() - timedelta(days=1) last_day = timezone.now() - timedelta(days=1)
@ -860,11 +921,15 @@ class InfoView(ListAPIView):
context["node_alias"] = config("NODE_ALIAS") context["node_alias"] = config("NODE_ALIAS")
context["node_id"] = config("NODE_ID") context["node_id"] = config("NODE_ID")
context["network"] = config("NETWORK") context["network"] = config("NETWORK")
context["maker_fee"] = float(config("FEE"))*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["taker_fee"] = float(config("FEE")) * (
1 - float(config("MAKER_FEE_SPLIT"))
)
context["bond_size"] = float(config("DEFAULT_BOND_SIZE")) context["bond_size"] = float(config("DEFAULT_BOND_SIZE"))
context["current_swap_fee_rate"] = Logics.compute_swap_fee_rate(BalanceLog.objects.latest('time')) context["current_swap_fee_rate"] = Logics.compute_swap_fee_rate(
BalanceLog.objects.latest("time")
)
if request.user.is_authenticated: if request.user.is_authenticated:
context["nickname"] = request.user.username context["nickname"] = request.user.username
@ -872,13 +937,16 @@ class InfoView(ListAPIView):
context["earned_rewards"] = request.user.profile.earned_rewards context["earned_rewards"] = request.user.profile.earned_rewards
context["wants_stealth"] = request.user.profile.wants_stealth context["wants_stealth"] = request.user.profile.wants_stealth
# Adds/generate telegram token and whether it is enabled # 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( has_no_active_order, _, order = Logics.validate_already_maker_or_taker(
request.user) request.user
)
if not has_no_active_order: if not has_no_active_order:
context["active_order_id"] = order.id context["active_order_id"] = order.id
else: 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: if last_order:
context["last_order_id"] = last_order.id context["last_order_id"] = last_order.id
@ -894,10 +962,7 @@ class RewardView(CreateAPIView):
if not request.user.is_authenticated: if not request.user.is_authenticated:
return Response( 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, status.HTTP_400_BAD_REQUEST,
) )
@ -909,7 +974,7 @@ class RewardView(CreateAPIView):
valid, context = Logics.withdraw_rewards(request.user, invoice) valid, context = Logics.withdraw_rewards(request.user, invoice)
if not valid: if not valid:
context['successful_withdrawal'] = False context["successful_withdrawal"] = False
return Response(context, status.HTTP_400_BAD_REQUEST) return Response(context, status.HTTP_400_BAD_REQUEST)
return Response({"successful_withdrawal": True}, status.HTTP_200_OK) return Response({"successful_withdrawal": True}, status.HTTP_200_OK)
@ -923,17 +988,19 @@ class PriceView(ListAPIView):
def get(self, request): def get(self, request):
payload = {} payload = {}
queryset = Currency.objects.all().order_by('currency') queryset = Currency.objects.all().order_by("currency")
for currency in queryset: for currency in queryset:
code = Currency.currency_dict[str(currency.currency)] code = Currency.currency_dict[str(currency.currency)]
try: try:
last_tick = MarketTick.objects.filter(currency=currency).latest('timestamp') last_tick = MarketTick.objects.filter(currency=currency).latest(
"timestamp"
)
payload[code] = { payload[code] = {
'price': last_tick.price, "price": last_tick.price,
'volume': last_tick.volume, "volume": last_tick.volume,
'premium': last_tick.premium, "premium": last_tick.premium,
'timestamp': last_tick.timestamp, "timestamp": last_tick.timestamp,
} }
except: except:
payload[code] = None payload[code] = None
@ -948,48 +1015,48 @@ class TickView(ListAPIView):
@extend_schema(**TickViewSchema.get) @extend_schema(**TickViewSchema.get)
def get(self, request): 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) return Response(data, status=status.HTTP_200_OK)
class LimitView(ListAPIView): class LimitView(ListAPIView):
@extend_schema(**LimitViewSchema.get) @extend_schema(**LimitViewSchema.get)
def get(self, request): def get(self, request):
# Trade limits as BTC # Trade limits as BTC
min_trade = float(config('MIN_TRADE')) / 100000000 min_trade = float(config("MIN_TRADE")) / 100000000
max_trade = float(config('MAX_TRADE')) / 100000000 max_trade = float(config("MAX_TRADE")) / 100000000
max_bondless_trade = float(config('MAX_TRADE_BONDLESS_TAKER')) / 100000000 max_bondless_trade = float(config("MAX_TRADE_BONDLESS_TAKER")) / 100000000
payload = {} payload = {}
queryset = Currency.objects.all().order_by('currency') queryset = Currency.objects.all().order_by("currency")
for currency in queryset: for currency in queryset:
code = Currency.currency_dict[str(currency.currency)] code = Currency.currency_dict[str(currency.currency)]
exchange_rate = float(currency.exchange_rate) exchange_rate = float(currency.exchange_rate)
payload[currency.currency] = { payload[currency.currency] = {
'code': code, "code": code,
'price': exchange_rate, "price": exchange_rate,
'min_amount': min_trade * exchange_rate, "min_amount": min_trade * exchange_rate,
'max_amount': max_trade * exchange_rate, "max_amount": max_trade * exchange_rate,
'max_bondless_amount': max_bondless_trade * exchange_rate, "max_bondless_amount": max_bondless_trade * exchange_rate,
} }
return Response(payload, status.HTTP_200_OK) return Response(payload, status.HTTP_200_OK)
class HistoricalView(ListAPIView): class HistoricalView(ListAPIView):
@extend_schema(**HistoricalViewSchema.get) @extend_schema(**HistoricalViewSchema.get)
def get(self, request): def get(self, request):
payload = {} payload = {}
queryset = AccountingDay.objects.all().order_by('day') queryset = AccountingDay.objects.all().order_by("day")
for accounting_day in queryset: for accounting_day in queryset:
payload[str(accounting_day.day)] = { payload[str(accounting_day.day)] = {
'volume': accounting_day.contracted, "volume": accounting_day.contracted,
'num_contracts': accounting_day.num_contracts, "num_contracts": accounting_day.num_contracts,
} }
return Response(payload, status.HTTP_200_OK) return Response(payload, status.HTTP_200_OK)
@ -998,16 +1065,14 @@ class HistoricalView(ListAPIView):
class StealthView(UpdateAPIView): class StealthView(UpdateAPIView):
serializer_class = StealthSerializer serializer_class = StealthSerializer
@extend_schema(**StealthViewSchema.put) @extend_schema(**StealthViewSchema.put)
def put(self, request): def put(self, request):
serializer = self.serializer_class(data=request.data) serializer = self.serializer_class(data=request.data)
if not request.user.is_authenticated: if not request.user.is_authenticated:
return Response( 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, status.HTTP_400_BAD_REQUEST,
) )

View File

@ -1,6 +1,7 @@
from django.contrib import admin from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin from django_admin_relation_links import AdminChangeLinksMixin
from chat.models import ChatRoom, Message from chat.models import ChatRoom, Message
# Register your models here. # Register your models here.
@ -17,9 +18,10 @@ class ChatRoomAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"taker_connect_date", "taker_connect_date",
"room_group_name", "room_group_name",
) )
change_links = ["order","maker","taker"] change_links = ["order", "maker", "taker"]
search_fields = ["id"] search_fields = ["id"]
@admin.register(Message) @admin.register(Message)
class MessageAdmin(AdminChangeLinksMixin, admin.ModelAdmin): class MessageAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = ( list_display = (
@ -31,7 +33,7 @@ class MessageAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"receiver_link", "receiver_link",
"created_at", "created_at",
) )
change_links = ["chatroom","order","sender","receiver"] change_links = ["chatroom", "order", "sender", "receiver"]
search_fields = ["id","index"] search_fields = ["id", "index"]
ordering = ["-chatroom_id","-index"] ordering = ["-chatroom_id", "-index"]
list_filter = ("chatroom",) list_filter = ("chatroom",)

View File

@ -6,8 +6,8 @@ from asgiref.sync import async_to_sync
import json import json
class ChatRoomConsumer(AsyncWebsocketConsumer):
class ChatRoomConsumer(AsyncWebsocketConsumer):
@database_sync_to_async @database_sync_to_async
def allow_in_chatroom(self): def allow_in_chatroom(self):
order = Order.objects.get(id=self.order_id) order = Order.objects.get(id=self.order_id)
@ -23,37 +23,37 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
@database_sync_to_async @database_sync_to_async
def save_connect_user(self): 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) order = Order.objects.get(id=self.order_id)
if order.maker == self.user: if order.maker == self.user:
ChatRoom.objects.update_or_create( ChatRoom.objects.update_or_create(
id=self.order_id, id=self.order_id,
order=order, order=order,
room_group_name=self.room_group_name, room_group_name=self.room_group_name,
defaults={ defaults={
"maker": self.user, "maker": self.user,
"maker_connected": True, "maker_connected": True,
} },
) )
elif order.taker == self.user: elif order.taker == self.user:
ChatRoom.objects.update_or_create( ChatRoom.objects.update_or_create(
id=self.order_id, id=self.order_id,
order=order, order=order,
room_group_name=self.room_group_name, room_group_name=self.room_group_name,
defaults={ defaults={
"taker": self.user, "taker": self.user,
"taker_connected": True, "taker_connected": True,
} },
) )
return None return None
@database_sync_to_async @database_sync_to_async
def save_new_PGP_message(self, PGP_message): def save_new_PGP_message(self, PGP_message):
'''Creates a Message object''' """Creates a Message object"""
order = Order.objects.get(id=self.order_id) order = Order.objects.get(id=self.order_id)
chatroom = ChatRoom.objects.get(order=order) chatroom = ChatRoom.objects.get(order=order)
@ -71,39 +71,33 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
receiver = order.taker receiver = order.taker
msg_obj = Message.objects.create( msg_obj = Message.objects.create(
order=order, order=order,
chatroom=chatroom, chatroom=chatroom,
index=index, index=index,
sender=sender, sender=sender,
receiver=receiver, receiver=receiver,
PGP_message=PGP_message, PGP_message=PGP_message,
) )
return msg_obj return msg_obj
@database_sync_to_async @database_sync_to_async
def save_disconnect_user(self): 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) order = Order.objects.get(id=self.order_id)
if order.maker == self.user: if order.maker == self.user:
ChatRoom.objects.update_or_create( ChatRoom.objects.update_or_create(
id=self.order_id, id=self.order_id, defaults={"maker_connected": False}
defaults={ )
"maker_connected": False
}
)
elif order.taker == self.user: elif order.taker == self.user:
ChatRoom.objects.update_or_create( ChatRoom.objects.update_or_create(
id=self.order_id, id=self.order_id, defaults={"taker_connected": False}
defaults={ )
"taker_connected": False
}
)
return None return None
@database_sync_to_async @database_sync_to_async
def is_peer_connected(self): 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) chatroom = ChatRoom.objects.get(id=self.order_id)
@ -115,7 +109,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
@database_sync_to_async @database_sync_to_async
def get_peer_PGP_public_key(self): 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) order = Order.objects.get(id=self.order_id)
@ -127,19 +121,21 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
@database_sync_to_async @database_sync_to_async
def get_all_PGP_messages(self): def get_all_PGP_messages(self):
'''Returns all PGP messages''' """Returns all PGP messages"""
order = Order.objects.get(id=self.order_id) order = Order.objects.get(id=self.order_id)
messages = Message.objects.filter(order=order) messages = Message.objects.filter(order=order)
msgs = [] msgs = []
for message in messages: for message in messages:
msgs.append({ msgs.append(
"index": message.index, {
"time": str(message.created_at), "index": message.index,
"message": message.PGP_message, "time": str(message.created_at),
"nick": str(message.sender), "message": message.PGP_message,
}) "nick": str(message.sender),
}
)
return msgs return msgs
@ -153,8 +149,7 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
if allowed: if allowed:
await self.save_connect_user() await self.save_connect_user()
await self.channel_layer.group_add(self.room_group_name, await self.channel_layer.group_add(self.room_group_name, self.channel_name)
self.channel_name)
await self.accept() await self.accept()
@ -173,13 +168,12 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
async def disconnect(self, close_code): async def disconnect(self, close_code):
await self.save_disconnect_user() await self.save_disconnect_user()
await self.channel_layer.group_discard(self.room_group_name, await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
self.channel_name)
await self.channel_layer.group_send( await self.channel_layer.group_send(
self.room_group_name, self.room_group_name,
{ {
"type": "chatroom_message", "type": "chatroom_message",
"message": 'peer-disconnected', "message": "peer-disconnected",
"nick": self.scope["user"].username, "nick": self.scope["user"].username,
"peer_connected": False, "peer_connected": False,
}, },
@ -189,9 +183,9 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
text_data_json = json.loads(text_data) text_data_json = json.loads(text_data)
message = text_data_json["message"] message = text_data_json["message"]
peer_connected = await self.is_peer_connected() peer_connected = await self.is_peer_connected()
# Encrypted messages are stored. They are served later when a user reconnects. # 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 # save to database
msg_obj = await self.save_new_PGP_message(message) msg_obj = await self.save_new_PGP_message(message)
@ -210,9 +204,9 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
"peer_connected": peer_connected, "peer_connected": peer_connected,
}, },
) )
# Encrypted messages are served when the user requests them # 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. # If there is any stored message, serve them.
msgs = await self.get_all_PGP_messages() msgs = await self.get_all_PGP_messages()
peer_connected = await self.is_peer_connected() peer_connected = await self.is_peer_connected()
@ -221,10 +215,10 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
self.room_group_name, self.room_group_name,
{ {
"type": "PGP_message", "type": "PGP_message",
"index": msg['index'], "index": msg["index"],
"time": msg['time'], "time": msg["time"],
"message": msg['message'], "message": msg["message"],
"nick": msg['nick'], "nick": msg["nick"],
"peer_connected": peer_connected, "peer_connected": peer_connected,
}, },
) )
@ -245,11 +239,15 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
nick = event["nick"] nick = event["nick"]
peer_connected = event["peer_connected"] peer_connected = event["peer_connected"]
await self.send(text_data=json.dumps({ await self.send(
"message": message, text_data=json.dumps(
"user_nick": nick, {
"peer_connected": peer_connected, "message": message,
})) "user_nick": nick,
"peer_connected": peer_connected,
}
)
)
async def PGP_message(self, event): async def PGP_message(self, event):
message = event["message"] message = event["message"]
@ -258,10 +256,14 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
peer_connected = event["peer_connected"] peer_connected = event["peer_connected"]
time = event["time"] time = event["time"]
await self.send(text_data=json.dumps({ await self.send(
"index": index, text_data=json.dumps(
"message": message, {
"user_nick": nick, "index": index,
"peer_connected": peer_connected, "message": message,
"time":time, "user_nick": nick,
})) "peer_connected": peer_connected,
"time": time,
}
)
)

View File

@ -3,24 +3,29 @@ from api.models import User, Order
from django.utils import timezone from django.utils import timezone
import uuid 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 = models.ForeignKey(
Order, Order,
related_name="chatroom", related_name="chatroom",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
default=None) default=None,
)
maker = models.ForeignKey( maker = models.ForeignKey(
User, User,
related_name="chat_maker", related_name="chat_maker",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
default=None) default=None,
)
taker = models.ForeignKey( taker = models.ForeignKey(
User, User,
related_name="chat_taker", related_name="chat_taker",
@ -30,8 +35,8 @@ class ChatRoom(models.Model):
blank=True, blank=True,
) )
maker_connected = models.BooleanField(default=False, null=False) maker_connected = models.BooleanField(default=False, null=False)
taker_connected = models.BooleanField(default=False, null=False) taker_connected = models.BooleanField(default=False, null=False)
maker_connect_date = models.DateTimeField(auto_now_add=True) maker_connect_date = models.DateTimeField(auto_now_add=True)
taker_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): def __str__(self):
return f"Chat:{str(self.id)}" return f"Chat:{str(self.id)}"
class Message(models.Model): class Message(models.Model):
class Meta: class Meta:
get_latest_by = 'index' get_latest_by = "index"
# id = models.PositiveBigIntegerField(primary_key=True, default=uuid.uuid4, editable=False) # id = models.PositiveBigIntegerField(primary_key=True, default=uuid.uuid4, editable=False)
order = models.ForeignKey( order = models.ForeignKey(
Order, Order, related_name="message", on_delete=models.CASCADE, null=True, default=None
related_name="message", )
on_delete=models.CASCADE,
null=True,
default=None)
chatroom = models.ForeignKey( chatroom = models.ForeignKey(
ChatRoom, ChatRoom,
related_name="chatroom", related_name="chatroom",
on_delete=models.CASCADE, on_delete=models.CASCADE,
null=True, null=True,
default=None) default=None,
index = models.PositiveIntegerField(null=False,default=None, blank=True) )
index = models.PositiveIntegerField(null=False, default=None, blank=True)
sender = models.ForeignKey( sender = models.ForeignKey(
User, User,
related_name="message_sender", related_name="message_sender",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
default=None) default=None,
)
receiver = models.ForeignKey( receiver = models.ForeignKey(
User, User,
related_name="message_receiver", related_name="message_receiver",
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
null=True, null=True,
default=None) default=None,
)
PGP_message = models.TextField(max_length=5000, PGP_message = models.TextField(max_length=5000, null=True, default=None, blank=True)
null=True,
default=None,
blank=True)
created_at = models.DateTimeField(default=timezone.now) created_at = models.DateTimeField(default=timezone.now)

View File

@ -2,6 +2,5 @@ from django.urls import re_path
from . import consumers from . import consumers
websocket_urlpatterns = [ websocket_urlpatterns = [
re_path(r"ws/chat/(?P<order_id>\w+)/$", re_path(r"ws/chat/(?P<order_id>\w+)/$", consumers.ChatRoomConsumer.as_asgi()),
consumers.ChatRoomConsumer.as_asgi()),
] ]

View File

@ -1,8 +1,8 @@
from rest_framework import serializers from rest_framework import serializers
from chat.models import Message from chat.models import Message
class ChatSerializer(serializers.ModelSerializer):
class ChatSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Message model = Message
fields = ( fields = (
@ -13,14 +13,17 @@ class ChatSerializer(serializers.ModelSerializer):
) )
depth = 0 depth = 0
class PostMessageSerializer(serializers.ModelSerializer): class PostMessageSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = Message model = Message
fields = ("PGP_message","order","offset") fields = ("PGP_message", "order", "offset")
depth = 0 depth = 0
offset = serializers.IntegerField(allow_null=True, offset = serializers.IntegerField(
default=None, allow_null=True,
required=False, default=None,
min_value=0, required=False,
help_text="Offset for message index to get as response") min_value=0,
help_text="Offset for message index to get as response",
)

View File

@ -1,9 +1,10 @@
from celery import shared_task from celery import shared_task
@shared_task(name="chatrooms_cleansing") @shared_task(name="chatrooms_cleansing")
def 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. that have completely finished more than 3 days ago.
""" """
@ -12,24 +13,28 @@ def chatrooms_cleansing():
from datetime import timedelta from datetime import timedelta
from django.utils import timezone from django.utils import timezone
finished_states = [Order.Status.SUC, finished_states = [
Order.Status.TLD, Order.Status.SUC,
Order.Status.MLD, Order.Status.TLD,
Order.Status.CCA, Order.Status.MLD,
Order.Status.UCA] Order.Status.CCA,
Order.Status.UCA,
]
# Orders that have expired more than 3 days ago # 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. # until encrypted messages are deleted.
finished_time = timezone.now() - timedelta(days=3) 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. # And do not have an active trade, any past contract or any reward.
deleted_chatrooms = [] deleted_chatrooms = []
for order in queryset: for order in queryset:
# Try an except. In case some chatroom is already missing. # Try an except. In case some chatroom is already missing.
try: try:
chatroom = ChatRoom.objects.get(id = order.id) chatroom = ChatRoom.objects.get(id=order.id)
deleted_chatrooms.append(str(chatroom)) deleted_chatrooms.append(str(chatroom))
chatroom.delete() chatroom.delete()
except: except:
@ -39,4 +44,4 @@ def chatrooms_cleansing():
"num_deleted": len(deleted_chatrooms), "num_deleted": len(deleted_chatrooms),
"deleted_chatrooms": deleted_chatrooms, "deleted_chatrooms": deleted_chatrooms,
} }
return results return results

View File

@ -10,11 +10,14 @@ from django.utils import timezone
from asgiref.sync import async_to_sync from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer from channels.layers import get_channel_layer
class ChatView(viewsets.ViewSet): class ChatView(viewsets.ViewSet):
serializer_class = PostMessageSerializer 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): def get(self, request, format=None):
""" """
@ -26,63 +29,57 @@ class ChatView(viewsets.ViewSet):
if order_id is None: if order_id is None:
return Response( return Response(
{ {"bad_request": "Order ID does not exist"},
"bad_request":
"Order ID does not exist"
},
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
order = Order.objects.get(id=order_id) order = Order.objects.get(id=order_id)
if not (request.user == order.maker or request.user == order.taker): if not (request.user == order.maker or request.user == order.taker):
return Response( 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, status.HTTP_400_BAD_REQUEST,
) )
if not order.status in [Order.Status.CHA, Order.Status.FSE]: if not order.status in [Order.Status.CHA, Order.Status.FSE]:
return Response( return Response(
{ {"bad_request": "Order is not in chat status"},
"bad_request":
"Order is not in chat status"
},
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
queryset = Message.objects.filter(order=order, index__gt=offset) queryset = Message.objects.filter(order=order, index__gt=offset)
chatroom = ChatRoom.objects.get(order=order) 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: 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.maker_connected = True
chatroom.save() chatroom.save()
peer_connected = chatroom.taker_connected peer_connected = chatroom.taker_connected
elif chatroom.taker == request.user: 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.taker_connected = True
chatroom.save() chatroom.save()
peer_connected = chatroom.maker_connected peer_connected = chatroom.maker_connected
messages = [] messages = []
for message in queryset: for message in queryset:
d = ChatSerializer(message).data d = ChatSerializer(message).data
print(d) print(d)
# Re-serialize so the response is identical to the consumer message # Re-serialize so the response is identical to the consumer message
data = { data = {
'index':d['index'], "index": d["index"],
'time':d['created_at'], "time": d["created_at"],
'message':d['PGP_message'], "message": d["PGP_message"],
'nick': User.objects.get(id=d['sender']).username "nick": User.objects.get(id=d["sender"]).username,
} }
messages.append(data) messages.append(data)
response = {'peer_connected': peer_connected, 'messages':messages} response = {"peer_connected": peer_connected, "messages": messages}
return Response(response, status.HTTP_200_OK) return Response(response, status.HTTP_200_OK)
@ -92,7 +89,7 @@ class ChatView(viewsets.ViewSet):
""" """
serializer = self.serializer_class(data=request.data) 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(): if not serializer.is_valid():
context = {"bad_request": "Invalid serializer"} context = {"bad_request": "Invalid serializer"}
return Response(context, status=status.HTTP_400_BAD_REQUEST) return Response(context, status=status.HTTP_400_BAD_REQUEST)
@ -102,30 +99,21 @@ class ChatView(viewsets.ViewSet):
if order_id is None: if order_id is None:
return Response( return Response(
{ {"bad_request": "Order ID does not exist"},
"bad_request":
"Order ID does not exist"
},
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
order = Order.objects.get(id=order_id) order = Order.objects.get(id=order_id)
if not (request.user == order.maker or request.user == order.taker): if not (request.user == order.maker or request.user == order.taker):
return Response( 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, status.HTTP_400_BAD_REQUEST,
) )
if not order.status in [Order.Status.CHA, Order.Status.FSE]: if not order.status in [Order.Status.CHA, Order.Status.FSE]:
return Response( return Response(
{ {"bad_request": "Order is not in chat status"},
"bad_request":
"Order is not in chat status"
},
status.HTTP_400_BAD_REQUEST, status.HTTP_400_BAD_REQUEST,
) )
@ -137,26 +125,26 @@ class ChatView(viewsets.ViewSet):
receiver = order.maker receiver = order.maker
chatroom, _ = ChatRoom.objects.get_or_create( chatroom, _ = ChatRoom.objects.get_or_create(
id=order_id, id=order_id,
order=order, order=order,
room_group_name=f"chat_order_{order_id}", room_group_name=f"chat_order_{order_id}",
defaults={ defaults={
"maker": order.maker, "maker": order.maker,
"maker_connected": order.maker == request.user, "maker_connected": order.maker == request.user,
"taker": order.taker, "taker": order.taker,
"taker_connected": order.taker == request.user, "taker_connected": order.taker == request.user,
} },
) )
last_index = Message.objects.filter(order=order, chatroom=chatroom).count() last_index = Message.objects.filter(order=order, chatroom=chatroom).count()
new_message = Message.objects.create( new_message = Message.objects.create(
index=last_index+1, index=last_index + 1,
PGP_message=serializer.data.get("PGP_message"), PGP_message=serializer.data.get("PGP_message"),
order=order, order=order,
chatroom=chatroom, chatroom=chatroom,
sender=sender, sender=sender,
receiver=receiver, receiver=receiver,
) )
# Send websocket message # Send websocket message
if chatroom.maker == request.user: if chatroom.maker == request.user:
@ -168,13 +156,13 @@ class ChatView(viewsets.ViewSet):
async_to_sync(channel_layer.group_send)( async_to_sync(channel_layer.group_send)(
f"chat_order_{order_id}", f"chat_order_{order_id}",
{ {
"type": "PGP_message", "type": "PGP_message",
"index": new_message.index, "index": new_message.index,
"message": new_message.PGP_message, "message": new_message.PGP_message,
"time": str(new_message.created_at), "time": str(new_message.created_at),
"nick": new_message.sender.username, "nick": new_message.sender.username,
"peer_connected": peer_connected, "peer_connected": peer_connected,
} },
) )
# if offset is given, reply with messages # if offset is given, reply with messages
@ -187,16 +175,15 @@ class ChatView(viewsets.ViewSet):
print(d) print(d)
# Re-serialize so the response is identical to the consumer message # Re-serialize so the response is identical to the consumer message
data = { data = {
'index':d['index'], "index": d["index"],
'time':d['created_at'], "time": d["created_at"],
'message':d['PGP_message'], "message": d["PGP_message"],
'nick': User.objects.get(id=d['sender']).username "nick": User.objects.get(id=d["sender"]).username,
} }
messages.append(data) messages.append(data)
response = {'peer_connected': peer_connected, 'messages':messages} response = {"peer_connected": peer_connected, "messages": messages}
else: else:
response = {} response = {}
return Response(response, status.HTTP_200_OK) return Response(response, status.HTTP_200_OK)

View File

@ -4,6 +4,7 @@ from import_export.admin import ImportExportModelAdmin
# Register your models here. # Register your models here.
@admin.register(AccountingDay) @admin.register(AccountingDay)
class AccountingDayAdmin(ImportExportModelAdmin): class AccountingDayAdmin(ImportExportModelAdmin):
@ -29,6 +30,7 @@ class AccountingDayAdmin(ImportExportModelAdmin):
change_links = ["day"] change_links = ["day"]
search_fields = ["day"] search_fields = ["day"]
@admin.register(BalanceLog) @admin.register(BalanceLog)
class BalanceLogAdmin(ImportExportModelAdmin): class BalanceLogAdmin(ImportExportModelAdmin):
@ -57,4 +59,4 @@ class BalanceLogAdmin(ImportExportModelAdmin):
"ln_remote_unsettled", "ln_remote_unsettled",
] ]
change_links = ["time"] change_links = ["time"]
search_fields = ["time"] search_fields = ["time"]

View File

@ -2,5 +2,5 @@ from django.apps import AppConfig
class ControlConfig(AppConfig): class ControlConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = "django.db.models.BigAutoField"
name = 'control' name = "control"

View File

@ -3,79 +3,129 @@ from django.utils import timezone
from api.lightning.node import LNNode from api.lightning.node import LNNode
class AccountingDay(models.Model): class AccountingDay(models.Model):
day = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False) day = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False)
# Every field is denominated in Sats with (3 decimals for millisats) # Every field is denominated in Sats with (3 decimals for millisats)
# Total volume contracted # 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 # Number of contracts
num_contracts = models.BigIntegerField(default=0, null=False, blank=False) num_contracts = models.BigIntegerField(default=0, null=False, blank=False)
# Net volume of trading invoices settled (excludes disputes) # 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 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 # 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 # 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 # 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 # 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 # 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 # Total inflows minus outflows and routing fees
cashflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False) cashflow = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Balance on earned rewards (referral rewards, slashed bonds and solved disputes) # Balance on earned rewards (referral rewards, slashed bonds and solved disputes)
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) # 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 # 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) # 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) # 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 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): class BalanceLog(models.Model):
def get_total(): 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(): def get_frac():
return LNNode.wallet_balance()['total_balance'] / (LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance']) return LNNode.wallet_balance()["total_balance"] / (
def get_oc_total(): LNNode.wallet_balance()["total_balance"]
return LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()["local_balance"]
def get_oc_conf(): )
return LNNode.wallet_balance()['confirmed_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(): def get_oc_unconf():
return LNNode.wallet_balance()['unconfirmed_balance'] return LNNode.wallet_balance()["unconfirmed_balance"]
def get_ln_local(): def get_ln_local():
return LNNode.channel_balance()['local_balance'] return LNNode.channel_balance()["local_balance"]
def get_ln_remote(): def get_ln_remote():
return LNNode.channel_balance()['remote_balance'] return LNNode.channel_balance()["remote_balance"]
def get_ln_local_unsettled(): def get_ln_local_unsettled():
return LNNode.channel_balance()['unsettled_local_balance'] return LNNode.channel_balance()["unsettled_local_balance"]
def get_ln_remote_unsettled(): 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) time = models.DateTimeField(primary_key=True, default=timezone.now)
# Every field is denominated in Sats # Every field is denominated in Sats
total = models.PositiveBigIntegerField(default=get_total) 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_total = models.PositiveBigIntegerField(default=get_oc_total)
onchain_confirmed = models.PositiveBigIntegerField(default=get_oc_conf) onchain_confirmed = models.PositiveBigIntegerField(default=get_oc_conf)
onchain_unconfirmed = models.PositiveBigIntegerField(default=get_oc_unconf) onchain_unconfirmed = models.PositiveBigIntegerField(default=get_oc_unconf)
ln_local = models.PositiveBigIntegerField(default=get_ln_local) ln_local = models.PositiveBigIntegerField(default=get_ln_local)
ln_remote = models.PositiveBigIntegerField(default=get_ln_remote) ln_remote = models.PositiveBigIntegerField(default=get_ln_remote)
ln_local_unsettled = models.PositiveBigIntegerField(default=get_ln_local_unsettled) 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): def __str__(self):
return f"Balance at {self.time.strftime('%d/%m/%Y %H:%M:%S')}" return f"Balance at {self.time.strftime('%d/%m/%Y %H:%M:%S')}"
class Dispute(models.Model): class Dispute(models.Model):
pass pass

View File

@ -1,10 +1,11 @@
from celery import shared_task from celery import shared_task
@shared_task(name="do_accounting") @shared_task(name="do_accounting")
def do_accounting(): def do_accounting():
''' """
Does all accounting from the beginning of time Does all accounting from the beginning of time
''' """
from api.models import Order, LNPayment, OnchainPayment, Profile, MarketTick from api.models import Order, LNPayment, OnchainPayment, Profile, MarketTick
from control.models import AccountingDay from control.models import AccountingDay
@ -18,61 +19,84 @@ def do_accounting():
today = timezone.now().date() today = timezone.now().date()
try: try:
last_accounted_day = AccountingDay.objects.latest('day').day.date() last_accounted_day = AccountingDay.objects.latest("day").day.date()
accounted_yesterday = AccountingDay.objects.latest('day') accounted_yesterday = AccountingDay.objects.latest("day")
except: except:
last_accounted_day = None last_accounted_day = None
accounted_yesterday = None accounted_yesterday = None
if last_accounted_day == today: if last_accounted_day == today:
return {'message':'no days to account for'} return {"message": "no days to account for"}
elif last_accounted_day != None: elif last_accounted_day != None:
initial_day = last_accounted_day + timedelta(days=1) initial_day = last_accounted_day + timedelta(days=1)
elif last_accounted_day == None: 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 day = initial_day
result = {} result = {}
while day <= today: while day <= today:
day_payments = all_payments.filter(created_at__gte=day,created_at__lte=day+timedelta(days=1)) day_payments = all_payments.filter(
day_onchain_payments = OnchainPayment.objects.filter(created_at__gte=day,created_at__lte=day+timedelta(days=1)) 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_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 # Coarse accounting based on LNpayment and OnchainPayment objects
contracted = day_ticks.aggregate(Sum('volume'))['volume__sum'] contracted = day_ticks.aggregate(Sum("volume"))["volume__sum"]
num_contracts = day_ticks.count() num_contracts = day_ticks.count()
inflow = day_payments.filter(type=LNPayment.Types.HOLD,status=LNPayment.Status.SETLED).aggregate(Sum('num_satoshis'))['num_satoshis__sum'] inflow = day_payments.filter(
onchain_outflow = day_onchain_payments.filter(status__in=[OnchainPayment.Status.MEMPO,OnchainPayment.Status.CONFI]).aggregate(Sum('sent_satoshis'))['sent_satoshis__sum'] 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) 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) 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'] routing_fees = day_payments.filter(
mining_fees = day_onchain_payments.filter(status__in=[OnchainPayment.Status.MEMPO,OnchainPayment.Status.CONFI]).aggregate(Sum('mining_fee_sats'))['mining_fee_sats__sum'] type=LNPayment.Types.NORM, status=LNPayment.Status.SUCCED
rewards_claimed = day_payments.filter(type=LNPayment.Types.NORM,concept=LNPayment.Concepts.WITHREWA,status=LNPayment.Status.SUCCED).aggregate(Sum('num_satoshis'))['num_satoshis__sum'] ).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 contracted = 0 if contracted == None else contracted
inflow = 0 if inflow == None else inflow inflow = 0 if inflow == None else inflow
outflow = offchain_outflow + onchain_outflow 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 rewards_claimed = 0 if rewards_claimed == None else rewards_claimed
mining_fees = 0 if mining_fees == None else mining_fees mining_fees = 0 if mining_fees == None else mining_fees
accounted_day = AccountingDay.objects.create( accounted_day = AccountingDay.objects.create(
day = day, day=day,
contracted = contracted, contracted=contracted,
num_contracts = num_contracts, num_contracts=num_contracts,
inflow = inflow, inflow=inflow,
outflow = outflow, outflow=outflow,
routing_fees = routing_fees, routing_fees=routing_fees,
mining_fees = mining_fees, mining_fees=mining_fees,
cashflow = inflow - outflow - routing_fees, cashflow=inflow - outflow - routing_fees,
rewards_claimed = rewards_claimed, rewards_claimed=rewards_claimed,
) )
# Fine Net Daily accounting based on orders # Fine Net Daily accounting based on orders
# Only account for orders where everything worked out right # 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 escrows_settled = 0
payouts_paid = 0 payouts_paid = 0
costs = 0 costs = 0
@ -80,31 +104,42 @@ def do_accounting():
escrows_settled += int(payout.order_paid_LN.trade_escrow.num_satoshis) escrows_settled += int(payout.order_paid_LN.trade_escrow.num_satoshis)
payouts_paid += int(payout.num_satoshis) payouts_paid += int(payout.num_satoshis)
costs += int(payout.fee) costs += int(payout.fee)
# Same for orders that use onchain payments. # 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: for payout_tx in payouts_tx:
escrows_settled += int(payout_tx.order_paid_TX.trade_escrow.num_satoshis) escrows_settled += int(payout_tx.order_paid_TX.trade_escrow.num_satoshis)
payouts_paid += int(payout_tx.sent_satoshis) payouts_paid += int(payout_tx.sent_satoshis)
costs += int(payout_tx.mining_fee_sats) costs += int(payout_tx.mining_fee_sats)
# account for those orders where bonds were lost # account for those orders where bonds were lost
# + Settled bonds / bond_split # + 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: 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: else:
collected_slashed_bonds = 0 collected_slashed_bonds = 0
accounted_day.net_settled = escrows_settled + collected_slashed_bonds accounted_day.net_settled = escrows_settled + collected_slashed_bonds
accounted_day.net_paid = payouts_paid + costs accounted_day.net_paid = payouts_paid + costs
accounted_day.net_balance = float(accounted_day.net_settled) - float(accounted_day.net_paid) accounted_day.net_balance = float(accounted_day.net_settled) - float(
accounted_day.net_paid
)
# Differential accounting based on change of outstanding states and disputes unreslved # Differential accounting based on change of outstanding states and disputes unreslved
if day == today: 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: if len(pending_disputes) > 0:
outstanding_pending_disputes = 0 outstanding_pending_disputes = 0
for order in pending_disputes: for order in pending_disputes:
@ -112,28 +147,44 @@ def do_accounting():
else: else:
outstanding_pending_disputes = 0 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.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: if accounted_yesterday != None:
accounted_day.earned_rewards = accounted_day.outstanding_earned_rewards - accounted_yesterday.outstanding_earned_rewards accounted_day.earned_rewards = (
accounted_day.disputes = outstanding_pending_disputes - accounted_yesterday.outstanding_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 # Close the loop
accounted_day.save() accounted_day.save()
accounted_yesterday = accounted_day 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) day = day + timedelta(days=1)
return result return result
@shared_task(name="compute_node_balance", ignore_result=True) @shared_task(name="compute_node_balance", ignore_result=True)
def compute_node_balance(): def compute_node_balance():
''' """
Queries LND for channel and wallet balance Queries LND for channel and wallet balance
''' """
from control.models import BalanceLog from control.models import BalanceLog
BalanceLog.objects.create() BalanceLog.objects.create()
return return

View File

@ -4,4 +4,4 @@ from __future__ import absolute_import, unicode_literals
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
from .celery import app as celery_app from .celery import app as celery_app
__all__ = ("celery_app", ) __all__ = ("celery_app",)

View File

@ -35,11 +35,11 @@ app.conf.beat_schedule = {
"task": "users_cleansing", "task": "users_cleansing",
"schedule": crontab(hour=0, minute=0), "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", "task": "chatrooms_cleansing",
"schedule": crontab(hour=0, minute=0), "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", "task": "payments_cleansing",
"schedule": crontab(hour=0, minute=0), "schedule": crontab(hour=0, minute=0),
}, },
@ -55,10 +55,10 @@ app.conf.beat_schedule = {
"task": "cache_external_market_prices", "task": "cache_external_market_prices",
"schedule": timedelta(seconds=60), "schedule": timedelta(seconds=60),
}, },
"compute-node-balance": { # Logs LND channel and wallet balance "compute-node-balance": { # Logs LND channel and wallet balance
"task":"compute_node_balance", "task": "compute_node_balance",
"schedule": timedelta(minutes=60), "schedule": timedelta(minutes=60),
} },
} }
app.conf.timezone = "UTC" app.conf.timezone = "UTC"

View File

@ -2,11 +2,13 @@ from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing import chat.routing
application = ProtocolTypeRouter({ application = ProtocolTypeRouter(
"websocket": {
AuthMiddlewareStack( "websocket": AuthMiddlewareStack(
URLRouter( URLRouter(
chat.routing.websocket_urlpatterns, chat.routing.websocket_urlpatterns,
# TODO add api.routing.websocket_urlpatterns when Order page works with websocket # TODO add api.routing.websocket_urlpatterns when Order page works with websocket
)), )
}) ),
}
)

View File

@ -53,21 +53,21 @@ SESSION_COOKIE_HTTPONLY = False
# Logging settings # Logging settings
if os.environ.get("LOG_TO_CONSOLE"): if os.environ.get("LOG_TO_CONSOLE"):
LOGGING = { LOGGING = {
'version': 1, "version": 1,
'disable_existing_loggers': False, "disable_existing_loggers": False,
'handlers': { "handlers": {
'console': { "console": {
'class': 'logging.StreamHandler', "class": "logging.StreamHandler",
}, },
}, },
'root': { "root": {
'handlers': ['console'], "handlers": ["console"],
'level': 'WARNING', "level": "WARNING",
}, },
'loggers': { "loggers": {
'api.utils': { "api.utils": {
'handlers': ['console'], "handlers": ["console"],
'level': 'WARNING', "level": "WARNING",
}, },
}, },
} }
@ -95,12 +95,12 @@ INSTALLED_APPS = [
] ]
REST_FRAMEWORK = { REST_FRAMEWORK = {
'DEFAULT_SCHEMA_CLASS': 'drf_spectacular.openapi.AutoSchema', "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema",
} }
SPECTACULAR_SETTINGS = { SPECTACULAR_SETTINGS = {
'TITLE': 'RoboSats REST API v0', "TITLE": "RoboSats REST API v0",
'DESCRIPTION': textwrap.dedent( "DESCRIPTION": textwrap.dedent(
""" """
REST API Documentation for [RoboSats](https://learn.robosats.com) - A Simple and Private LN P2P Exchange 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', "VERSION": "0.1.0",
'SERVE_INCLUDE_SCHEMA': False, "SERVE_INCLUDE_SCHEMA": False,
'SWAGGER_UI_DIST': 'SIDECAR', # shorthand to use the sidecar instead "SWAGGER_UI_DIST": "SIDECAR", # shorthand to use the sidecar instead
'SWAGGER_UI_FAVICON_HREF': 'SIDECAR', "SWAGGER_UI_FAVICON_HREF": "SIDECAR",
'REDOC_UI_SETTINGS': { "REDOC_UI_SETTINGS": {
'expandResponses': '200,201', "expandResponses": "200,201",
}, },
'EXTENSIONS_INFO': { "EXTENSIONS_INFO": {
'x-logo': { "x-logo": {
'url': 'https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats-0.1.1-banner.png', "url": "https://raw.githubusercontent.com/Reckless-Satoshi/robosats/main/frontend/static/assets/images/robosats-0.1.1-banner.png",
'backgroundColor': '#FFFFFF', "backgroundColor": "#FFFFFF",
'altText': 'RoboSats logo' "altText": "RoboSats logo",
} }
}, },
'REDOC_DIST': 'SIDECAR', "REDOC_DIST": "SIDECAR",
} }
from .celery.conf import * from .celery.conf import *
@ -173,7 +173,7 @@ DATABASES = {
"NAME": config("POSTGRES_DB"), "NAME": config("POSTGRES_DB"),
"USER": config("POSTGRES_USER"), "USER": config("POSTGRES_USER"),
"PASSWORD": config("POSTGRES_PASSWORD"), "PASSWORD": config("POSTGRES_PASSWORD"),
'HOST': config("POSTGRES_HOST"), "HOST": config("POSTGRES_HOST"),
"PORT": config("POSTGRES_PORT"), "PORT": config("POSTGRES_PORT"),
} }
} }
@ -183,20 +183,16 @@ DATABASES = {
AUTH_PASSWORD_VALIDATORS = [ AUTH_PASSWORD_VALIDATORS = [
{ {
"NAME": "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
"django.contrib.auth.password_validation.UserAttributeSimilarityValidator",
}, },
{ {
"NAME": "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator",
"django.contrib.auth.password_validation.MinimumLengthValidator",
}, },
{ {
"NAME": "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator",
"django.contrib.auth.password_validation.CommonPasswordValidator",
}, },
{ {
"NAME": "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator",
"django.contrib.auth.password_validation.NumericPasswordValidator",
}, },
] ]
@ -230,9 +226,7 @@ CACHES = {
"default": { "default": {
"BACKEND": "django_redis.cache.RedisCache", "BACKEND": "django_redis.cache.RedisCache",
"LOCATION": config("REDIS_URL"), "LOCATION": config("REDIS_URL"),
"OPTIONS": { "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
"CLIENT_CLASS": "django_redis.client.DefaultClient"
},
} }
} }