Fix code style issues with Black

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

View File

@ -5,6 +5,7 @@ from django.contrib.auth.admin import UserAdmin
from api.models import OnchainPayment, Order, LNPayment, Profile, MarketTick, Currency
from api.logics import Logics
from statistics import median
admin.site.unregister(Group)
admin.site.unregister(User)
@ -16,6 +17,7 @@ class ProfileInline(admin.StackedInline):
readonly_fields = ["avatar_tag"]
show_change_link = True
# extended users with avatars
@admin.register(User)
class EUserAdmin(AdminChangeLinksMixin, UserAdmin):
@ -30,14 +32,13 @@ class EUserAdmin(AdminChangeLinksMixin, UserAdmin):
"is_staff",
)
list_display_links = ("id", "username")
change_links = (
"profile",
)
change_links = ("profile",)
ordering = ("-id",)
def avatar_tag(self, obj):
return obj.profile.avatar_tag()
@admin.register(Order)
class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = (
@ -79,19 +80,34 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"taker_bond",
"trade_escrow",
)
list_filter = ("is_disputed", "is_fiat_sent", "is_swap","type", "currency", "status")
list_filter = (
"is_disputed",
"is_fiat_sent",
"is_swap",
"type",
"currency",
"status",
)
search_fields = ["id", "amount", "min_amount", "max_amount"]
actions = ['maker_wins', 'taker_wins', 'return_everything','compite_median_trade_time']
actions = [
"maker_wins",
"taker_wins",
"return_everything",
"compite_median_trade_time",
]
@admin.action(description='Solve dispute: maker wins')
@admin.action(description="Solve dispute: maker wins")
def maker_wins(self, request, queryset):
'''
"""
Solves a dispute on favor of the maker.
Adds Sats to compensations (earned_rewards) of the maker profile.
'''
"""
for order in queryset:
if order.status in [Order.Status.DIS, Order.Status.WFR] and order.is_disputed:
if (
order.status in [Order.Status.DIS, Order.Status.WFR]
and order.is_disputed
):
own_bond_sats = order.maker_bond.num_satoshis
if Logics.is_buyer(order, order.maker):
if order.is_swap:
@ -105,19 +121,30 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
order.maker.profile.earned_rewards = own_bond_sats + trade_sats
order.maker.profile.save()
order.save()
self.message_user(request,f"Dispute of order {order.id} solved successfully on favor of the maker", messages.SUCCESS)
self.message_user(
request,
f"Dispute of order {order.id} solved successfully on favor of the maker",
messages.SUCCESS,
)
else:
self.message_user(request,f"Order {order.id} is not in a disputed state", messages.ERROR)
self.message_user(
request,
f"Order {order.id} is not in a disputed state",
messages.ERROR,
)
@admin.action(description='Solve dispute: taker wins')
@admin.action(description="Solve dispute: taker wins")
def taker_wins(self, request, queryset):
'''
"""
Solves a dispute on favor of the taker.
Adds Sats to compensations (earned_rewards) of the taker profile.
'''
"""
for order in queryset:
if order.status in [Order.Status.DIS, Order.Status.WFR] and order.is_disputed:
if (
order.status in [Order.Status.DIS, Order.Status.WFR]
and order.is_disputed
):
own_bond_sats = order.maker_bond.num_satoshis
if Logics.is_buyer(order, order.taker):
if order.is_swap:
@ -131,37 +158,62 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
order.taker.profile.earned_rewards = own_bond_sats + trade_sats
order.taker.profile.save()
order.save()
self.message_user(request,f"Dispute of order {order.id} solved successfully on favor of the taker", messages.SUCCESS)
self.message_user(
request,
f"Dispute of order {order.id} solved successfully on favor of the taker",
messages.SUCCESS,
)
else:
self.message_user(request,f"Order {order.id} is not in a disputed state", messages.ERROR)
self.message_user(
request,
f"Order {order.id} is not in a disputed state",
messages.ERROR,
)
@admin.action(description='Solve dispute: return everything')
@admin.action(description="Solve dispute: return everything")
def return_everything(self, request, queryset):
'''
"""
Solves a dispute by pushing back every bond and escrow to their sender.
'''
"""
for order in queryset:
if order.status in [Order.Status.DIS, Order.Status.WFR] and order.is_disputed:
order.maker_bond.sender.profile.earned_rewards += order.maker_bond.num_satoshis
if (
order.status in [Order.Status.DIS, Order.Status.WFR]
and order.is_disputed
):
order.maker_bond.sender.profile.earned_rewards += (
order.maker_bond.num_satoshis
)
order.maker_bond.sender.profile.save()
order.taker_bond.sender.profile.earned_rewards += order.taker_bond.num_satoshis
order.taker_bond.sender.profile.earned_rewards += (
order.taker_bond.num_satoshis
)
order.taker_bond.sender.profile.save()
order.trade_escrow.sender.profile.earned_rewards += order.trade_escrow.num_satoshis
order.trade_escrow.sender.profile.earned_rewards += (
order.trade_escrow.num_satoshis
)
order.trade_escrow.sender.profile.save()
order.status = Order.Status.CCA
order.save()
self.message_user(request,f"Dispute of order {order.id} solved successfully, everything returned as compensations", messages.SUCCESS)
self.message_user(
request,
f"Dispute of order {order.id} solved successfully, everything returned as compensations",
messages.SUCCESS,
)
else:
self.message_user(request,f"Order {order.id} is not in a disputed state", messages.ERROR)
self.message_user(
request,
f"Order {order.id} is not in a disputed state",
messages.ERROR,
)
@admin.action(description='Compute median trade completion time')
@admin.action(description="Compute median trade completion time")
def compite_median_trade_time(self, request, queryset):
'''
"""
Computes the median time from an order taken to finishing
successfully for the set of selected orders.
'''
"""
times = []
for order in queryset:
if order.contract_finalization_time:
@ -172,9 +224,17 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
median_time_secs = median(times)
mins = int(median_time_secs / 60)
secs = int(median_time_secs - mins * 60)
self.message_user(request, f"The median time to complete the trades is {mins}m {secs}s", messages.SUCCESS)
self.message_user(
request,
f"The median time to complete the trades is {mins}m {secs}s",
messages.SUCCESS,
)
else:
self.message_user(request, "There is no successfully finished orders in the selection", messages.ERROR)
self.message_user(
request,
"There is no successfully finished orders in the selection",
messages.ERROR,
)
def amt(self, obj):
if obj.has_range and obj.amount == None:
@ -182,6 +242,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
else:
return float(obj.amount)
@admin.register(LNPayment)
class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = (
@ -211,7 +272,14 @@ class LNPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
)
list_filter = ("type", "concept", "status")
ordering = ("-expires_at",)
search_fields = ["payment_hash","num_satoshis","sender__username","receiver__username","description"]
search_fields = [
"payment_hash",
"num_satoshis",
"sender__username",
"receiver__username",
"description",
]
@admin.register(OnchainPayment)
class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
@ -235,6 +303,7 @@ class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_filter = ("concept", "status")
search_fields = ["address", "num_satoshis", "receiver__username", "txid"]
@admin.register(Profile)
class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = (
@ -272,11 +341,10 @@ class CurrencieAdmin(admin.ModelAdmin):
readonly_fields = ("currency", "exchange_rate", "timestamp")
ordering = ("id",)
@admin.register(MarketTick)
class MarketTickAdmin(admin.ModelAdmin):
list_display = ("timestamp", "price", "volume", "premium", "currency",
"fee")
readonly_fields = ("timestamp", "price", "volume", "premium", "currency",
"fee")
list_display = ("timestamp", "price", "volume", "premium", "currency", "fee")
readonly_fields = ("timestamp", "price", "volume", "premium", "currency", "fee")
list_filter = ["currency"]
ordering = ("-timestamp",)

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -36,7 +36,8 @@ class Command(BaseCommand):
queryset = Order.objects.exclude(status__in=do_nothing)
queryset = queryset.filter(
expires_at__lt=timezone.now()) # expires at lower than now
expires_at__lt=timezone.now()
) # expires at lower than now
debug = {}
debug["num_expired_orders"] = len(queryset)
@ -45,11 +46,9 @@ class Command(BaseCommand):
debug["reason_failure"] = []
for idx, order in enumerate(queryset):
context = str(order) + " was " + Order.Status(
order.status).label
context = str(order) + " was " + Order.Status(order.status).label
try:
if Logics.order_expires(
order): # Order send to expire here
if Logics.order_expires(order): # Order send to expire here
debug["expired_orders"].append({idx: context})
# It should not happen, but if it cannot locate the hold invoice

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
from celery import shared_task
@shared_task(name="users_cleansing")
def users_cleansing():
"""
@ -21,7 +22,11 @@ def users_cleansing():
for user in queryset:
# Try an except, due to unknown cause for users lacking profiles.
try:
if user.profile.pending_rewards > 0 or user.profile.earned_rewards > 0 or user.profile.claimed_rewards > 0:
if (
user.profile.pending_rewards > 0
or user.profile.earned_rewards > 0
or user.profile.claimed_rewards > 0
):
continue
if not user.profile.total_contracts == 0:
continue
@ -38,6 +43,7 @@ def users_cleansing():
}
return results
@shared_task(name="give_rewards")
def give_rewards():
"""
@ -57,10 +63,14 @@ def give_rewards():
profile.pending_rewards = 0
profile.save()
results[profile.user.username] = {'given_reward':given_reward,'earned_rewards':profile.earned_rewards}
results[profile.user.username] = {
"given_reward": given_reward,
"earned_rewards": profile.earned_rewards,
}
return results
@shared_task(name="follow_send_payment")
def follow_send_payment(hash):
"""Sends sats to buyer, continuous update"""
@ -75,10 +85,10 @@ def follow_send_payment(hash):
lnpayment = LNPayment.objects.get(payment_hash=hash)
fee_limit_sat = int(
max(
lnpayment.num_satoshis *
float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
lnpayment.num_satoshis * float(config("PROPORTIONAL_ROUTING_FEE_LIMIT")),
float(config("MIN_FLAT_ROUTING_FEE_LIMIT")),
)) # 1000 ppm or 10 sats
)
) # 1000 ppm or 10 sats
timeout_seconds = int(config("PAYOUT_TIMEOUT_SECONDS"))
request = LNNode.routerrpc.SendPaymentRequest(
@ -89,11 +99,9 @@ def follow_send_payment(hash):
order = lnpayment.order_paid_LN
try:
for response in LNNode.routerstub.SendPaymentV2(request,
metadata=[
("macaroon",
MACAROON.hex())
]):
for response in LNNode.routerstub.SendPaymentV2(
request, metadata=[("macaroon", MACAROON.hex())]
):
lnpayment.in_flight = True
lnpayment.save()
@ -125,11 +133,13 @@ def follow_send_payment(hash):
order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.FAI))
seconds=order.t_to_expire(Order.Status.FAI)
)
order.save()
context = {
"routing_failed":
LNNode.payment_failure_context[response.failure_reason],
"routing_failed": LNNode.payment_failure_context[
response.failure_reason
],
"IN_FLIGHT": False,
}
print(context)
@ -149,7 +159,8 @@ def follow_send_payment(hash):
lnpayment.save()
order.status = Order.Status.SUC
order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.SUC))
seconds=order.t_to_expire(Order.Status.SUC)
)
order.save()
return True, None
@ -162,11 +173,13 @@ def follow_send_payment(hash):
lnpayment.save()
order.status = Order.Status.FAI
order.expires_at = timezone.now() + timedelta(
seconds=order.t_to_expire(Order.Status.FAI))
seconds=order.t_to_expire(Order.Status.FAI)
)
order.save()
context = {"routing_failed": "The payout invoice has expired"}
return False, context
@shared_task(name="payments_cleansing")
def payments_cleansing():
"""
@ -185,10 +198,11 @@ def payments_cleansing():
# Usually expiry is 1 day for every finished order. So ~4 days until
# a never locked hodl invoice is removed.
finished_time = timezone.now() - timedelta(days=3)
queryset = LNPayment.objects.filter(Q(status=LNPayment.Status.CANCEL),
Q(order_made__expires_at__lt=finished_time)|
Q(order_taken__expires_at__lt=finished_time))
queryset = LNPayment.objects.filter(
Q(status=LNPayment.Status.CANCEL),
Q(order_made__expires_at__lt=finished_time)
| Q(order_taken__expires_at__lt=finished_time),
)
# And do not have an active trade, any past contract or any reward.
deleted_lnpayments = []
@ -202,8 +216,10 @@ def payments_cleansing():
pass
# same for onchain payments
queryset = OnchainPayment.objects.filter(Q(status__in=[OnchainPayment.Status.CANCE, OnchainPayment.Status.CREAT]),
Q(order_paid_TX__expires_at__lt=finished_time)|Q(order_paid_TX__isnull=True))
queryset = OnchainPayment.objects.filter(
Q(status__in=[OnchainPayment.Status.CANCE, OnchainPayment.Status.CREAT]),
Q(order_paid_TX__expires_at__lt=finished_time) | Q(order_paid_TX__isnull=True),
)
# And do not have an active trade, any past contract or any reward.
deleted_onchainpayments = []
@ -224,6 +240,7 @@ def payments_cleansing():
}
return results
@shared_task(name="cache_external_market_prices", ignore_result=True)
def cache_market():
@ -236,7 +253,9 @@ def cache_market():
exchange_rates = get_exchange_rates(currency_codes)
results = {}
for i in range(len(Currency.currency_dict.values())): # currencies are indexed starting at 1 (USD)
for i in range(
len(Currency.currency_dict.values())
): # currencies are indexed starting at 1 (USD)
rate = exchange_rates[i]
results[i] = {currency_codes[i], rate}
@ -259,45 +278,48 @@ def cache_market():
return results
@shared_task(name="send_message", ignore_result=True)
def send_message(order_id, message):
from api.models import Order
order = Order.objects.get(id=order_id)
if not order.maker.profile.telegram_enabled:
return
from api.messages import Telegram
telegram = Telegram()
if message == 'welcome':
if message == "welcome":
telegram.welcome(order)
elif message == 'order_expired_untaken':
elif message == "order_expired_untaken":
telegram.order_expired_untaken(order)
elif message == 'trade_successful':
elif message == "trade_successful":
telegram.trade_successful(order)
elif message == 'public_order_cancelled':
elif message == "public_order_cancelled":
telegram.public_order_cancelled(order)
elif message == 'taker_expired_b4bond':
elif message == "taker_expired_b4bond":
telegram.taker_expired_b4bond(order)
elif message == 'order_published':
elif message == "order_published":
telegram.order_published(order)
elif message == 'order_taken_confirmed':
elif message == "order_taken_confirmed":
telegram.order_taken_confirmed(order)
elif message == 'fiat_exchange_starts':
elif message == "fiat_exchange_starts":
telegram.fiat_exchange_starts(order)
elif message == 'dispute_opened':
elif message == "dispute_opened":
telegram.dispute_opened(order)
elif message == 'collaborative_cancelled':
elif message == "collaborative_cancelled":
telegram.collaborative_cancelled(order)
return

