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

View File

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

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

View File

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

View File

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

View File

@ -3,37 +3,38 @@ from secrets import token_urlsafe
from api.models import Order
from api.utils import get_session
class Telegram():
''' Simple telegram messages by requesting to API'''
class Telegram:
"""Simple telegram messages by requesting to API"""
session = get_session()
site = config('HOST_NAME')
site = config("HOST_NAME")
def get_context(user):
"""returns context needed to enable TG notifications"""
context = {}
if user.profile.telegram_enabled :
context['tg_enabled'] = True
if user.profile.telegram_enabled:
context["tg_enabled"] = True
else:
context['tg_enabled'] = False
context["tg_enabled"] = False
if user.profile.telegram_token == None:
user.profile.telegram_token = token_urlsafe(15)
user.profile.save()
context['tg_token'] = user.profile.telegram_token
context['tg_bot_name'] = config("TELEGRAM_BOT_NAME")
context["tg_token"] = user.profile.telegram_token
context["tg_bot_name"] = config("TELEGRAM_BOT_NAME")
return context
def send_message(self, user, text):
""" sends a message to a user with telegram notifications enabled"""
"""sends a message to a user with telegram notifications enabled"""
bot_token=config('TELEGRAM_TOKEN')
bot_token = config("TELEGRAM_TOKEN")
chat_id = user.profile.telegram_chat_id
message_url = f'https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}'
message_url = f"https://api.telegram.org/bot{bot_token}/sendMessage?chat_id={chat_id}&text={text}"
# if it fails, it should keep trying
while True:
try:
@ -41,13 +42,13 @@ class Telegram():
return
except:
pass
def welcome(self, user):
''' User enabled Telegram Notifications'''
"""User enabled Telegram Notifications"""
lang = user.profile.telegram_lang_code
if lang == 'es':
text = f'Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats.'
if lang == "es":
text = f"Hola {user.username}, te enviaré notificaciones sobre tus órdenes en RoboSats."
else:
text = f"Hey {user.username}, I will send you notifications about your RoboSats orders."
self.send_message(user, text)
@ -75,18 +76,18 @@ class Telegram():
def order_taken_confirmed(self, order):
if order.maker.profile.telegram_enabled:
lang = order.maker.profile.telegram_lang_code
if lang == 'es':
text = f'Hey {order.maker.username} ¡Tu orden con ID {order.id} ha sido tomada por {order.taker.username}!🥳 Visita http://{self.site}/order/{order.id} para continuar.'
if lang == "es":
text = f"Hey {order.maker.username} ¡Tu orden con ID {order.id} ha sido tomada por {order.taker.username}!🥳 Visita http://{self.site}/order/{order.id} para continuar."
else:
text = f'Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳 Visit http://{self.site}/order/{order.id} to proceed with the trade.'
text = f"Hey {order.maker.username}, your order was taken by {order.taker.username}!🥳 Visit http://{self.site}/order/{order.id} to proceed with the trade."
self.send_message(order.maker, text)
if order.taker.profile.telegram_enabled:
lang = order.taker.profile.telegram_lang_code
if lang == 'es':
text = f'Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}.'
if lang == "es":
text = f"Hey {order.taker.username}, acabas de tomar la orden con ID {order.id}."
else:
text = f'Hey {order.taker.username}, you just took the order with ID {order.id}.'
text = f"Hey {order.taker.username}, you just took the order with ID {order.id}."
self.send_message(order.taker, text)
return
@ -95,20 +96,20 @@ class Telegram():
for user in [order.maker, order.taker]:
if user.profile.telegram_enabled:
lang = user.profile.telegram_lang_code
if lang == 'es':
text = f'Hey {user.username}, el depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat. Visita http://{self.site}/order/{order.id} para hablar con tu contraparte.'
if lang == "es":
text = f"Hey {user.username}, el depósito de garantía y el recibo del comprador han sido recibidos. Es hora de enviar el dinero fiat. Visita http://{self.site}/order/{order.id} para hablar con tu contraparte."
else:
text = f'Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat. Visit http://{self.site}/order/{order.id} to talk with your counterpart.'
text = f"Hey {user.username}, the escrow and invoice have been submitted. The fiat exchange starts now via the platform chat. Visit http://{self.site}/order/{order.id} to talk with your counterpart."
self.send_message(user, text)
return
def order_expired_untaken(self, order):
if order.maker.profile.telegram_enabled:
lang = order.maker.profile.telegram_lang_code
if lang == 'es':
text = f'Hey {order.maker.username}, tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{self.site}/order/{order.id} para renovarla.'
if lang == "es":
text = f"Hey {order.maker.username}, tu orden con ID {order.id} ha expirado sin ser tomada por ningún robot. Visita http://{self.site}/order/{order.id} para renovarla."
else:
text = f'Hey {order.maker.username}, your order with ID {order.id} has expired without a taker. Visit http://{self.site}/order/{order.id} to renew it.'
text = f"Hey {order.maker.username}, your order with ID {order.id} has expired without a taker. Visit http://{self.site}/order/{order.id} to renew it."
self.send_message(order.maker, text)
return
@ -116,42 +117,42 @@ class Telegram():
for user in [order.maker, order.taker]:
if user.profile.telegram_enabled:
lang = user.profile.telegram_lang_code
if lang == 'es':
text = f'¡Tu orden con ID {order.id} ha finalizado exitosamente!⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar.'
if lang == "es":
text = f"¡Tu orden con ID {order.id} ha finalizado exitosamente!⚡ Únete a nosotros en @robosats_es y ayúdanos a mejorar."
else:
text = f'Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve.'
text = f"Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve."
self.send_message(user, text)
return
def public_order_cancelled(self, order):
if order.maker.profile.telegram_enabled:
lang = order.maker.profile.telegram_lang_code
if lang == 'es':
text = f'Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}.'
if lang == "es":
text = f"Hey {order.maker.username}, has cancelado tu orden pública con ID {order.id}."
else:
text = f'Hey {order.maker.username}, you have cancelled your public order with ID {order.id}.'
text = f"Hey {order.maker.username}, you have cancelled your public order with ID {order.id}."
self.send_message(order.maker, text)
return
def collaborative_cancelled(self, order):
for user in [order.maker, order.taker]:
if user.profile.telegram_enabled:
lang = user.profile.telegram_lang_code
if lang == 'es':
text = f'Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente.'
if lang == "es":
text = f"Hey {user.username}, tu orden con ID {str(order.id)} fue cancelada colaborativamente."
else:
text = f'Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled.'
text = f"Hey {user.username}, your order with ID {str(order.id)} has been collaboratively cancelled."
self.send_message(user, text)
return
def dispute_opened(self, order):
for user in [order.maker, order.taker]:
if user.profile.telegram_enabled:
lang = user.profile.telegram_lang_code
if lang == 'es':
text = f'Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa.'
if lang == "es":
text = f"Hey {user.username}, la orden con ID {str(order.id)} ha entrado en disputa."
else:
text = f'Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}.'
text = f"Hey {user.username}, a dispute has been opened on your order with ID {str(order.id)}."
self.send_message(user, text)
return
@ -163,8 +164,8 @@ class Telegram():
if len(queryset) == 0:
return
order = queryset.last()
if lang == 'es':
text = f'Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes.'
if lang == "es":
text = f"Hey {order.maker.username}, tu orden con ID {str(order.id)} es pública en el libro de ordenes."
else:
text = f"Hey {order.maker.username}, your order with ID {str(order.id)} is public in the order book."
self.send_message(order.maker, text)

