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