View File

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

View File

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

View File

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

View File

@ -1,6 +1,7 @@
from django.contrib import admin
from django_admin_relation_links import AdminChangeLinksMixin
from chat.models import ChatRoom, Message
# Register your models here.
@ -20,6 +21,7 @@ class ChatRoomAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
change_links = ["order", "maker", "taker"]
search_fields = ["id"]
@admin.register(Message)
class MessageAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = (

View File

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

View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
from celery import shared_task
@shared_task(name="chatrooms_cleansing")
def chatrooms_cleansing():
"""
@ -12,17 +13,21 @@ def chatrooms_cleansing():
from datetime import timedelta
from django.utils import timezone
finished_states = [Order.Status.SUC,
finished_states = [
Order.Status.SUC,
Order.Status.TLD,
Order.Status.MLD,
Order.Status.CCA,
Order.Status.UCA]
Order.Status.UCA,
]
# Orders that have expired more than 3 days ago
# Usually expiry takes place 1 day after a finished order. So, ~4 days
# until encrypted messages are deleted.
finished_time = timezone.now() - timedelta(days=3)
queryset = Order.objects.filter(status__in=finished_states, expires_at__lt=finished_time)
queryset = Order.objects.filter(
status__in=finished_states, expires_at__lt=finished_time
)
# And do not have an active trade, any past contract or any reward.
deleted_chatrooms = []

View File

@ -10,11 +10,14 @@ from django.utils import timezone
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
class ChatView(viewsets.ViewSet):
serializer_class = PostMessageSerializer
lookup_url_kwarg = ["order_id", "offset"]
queryset = Message.objects.filter(order__status__in=[Order.Status.CHA, Order.Status.FSE])
queryset = Message.objects.filter(
order__status__in=[Order.Status.CHA, Order.Status.FSE]
)
def get(self, request, format=None):
"""
@ -26,10 +29,7 @@ class ChatView(viewsets.ViewSet):
if order_id is None:
return Response(
{
"bad_request":
"Order ID does not exist"
},
{"bad_request": "Order ID does not exist"},
status.HTTP_400_BAD_REQUEST,
)
@ -37,19 +37,13 @@ class ChatView(viewsets.ViewSet):
if not (request.user == order.maker or request.user == order.taker):
return Response(
{
"bad_request":
"You are not participant in this order"
},
{"bad_request": "You are not participant in this order"},
status.HTTP_400_BAD_REQUEST,
)
if not order.status in [Order.Status.CHA, Order.Status.FSE]:
return Response(
{
"bad_request":
"Order is not in chat status"
},
{"bad_request": "Order is not in chat status"},
status.HTTP_400_BAD_REQUEST,
)
@ -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
if chatroom.maker == request.user:
chatroom.taker_connected = order.taker_last_seen > (timezone.now() - timedelta(minutes=1))
chatroom.taker_connected = order.taker_last_seen > (
timezone.now() - timedelta(minutes=1)
)
chatroom.maker_connected = True
chatroom.save()
peer_connected = chatroom.taker_connected
elif chatroom.taker == request.user:
chatroom.maker_connected = order.maker_last_seen > (timezone.now() - timedelta(minutes=1))
chatroom.maker_connected = order.maker_last_seen > (
timezone.now() - timedelta(minutes=1)
)
chatroom.taker_connected = True
chatroom.save()
peer_connected = chatroom.maker_connected
messages = []
for message in queryset:
d = ChatSerializer(message).data
print(d)
# Re-serialize so the response is identical to the consumer message
data = {
'index':d['index'],
'time':d['created_at'],
'message':d['PGP_message'],
'nick': User.objects.get(id=d['sender']).username
"index": d["index"],
"time": d["created_at"],
"message": d["PGP_message"],
"nick": User.objects.get(id=d["sender"]).username,
}
messages.append(data)
response = {'peer_connected': peer_connected, 'messages':messages}
response = {"peer_connected": peer_connected, "messages": messages}
return Response(response, status.HTTP_200_OK)
@ -102,10 +99,7 @@ class ChatView(viewsets.ViewSet):
if order_id is None:
return Response(
{
"bad_request":
"Order ID does not exist"
},
{"bad_request": "Order ID does not exist"},
status.HTTP_400_BAD_REQUEST,
)
@ -113,19 +107,13 @@ class ChatView(viewsets.ViewSet):
if not (request.user == order.maker or request.user == order.taker):
return Response(
{
"bad_request":
"You are not participant in this order"
},
{"bad_request": "You are not participant in this order"},
status.HTTP_400_BAD_REQUEST,
)
if not order.status in [Order.Status.CHA, Order.Status.FSE]:
return Response(
{
"bad_request":
"Order is not in chat status"
},
{"bad_request": "Order is not in chat status"},
status.HTTP_400_BAD_REQUEST,
)
@ -145,7 +133,7 @@ class ChatView(viewsets.ViewSet):
"maker_connected": order.maker == request.user,
"taker": order.taker,
"taker_connected": order.taker == request.user,
}
},
)
last_index = Message.objects.filter(order=order, chatroom=chatroom).count()
@ -174,7 +162,7 @@ class ChatView(viewsets.ViewSet):
"time": str(new_message.created_at),
"nick": new_message.sender.username,
"peer_connected": peer_connected,
}
},
)
# if offset is given, reply with messages
@ -187,16 +175,15 @@ class ChatView(viewsets.ViewSet):
print(d)
# Re-serialize so the response is identical to the consumer message
data = {
'index':d['index'],
'time':d['created_at'],
'message':d['PGP_message'],
'nick': User.objects.get(id=d['sender']).username
"index": d["index"],
"time": d["created_at"],
"message": d["PGP_message"],
"nick": User.objects.get(id=d["sender"]).username,
}
messages.append(data)
response = {'peer_connected': peer_connected, 'messages':messages}
response = {"peer_connected": peer_connected, "messages": messages}
else:
response = {}
return Response(response, status.HTTP_200_OK)

View File

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

View File

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

View File

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

View File

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

View File

@ -58,7 +58,7 @@ app.conf.beat_schedule = {
"compute-node-balance": { # Logs LND channel and wallet balance
"task": "compute_node_balance",
"schedule": timedelta(minutes=60),
}
},
}
app.conf.timezone = "UTC"

View File

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

View File

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