View File

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

View File

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

View File

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

View File

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

View File

@ -3,17 +3,26 @@ from .models import MarketTick, Order
from decouple import config
RETRY_TIME = int(config("RETRY_TIME"))
MIN_PUBLIC_ORDER_DURATION_SECS=60*60*float(config("MIN_PUBLIC_ORDER_DURATION"))
MAX_PUBLIC_ORDER_DURATION_SECS=60*60*float(config("MAX_PUBLIC_ORDER_DURATION"))
MIN_PUBLIC_ORDER_DURATION_SECS = 60 * 60 * float(config("MIN_PUBLIC_ORDER_DURATION"))
MAX_PUBLIC_ORDER_DURATION_SECS = 60 * 60 * float(config("MAX_PUBLIC_ORDER_DURATION"))
class InfoSerializer(serializers.Serializer):
num_public_buy_orders = serializers.IntegerField()
num_public_sell_orders = serializers.IntegerField()
book_liquidity = serializers.IntegerField(help_text='Total amount of BTC in the order book')
book_liquidity = serializers.IntegerField(
help_text="Total amount of BTC in the order book"
)
active_robots_today = serializers.CharField()
last_day_nonkyc_btc_premium = serializers.FloatField(help_text='Average premium (weighted by volume) of the orders in the last 24h')
last_day_volume = serializers.FloatField(help_text='Total volume in BTC in the last 24h')
lifetime_volume = serializers.FloatField(help_text='Total volume in BTC since exchange\'s inception')
last_day_nonkyc_btc_premium = serializers.FloatField(
help_text="Average premium (weighted by volume) of the orders in the last 24h"
)
last_day_volume = serializers.FloatField(
help_text="Total volume in BTC in the last 24h"
)
lifetime_volume = serializers.FloatField(
help_text="Total volume in BTC since exchange's inception"
)
lnd_version = serializers.CharField()
robosats_running_commit_hash = serializers.CharField()
alternative_site = serializers.CharField()
@ -21,17 +30,20 @@ class InfoSerializer(serializers.Serializer):
node_alias = serializers.CharField()
node_id = serializers.CharField()
network = serializers.CharField()
maker_fee = serializers.FloatField(help_text='Exchange\'s set maker fee')
taker_fee = serializers.FloatField(help_text='Exchange\'s set taker fee ')
bond_size = serializers.FloatField(help_text='Default bond size (percent)')
current_swap_fee_rate = serializers.FloatField(help_text='Swap fees to perform on-chain transaction (percent)')
nickname = serializers.CharField(help_text='Currenlty logged in Robot name')
referral_code = serializers.CharField(help_text='Logged in users\'s referral code')
earned_rewards = serializers.IntegerField(help_text='Logged in user\'s earned rewards in satoshis')
maker_fee = serializers.FloatField(help_text="Exchange's set maker fee")
taker_fee = serializers.FloatField(help_text="Exchange's set taker fee ")
bond_size = serializers.FloatField(help_text="Default bond size (percent)")
current_swap_fee_rate = serializers.FloatField(
help_text="Swap fees to perform on-chain transaction (percent)"
)
nickname = serializers.CharField(help_text="Currenlty logged in Robot name")
referral_code = serializers.CharField(help_text="Logged in users's referral code")
earned_rewards = serializers.IntegerField(
help_text="Logged in user's earned rewards in satoshis"
)
class ListOrderSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = (
@ -53,51 +65,46 @@ class ListOrderSerializer(serializers.ModelSerializer):
"maker",
"taker",
"escrow_duration",
"bond_size"
"bond_size",
)
# Only used in oas_schemas
class SummarySerializer(serializers.Serializer):
sent_fiat = serializers.IntegerField(
required=False,
help_text="same as `amount` (only for buyer)"
required=False, help_text="same as `amount` (only for buyer)"
)
received_sats = serializers.IntegerField(
required=False,
help_text="same as `trade_satoshis` (only for buyer)"
required=False, help_text="same as `trade_satoshis` (only for buyer)"
)
is_swap = serializers.BooleanField(
required=False,
help_text="True if the payout was on-chain (only for buyer)"
required=False, help_text="True if the payout was on-chain (only for buyer)"
)
received_onchain_sats = serializers.IntegerField(
required=False,
help_text="The on-chain sats received (only for buyer and if `is_swap` is `true`)"
help_text="The on-chain sats received (only for buyer and if `is_swap` is `true`)",
)
mining_fee_sats = serializers.IntegerField(
required=False,
help_text="Mining fees paid in satoshis (only for buyer and if `is_swap` is `true`)"
help_text="Mining fees paid in satoshis (only for buyer and if `is_swap` is `true`)",
)
swap_fee_sats = serializers.IntegerField(
required=False,
help_text="Exchange swap fee in sats (i.e excluding miner fees) (only for buyer and if `is_swap` is `true`)"
help_text="Exchange swap fee in sats (i.e excluding miner fees) (only for buyer and if `is_swap` is `true`)",
)
swap_fee_percent = serializers.FloatField(
required=False,
help_text="same as `swap_fee_rate` (only for buyer and if `is_swap` is `true`"
help_text="same as `swap_fee_rate` (only for buyer and if `is_swap` is `true`",
)
sent_sats = serializers.IntegerField(
required=False,
help_text="The total sats you sent (only for seller)"
required=False, help_text="The total sats you sent (only for seller)"
)
received_fiat = serializers.IntegerField(
required=False,
help_text="same as `amount` (only for seller)"
required=False, help_text="same as `amount` (only for seller)"
)
trade_fee_sats = serializers.IntegerField(
required=False,
help_text="Exchange fees in sats (Does not include swap fee and miner fee)"
help_text="Exchange fees in sats (Does not include swap fee and miner fee)",
)
@ -105,45 +112,42 @@ class SummarySerializer(serializers.Serializer):
class PlatformSummarySerializer(serializers.Serializer):
contract_timestamp = serializers.DateTimeField(
required=False,
help_text="Timestamp of when the contract was finalized (price and sats fixed)"
help_text="Timestamp of when the contract was finalized (price and sats fixed)",
)
contract_total_time = serializers.FloatField(
required=False,
help_text="The time taken for the contract to complete (from taker taking the order to completion of order) in seconds"
help_text="The time taken for the contract to complete (from taker taking the order to completion of order) in seconds",
)
routing_fee_sats = serializers.IntegerField(
required=False,
help_text="Sats payed by the exchange for routing fees. Mining fee in case of on-chain swap payout"
help_text="Sats payed by the exchange for routing fees. Mining fee in case of on-chain swap payout",
)
trade_revenue_sats = serializers.IntegerField(
required=False,
help_text="The sats the exchange earned from the trade"
required=False, help_text="The sats the exchange earned from the trade"
)
# Only used in oas_schemas
class OrderDetailSerializer(serializers.ModelSerializer):
total_secs_exp = serializers.IntegerField(
required=False,
help_text="Duration of time (in seconds) to expire, according to the current status of order."
"This is duration of time after `created_at` (in seconds) that the order will automatically expire."
"This value changes according to which stage the order is in"
"This is duration of time after `created_at` (in seconds) that the order will automatically expire."
"This value changes according to which stage the order is in",
)
penalty = serializers.DateTimeField(
required=False,
help_text="Time when the user penalty will expire. Penalty applies when you create orders repeatedly without commiting a bond"
help_text="Time when the user penalty will expire. Penalty applies when you create orders repeatedly without commiting a bond",
)
is_maker = serializers.BooleanField(
required=False,
help_text="Whether you are the maker or not"
required=False, help_text="Whether you are the maker or not"
)
is_taker = serializers.BooleanField(
required=False,
help_text="Whether you are the taker or not"
required=False, help_text="Whether you are the taker or not"
)
is_participant = serializers.BooleanField(
required=False,
help_text="True if you are either a taker or maker, False otherwise"
help_text="True if you are either a taker or maker, False otherwise",
)
maker_status = serializers.CharField(
required=False,
@ -151,193 +155,170 @@ class OrderDetailSerializer(serializers.ModelSerializer):
"- **'Active'** (seen within last 2 min)\n"
"- **'Seen Recently'** (seen within last 10 min)\n"
"- **'Inactive'** (seen more than 10 min ago)\n\n"
"Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty"
"Note: When you make a request to this route, your own status get's updated and can be seen by your counterparty",
)
taker_status = serializers.BooleanField(
required=False,
help_text="True if you are either a taker or maker, False otherwise"
help_text="True if you are either a taker or maker, False otherwise",
)
price_now = serializers.IntegerField(
required=False,
help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)"
help_text="Price of the order in the order's currency at the time of request (upto 5 significant digits)",
)
premium = serializers.IntegerField(
required=False,
help_text="Premium over the CEX price at the current time"
required=False, help_text="Premium over the CEX price at the current time"
)
premium_percentile = serializers.IntegerField(
required=False,
help_text="(Only if `is_maker`) Premium percentile of your order compared to other public orders in the same currency currently in the order book"
help_text="(Only if `is_maker`) Premium percentile of your order compared to other public orders in the same currency currently in the order book",
)
num_similar_orders = serializers.IntegerField(
required=False,
help_text="(Only if `is_maker`) The number of public orders of the same currency currently in the order book"
help_text="(Only if `is_maker`) The number of public orders of the same currency currently in the order book",
)
tg_enabled = serializers.BooleanField(
required=False,
help_text="(Only if `is_maker`) Whether Telegram notification is enabled or not"
help_text="(Only if `is_maker`) Whether Telegram notification is enabled or not",
)
tg_token = serializers.CharField(
required=False,
help_text="(Only if `is_maker`) Your telegram bot token required to enable notifications."
help_text="(Only if `is_maker`) Your telegram bot token required to enable notifications.",
)
tg_bot_name = serializers.CharField(
required=False,
help_text="(Only if `is_maker`) The Telegram username of the bot"
help_text="(Only if `is_maker`) The Telegram username of the bot",
)
is_buyer = serializers.BooleanField(
required=False,
help_text="Whether you are a buyer of sats (you will be receiving sats)"
help_text="Whether you are a buyer of sats (you will be receiving sats)",
)
is_seller = serializers.BooleanField(
required=False,
help_text="Whether you are a seller of sats or not (you will be sending sats)"
help_text="Whether you are a seller of sats or not (you will be sending sats)",
)
maker_nick = serializers.CharField(
required=False,
help_text="Nickname (Robot name) of the maker"
required=False, help_text="Nickname (Robot name) of the maker"
)
taker_nick = serializers.CharField(
required=False,
help_text="Nickname (Robot name) of the taker"
required=False, help_text="Nickname (Robot name) of the taker"
)
status_message = serializers.CharField(
required=False,
help_text="The current status of the order corresponding to the `status`"
help_text="The current status of the order corresponding to the `status`",
)
is_fiat_sent = serializers.BooleanField(
required=False,
help_text="Whether or not the fiat amount is sent by the buyer"
required=False, help_text="Whether or not the fiat amount is sent by the buyer"
)
is_disputed = serializers.BooleanField(
required=False,
help_text="Whether or not the counterparty raised a dispute"
)
ur_nick = serializers.CharField(
required=False,
help_text="Your Nickname"
)
ur_nick = serializers.CharField(
required=False,
help_text="Your Nick"
required=False, help_text="Whether or not the counterparty raised a dispute"
)
ur_nick = serializers.CharField(required=False, help_text="Your Nickname")
ur_nick = serializers.CharField(required=False, help_text="Your Nick")
maker_locked = serializers.BooleanField(
required=False,
help_text="True if maker bond is locked, False otherwise"
required=False, help_text="True if maker bond is locked, False otherwise"
)
taker_locked = serializers.BooleanField(
required=False,
help_text="True if taker bond is locked, False otherwise"
required=False, help_text="True if taker bond is locked, False otherwise"
)
escrow_locked = serializers.BooleanField(
required=False,
help_text="True if escrow is locked, False otherwise. Escrow is the sats to be sold, held by Robosats until the trade is finised."
help_text="True if escrow is locked, False otherwise. Escrow is the sats to be sold, held by Robosats until the trade is finised.",
)
trade_satoshis = serializers.IntegerField(
required=False,
help_text="Seller sees the amount of sats they need to send. Buyer sees the amount of sats they will receive "
help_text="Seller sees the amount of sats they need to send. Buyer sees the amount of sats they will receive ",
)
bond_invoice = serializers.CharField(
required=False,
help_text="When `status` = `0`, `3`. Bond invoice to be paid"
required=False, help_text="When `status` = `0`, `3`. Bond invoice to be paid"
)
bond_satoshis = serializers.IntegerField(
required=False,
help_text="The bond amount in satoshis"
required=False, help_text="The bond amount in satoshis"
)
escrow_invoice = serializers.CharField(
required=False,
help_text="For the seller, the escrow invoice to be held by RoboSats"
help_text="For the seller, the escrow invoice to be held by RoboSats",
)
escrow_satoshis = serializers.IntegerField(
required=False,
help_text="The escrow amount in satoshis"
required=False, help_text="The escrow amount in satoshis"
)
invoice_amount = serializers.IntegerField(
required=False,
help_text="The amount in sats the buyer needs to submit an invoice of to receive the trade amount"
help_text="The amount in sats the buyer needs to submit an invoice of to receive the trade amount",
)
swap_allowed = serializers.BooleanField(
required=False,
help_text="Whether on-chain swap is allowed"
required=False, help_text="Whether on-chain swap is allowed"
)
swap_failure_reason = serializers.CharField(
required=False,
help_text="Reason for why on-chain swap is not available"
required=False, help_text="Reason for why on-chain swap is not available"
)
suggested_mining_fee_rate = serializers.IntegerField(
required=False,
help_text="fee in sats/vbyte for the on-chain swap"
required=False, help_text="fee in sats/vbyte for the on-chain swap"
)
swap_fee_rate = serializers.FloatField(
swap_fee_rate = serializers.FloatField(
required=False,
help_text="in percentage, the swap fee rate the platform charges"
help_text="in percentage, the swap fee rate the platform charges",
)
pending_cancel = serializers.BooleanField(
required=False,
help_text="Your counterparty requested for a collaborative cancel when `status` is either `8`, `9` or `10`"
help_text="Your counterparty requested for a collaborative cancel when `status` is either `8`, `9` or `10`",
)
asked_for_cancel = serializers.BooleanField(
required=False,
help_text="You requested for a collaborative cancel `status` is either `8`, `9` or `10`"
help_text="You requested for a collaborative cancel `status` is either `8`, `9` or `10`",
)
statement_submitted = serializers.BooleanField(
required=False,
help_text="True if you have submitted a statement. Available when `status` is `11`"
help_text="True if you have submitted a statement. Available when `status` is `11`",
)
retries = serializers.IntegerField(
required=False,
help_text="Number of times ln node has tried to make the payment to you (only if you are the buyer)"
help_text="Number of times ln node has tried to make the payment to you (only if you are the buyer)",
)
next_retry_time = serializers.DateTimeField(
required=False,
help_text=f"The next time payment will be retried. Payment is retried every {RETRY_TIME} sec"
help_text=f"The next time payment will be retried. Payment is retried every {RETRY_TIME} sec",
)
failure_reason = serializers.CharField(
required=False,
help_text="The reason the payout failed"
required=False, help_text="The reason the payout failed"
)
invoice_expired = serializers.BooleanField(
required=False,
help_text="True if the payout invoice expired. `invoice_amount` will be re-set and sent which means the user has to submit a new invoice to be payed"
help_text="True if the payout invoice expired. `invoice_amount` will be re-set and sent which means the user has to submit a new invoice to be payed",
)
trade_fee_percent = serializers.IntegerField(
required=False,
help_text="The fee for the trade (fees differ for maker and taker)"
help_text="The fee for the trade (fees differ for maker and taker)",
)
bond_size_sats = serializers.IntegerField(
required=False,
help_text="The size of the bond in sats"
required=False, help_text="The size of the bond in sats"
)
bond_size_percent = serializers.IntegerField(
required=False,
help_text="same as `bond_size`"
required=False, help_text="same as `bond_size`"
)
maker_summary = SummarySerializer(required=False)
taker_summary = SummarySerializer(required=False)
platform_summary = PlatformSummarySerializer(required=True)
expiry_message = serializers.CharField(
required=False,
help_text="The reason the order expired (message associated with the `expiry_reason`)"
help_text="The reason the order expired (message associated with the `expiry_reason`)",
)
num_satoshis = serializers.IntegerField(
required=False,
help_text="only if status = `14` (Successful Trade) and is_buyer = `true`"
help_text="only if status = `14` (Successful Trade) and is_buyer = `true`",
)
sent_satoshis = serializers.IntegerField(
required=False,
help_text="only if status = `14` (Successful Trade) and is_buyer = `true`"
help_text="only if status = `14` (Successful Trade) and is_buyer = `true`",
)
txid = serializers.CharField(
required=False,
help_text="Transaction id of the on-chain swap payout. Only if status = `14` (Successful Trade) and is_buyer = `true`"
help_text="Transaction id of the on-chain swap payout. Only if status = `14` (Successful Trade) and is_buyer = `true`",
)
network = serializers.CharField(
required=False,
help_text="The network eg. 'testnet', 'mainnet'. Only if status = `14` (Successful Trade) and is_buyer = `true`"
help_text="The network eg. 'testnet', 'mainnet'. Only if status = `14` (Successful Trade) and is_buyer = `true`",
)
class Meta:
model = Order
fields = (
@ -392,7 +373,7 @@ class OrderDetailSerializer(serializers.ModelSerializer):
"escrow_satoshis",
"invoice_amount",
"swap_allowed",
'swap_failure_reason',
"swap_failure_reason",
"suggested_mining_fee_rate",
"swap_fee_rate",
"pending_cancel",
@ -421,9 +402,16 @@ class OrderDetailSerializer(serializers.ModelSerializer):
class OrderPublicSerializer(serializers.ModelSerializer):
maker_nick = serializers.CharField(required=False)
maker_status = serializers.CharField(help_text='Status of the nick - "Active" or "Inactive"', required=False)
price = serializers.FloatField(help_text="Price in order's fiat currency", required=False)
satoshis_now = serializers.IntegerField(help_text="The amount of sats to be traded at the present moment (not including the fees)", required=False)
maker_status = serializers.CharField(
help_text='Status of the nick - "Active" or "Inactive"', required=False
)
price = serializers.FloatField(
help_text="Price in order's fiat currency", required=False
)
satoshis_now = serializers.IntegerField(
help_text="The amount of sats to be traded at the present moment (not including the fees)",
required=False,
)
class Meta:
model = Order
@ -448,7 +436,7 @@ class OrderPublicSerializer(serializers.ModelSerializer):
"price",
"escrow_duration",
"satoshis_now",
"bond_size"
"bond_size",
)
@ -461,19 +449,19 @@ class MakeOrderSerializer(serializers.ModelSerializer):
max_length=70,
default="not specified",
required=False,
help_text="Can be any string. The UI recognizes [these payment methods](https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/src/components/payment-methods/Methods.js) and displays them with a logo."
help_text="Can be any string. The UI recognizes [these payment methods](https://github.com/Reckless-Satoshi/robosats/blob/main/frontend/src/components/payment-methods/Methods.js) and displays them with a logo.",
)
is_explicit = serializers.BooleanField(
default=False,
help_text='Whether the order is explicitly priced or not. If set to `true` then `satoshis` need to be specified'
help_text="Whether the order is explicitly priced or not. If set to `true` then `satoshis` need to be specified",
)
has_range = serializers.BooleanField(
default=False,
help_text='Whether the order specifies a range of amount or a fixed amount.\n\nIf `true`, then `min_amount` and `max_amount` fields are **required**.\n\n If `false` then `amount` is **required**',
help_text="Whether the order specifies a range of amount or a fixed amount.\n\nIf `true`, then `min_amount` and `max_amount` fields are **required**.\n\n If `false` then `amount` is **required**",
)
bondless_taker = serializers.BooleanField(
default=False,
help_text='Whether bondless takers are allowed for this order or not',
help_text="Whether bondless takers are allowed for this order or not",
)
class Meta:
@ -495,19 +483,17 @@ class MakeOrderSerializer(serializers.ModelSerializer):
"bondless_taker",
)
class UpdateOrderSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000,
allow_null=True,
allow_blank=True,
default=None)
address = serializers.CharField(max_length=100,
allow_null=True,
allow_blank=True,
default=None)
statement = serializers.CharField(max_length=10000,
allow_null=True,
allow_blank=True,
default=None)
invoice = serializers.CharField(
max_length=2000, allow_null=True, allow_blank=True, default=None
)
address = serializers.CharField(
max_length=100, allow_null=True, allow_blank=True, default=None
)
statement = serializers.CharField(
max_length=10000, allow_null=True, allow_blank=True, default=None
)
action = serializers.ChoiceField(
choices=(
"pause",
@ -529,64 +515,86 @@ class UpdateOrderSerializer(serializers.Serializer):
allow_blank=True,
default=None,
)
amount = serializers.DecimalField(max_digits=18, decimal_places=8, allow_null=True, required=False, default=None)
mining_fee_rate = serializers.DecimalField(max_digits=6, decimal_places=3, allow_null=True, required=False, default=None)
amount = serializers.DecimalField(
max_digits=18, decimal_places=8, allow_null=True, required=False, default=None
)
mining_fee_rate = serializers.DecimalField(
max_digits=6, decimal_places=3, allow_null=True, required=False, default=None
)
class UserGenSerializer(serializers.Serializer):
# Mandatory fields
token_sha256 = serializers.CharField(
min_length=64,
max_length=64,
allow_null=False,
allow_blank=False,
required=True,
help_text="SHA256 of user secret")
public_key = serializers.CharField(max_length=2000,
allow_null=False,
allow_blank=False,
required=True,
help_text="Armored ASCII PGP public key block")
encrypted_private_key = serializers.CharField(max_length=2000,
allow_null=False,
allow_blank=False,
required=True,
help_text="Armored ASCII PGP encrypted private key block")
min_length=64,
max_length=64,
allow_null=False,
allow_blank=False,
required=True,
help_text="SHA256 of user secret",
)
public_key = serializers.CharField(
max_length=2000,
allow_null=False,
allow_blank=False,
required=True,
help_text="Armored ASCII PGP public key block",
)
encrypted_private_key = serializers.CharField(
max_length=2000,
allow_null=False,
allow_blank=False,
required=True,
help_text="Armored ASCII PGP encrypted private key block",
)
# Optional fields
ref_code = serializers.CharField(max_length=30,
allow_null=True,
allow_blank=True,
required=False,
default=None,
help_text="Referal code")
counts = serializers.ListField(child=serializers.IntegerField(),
allow_null=True,
required=False,
default=None,
help_text="Counts of the unique characters in the token")
length = serializers.IntegerField(allow_null=True,
default=None,
required=False,
min_value=1,
help_text="Length of the token")
unique_values = serializers.IntegerField(allow_null=True,
default=None,
required=False,
min_value=1,
help_text="Number of unique values in the token")
ref_code = serializers.CharField(
max_length=30,
allow_null=True,
allow_blank=True,
required=False,
default=None,
help_text="Referal code",
)
counts = serializers.ListField(
child=serializers.IntegerField(),
allow_null=True,
required=False,
default=None,
help_text="Counts of the unique characters in the token",
)
length = serializers.IntegerField(
allow_null=True,
default=None,
required=False,
min_value=1,
help_text="Length of the token",
)
unique_values = serializers.IntegerField(
allow_null=True,
default=None,
required=False,
min_value=1,
help_text="Number of unique values in the token",
)
class ClaimRewardSerializer(serializers.Serializer):
invoice = serializers.CharField(max_length=2000,
allow_null=True,
allow_blank=True,
default=None,
help_text="A valid LN invoice with the reward amount to withdraw")
invoice = serializers.CharField(
max_length=2000,
allow_null=True,
allow_blank=True,
default=None,
help_text="A valid LN invoice with the reward amount to withdraw",
)
class PriceSerializer(serializers.Serializer):
pass
class TickSerializer(serializers.ModelSerializer):
class TickSerializer(serializers.ModelSerializer):
class Meta:
model = MarketTick
fields = (
@ -599,5 +607,6 @@ class TickSerializer(serializers.ModelSerializer):
)
depth = 1
class StealthSerializer(serializers.Serializer):
wantsStealth = serializers.BooleanField()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,24 +3,29 @@ from api.models import User, Order
from django.utils import timezone
import uuid
class ChatRoom(models.Model):
'''
Simple ChatRoom model. Needed to facilitate communication: Is my counterpart in the room?
'''
id = models.PositiveBigIntegerField(primary_key=True, null=False,default=None, blank=True)
class ChatRoom(models.Model):
"""
Simple ChatRoom model. Needed to facilitate communication: Is my counterpart in the room?
"""
id = models.PositiveBigIntegerField(
primary_key=True, null=False, default=None, blank=True
)
order = models.ForeignKey(
Order,
related_name="chatroom",
on_delete=models.SET_NULL,
null=True,
default=None)
default=None,
)
maker = models.ForeignKey(
User,
related_name="chat_maker",
on_delete=models.SET_NULL,
null=True,
default=None)
default=None,
)
taker = models.ForeignKey(
User,
related_name="chat_taker",
@ -30,8 +35,8 @@ class ChatRoom(models.Model):
blank=True,
)
maker_connected = models.BooleanField(default=False, null=False)
taker_connected = models.BooleanField(default=False, null=False)
maker_connected = models.BooleanField(default=False, null=False)
taker_connected = models.BooleanField(default=False, null=False)
maker_connect_date = models.DateTimeField(auto_now_add=True)
taker_connect_date = models.DateTimeField(auto_now_add=True)
@ -46,41 +51,39 @@ class ChatRoom(models.Model):
def __str__(self):
return f"Chat:{str(self.id)}"
class Message(models.Model):
class Meta:
get_latest_by = 'index'
get_latest_by = "index"
# id = models.PositiveBigIntegerField(primary_key=True, default=uuid.uuid4, editable=False)
order = models.ForeignKey(
Order,
related_name="message",
on_delete=models.CASCADE,
null=True,
default=None)
Order, related_name="message", on_delete=models.CASCADE, null=True, default=None
)
chatroom = models.ForeignKey(
ChatRoom,
related_name="chatroom",
on_delete=models.CASCADE,
null=True,
default=None)
index = models.PositiveIntegerField(null=False,default=None, blank=True)
default=None,
)
index = models.PositiveIntegerField(null=False, default=None, blank=True)
sender = models.ForeignKey(
User,
related_name="message_sender",
on_delete=models.SET_NULL,
null=True,
default=None)
default=None,
)
receiver = models.ForeignKey(
User,
related_name="message_receiver",
on_delete=models.SET_NULL,
null=True,
default=None)
default=None,
)
PGP_message = models.TextField(max_length=5000,
null=True,
default=None,
blank=True)
PGP_message = models.TextField(max_length=5000, null=True, default=None, blank=True)
created_at = models.DateTimeField(default=timezone.now)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -3,79 +3,129 @@ from django.utils import timezone
from api.lightning.node import LNNode
class AccountingDay(models.Model):
day = models.DateTimeField(primary_key=True, auto_now=False, auto_now_add=False)
# Every field is denominated in Sats with (3 decimals for millisats)
# Total volume contracted
contracted = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
contracted = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Number of contracts
num_contracts = models.BigIntegerField(default=0, null=False, blank=False)
# Net volume of trading invoices settled (excludes disputes)
net_settled = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
net_settled = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Net volume of trading invoices paid (excludes rewards and disputes)
net_paid = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
net_paid = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Sum of net settled and net paid
net_balance = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
net_balance = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Total volume of invoices settled
inflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
inflow = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Total volume of invoices paid
outflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
outflow = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Total cost in routing fees
routing_fees = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
routing_fees = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Total cost in minig fees
mining_fees = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
mining_fees = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Total inflows minus outflows and routing fees
cashflow = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
cashflow = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Balance on earned rewards (referral rewards, slashed bonds and solved disputes)
outstanding_earned_rewards = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
outstanding_earned_rewards = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Balance on pending disputes (not resolved yet)
outstanding_pending_disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
outstanding_pending_disputes = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Rewards claimed lifetime
lifetime_rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
lifetime_rewards_claimed = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Balance change from last day on earned rewards (referral rewards, slashed bonds and solved disputes)
earned_rewards = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
earned_rewards = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Balance change on pending disputes (not resolved yet)
disputes = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
disputes = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
# Rewards claimed on day
rewards_claimed = models.DecimalField(max_digits=15, decimal_places=3, default=0, null=False, blank=False)
rewards_claimed = models.DecimalField(
max_digits=15, decimal_places=3, default=0, null=False, blank=False
)
class BalanceLog(models.Model):
def get_total():
return LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance']
return (
LNNode.wallet_balance()["total_balance"]
+ LNNode.channel_balance()["local_balance"]
)
def get_frac():
return LNNode.wallet_balance()['total_balance'] / (LNNode.wallet_balance()['total_balance'] + LNNode.channel_balance()['local_balance'])
def get_oc_total():
return LNNode.wallet_balance()['total_balance']
def get_oc_conf():
return LNNode.wallet_balance()['confirmed_balance']
return LNNode.wallet_balance()["total_balance"] / (
LNNode.wallet_balance()["total_balance"]
+ LNNode.channel_balance()["local_balance"]
)
def get_oc_total():
return LNNode.wallet_balance()["total_balance"]
def get_oc_conf():
return LNNode.wallet_balance()["confirmed_balance"]
def get_oc_unconf():
return LNNode.wallet_balance()['unconfirmed_balance']
return LNNode.wallet_balance()["unconfirmed_balance"]
def get_ln_local():
return LNNode.channel_balance()['local_balance']
return LNNode.channel_balance()["local_balance"]
def get_ln_remote():
return LNNode.channel_balance()['remote_balance']
return LNNode.channel_balance()["remote_balance"]
def get_ln_local_unsettled():
return LNNode.channel_balance()['unsettled_local_balance']
return LNNode.channel_balance()["unsettled_local_balance"]
def get_ln_remote_unsettled():
return LNNode.channel_balance()['unsettled_remote_balance']
return LNNode.channel_balance()["unsettled_remote_balance"]
time = models.DateTimeField(primary_key=True, default=timezone.now)
# Every field is denominated in Sats
total = models.PositiveBigIntegerField(default=get_total)
onchain_fraction = models.DecimalField(max_digits=6, decimal_places=5, default=get_frac)
onchain_fraction = models.DecimalField(
max_digits=6, decimal_places=5, default=get_frac
)
onchain_total = models.PositiveBigIntegerField(default=get_oc_total)
onchain_confirmed = models.PositiveBigIntegerField(default=get_oc_conf)
onchain_unconfirmed = models.PositiveBigIntegerField(default=get_oc_unconf)
ln_local = models.PositiveBigIntegerField(default=get_ln_local)
ln_remote = models.PositiveBigIntegerField(default=get_ln_remote)
ln_local_unsettled = models.PositiveBigIntegerField(default=get_ln_local_unsettled)
ln_remote_unsettled = models.PositiveBigIntegerField(default=get_ln_remote_unsettled)
ln_remote_unsettled = models.PositiveBigIntegerField(
default=get_ln_remote_unsettled
)
def __str__(self):
return f"Balance at {self.time.strftime('%d/%m/%Y %H:%M:%S')}"
class Dispute(models.Model):
pass
pass

View File

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

View File

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

View File

@ -35,11 +35,11 @@ app.conf.beat_schedule = {
"task": "users_cleansing",
"schedule": crontab(hour=0, minute=0),
},
"chatrooms-cleansing": { # Cleans 3+ days old encrypted messages and chatrooms at midnight
"chatrooms-cleansing": { # Cleans 3+ days old encrypted messages and chatrooms at midnight
"task": "chatrooms_cleansing",
"schedule": crontab(hour=0, minute=0),
},
"lnpayments-cleansing": { # Cleans 3+ days old unlocked hodl invoices
"lnpayments-cleansing": { # Cleans 3+ days old unlocked hodl invoices
"task": "payments_cleansing",
"schedule": crontab(hour=0, minute=0),
},
@ -55,10 +55,10 @@ app.conf.beat_schedule = {
"task": "cache_external_market_prices",
"schedule": timedelta(seconds=60),
},
"compute-node-balance": { # Logs LND channel and wallet balance
"task":"compute_node_balance",
"compute-node-balance": { # Logs LND channel and wallet balance
"task": "compute_node_balance",
"schedule": timedelta(minutes=60),
}
},
}
app.conf.timezone = "UTC"

View File

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

View File

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