From 75f04579ed685815329b9733e4366b9a07fa8f9b Mon Sep 17 00:00:00 2001
From: Reckless_Satoshi <90936742+Reckless-Satoshi@users.noreply.github.com>
Date: Mon, 1 May 2023 10:30:53 +0000
Subject: [PATCH] Refactor models into a module (#481)
* Refactor models into a module
* Rename Profile model as Robot
* Underscore numeric literals
---
api/admin.py | 53 +-
api/lightning/node.py | 6 +-
api/logics.py | 151 ++--
api/management/commands/telegram_watcher.py | 18 +-
api/models.py | 758 --------------------
api/models/__init__.py | 8 +
api/models/currency.py | 31 +
api/models/ln_payment.py | 132 ++++
api/models/market_tick.py | 84 +++
api/models/onchain_payment.py | 120 ++++
api/models/order.py | 285 ++++++++
api/models/robot.py | 137 ++++
api/nick_generator/dicts/en/nouns.py | 4 +-
api/nick_generator/nick_generator.py | 31 +-
api/notifications.py | 76 +-
api/serializers.py | 4 +-
api/tasks.py | 38 +-
api/views.py | 54 +-
chat/consumers.py | 4 +-
chat/models.py | 3 +-
chat/views.py | 7 +-
control/tasks.py | 6 +-
22 files changed, 1029 insertions(+), 981 deletions(-)
delete mode 100644 api/models.py
create mode 100644 api/models/__init__.py
create mode 100644 api/models/currency.py
create mode 100644 api/models/ln_payment.py
create mode 100644 api/models/market_tick.py
create mode 100644 api/models/onchain_payment.py
create mode 100644 api/models/order.py
create mode 100644 api/models/robot.py
diff --git a/api/admin.py b/api/admin.py
index c3bbd014..954fd738 100644
--- a/api/admin.py
+++ b/api/admin.py
@@ -6,14 +6,14 @@ from django.contrib.auth.models import Group, User
from django_admin_relation_links import AdminChangeLinksMixin
from api.logics import Logics
-from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order, Profile
+from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order, Robot
admin.site.unregister(Group)
admin.site.unregister(User)
-class ProfileInline(admin.StackedInline):
- model = Profile
+class RobotInline(admin.StackedInline):
+ model = Robot
can_delete = False
fields = ("avatar_tag",)
readonly_fields = ["avatar_tag"]
@@ -23,22 +23,22 @@ class ProfileInline(admin.StackedInline):
# extended users with avatars
@admin.register(User)
class EUserAdmin(AdminChangeLinksMixin, UserAdmin):
- inlines = [ProfileInline]
+ inlines = [RobotInline]
list_display = (
"avatar_tag",
"id",
- "profile_link",
+ "robot_link",
"username",
"last_login",
"date_joined",
"is_staff",
)
list_display_links = ("id", "username")
- change_links = ("profile",)
+ change_links = ("robot",)
ordering = ("-id",)
def avatar_tag(self, obj):
- return obj.profile.avatar_tag()
+ return obj.robot.avatar_tag()
@admin.register(Order)
@@ -90,7 +90,16 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
"currency",
"status",
)
- search_fields = ["id", "amount", "min_amount", "max_amount"]
+ search_fields = [
+ "id",
+ "reference",
+ "maker",
+ "taker",
+ "amount",
+ "min_amount",
+ "max_amount",
+ ]
+ readonly_fields = ["reference"]
actions = [
"maker_wins",
@@ -103,7 +112,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
def maker_wins(self, request, queryset):
"""
Solves a dispute on favor of the maker.
- Adds Sats to compensations (earned_rewards) of the maker profile.
+ Adds Sats to compensations (earned_rewards) of the maker robot.
"""
for order in queryset:
if (
@@ -120,8 +129,8 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
trade_sats = order.trade_escrow.num_satoshis
order.status = Order.Status.TLD
- order.maker.profile.earned_rewards = own_bond_sats + trade_sats
- order.maker.profile.save()
+ order.maker.robot.earned_rewards = own_bond_sats + trade_sats
+ order.maker.robot.save()
order.save()
self.message_user(
request,
@@ -140,7 +149,7 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
def taker_wins(self, request, queryset):
"""
Solves a dispute on favor of the taker.
- Adds Sats to compensations (earned_rewards) of the taker profile.
+ Adds Sats to compensations (earned_rewards) of the taker robot.
"""
for order in queryset:
if (
@@ -157,8 +166,8 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
trade_sats = order.trade_escrow.num_satoshis
order.status = Order.Status.MLD
- order.taker.profile.earned_rewards = own_bond_sats + trade_sats
- order.taker.profile.save()
+ order.taker.robot.earned_rewards = own_bond_sats + trade_sats
+ order.taker.robot.save()
order.save()
self.message_user(
request,
@@ -183,18 +192,18 @@ class OrderAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
order.status in [Order.Status.DIS, Order.Status.WFR]
and order.is_disputed
):
- order.maker_bond.sender.profile.earned_rewards += (
+ order.maker_bond.sender.robot.earned_rewards += (
order.maker_bond.num_satoshis
)
- order.maker_bond.sender.profile.save()
- order.taker_bond.sender.profile.earned_rewards += (
+ order.maker_bond.sender.robot.save()
+ order.taker_bond.sender.robot.earned_rewards += (
order.taker_bond.num_satoshis
)
- order.taker_bond.sender.profile.save()
- order.trade_escrow.sender.profile.earned_rewards += (
+ order.taker_bond.sender.robot.save()
+ order.trade_escrow.sender.robot.earned_rewards += (
order.trade_escrow.num_satoshis
)
- order.trade_escrow.sender.profile.save()
+ order.trade_escrow.sender.robot.save()
order.status = Order.Status.CCA
order.save()
self.message_user(
@@ -315,8 +324,8 @@ class OnchainPaymentAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
search_fields = ["address", "num_satoshis", "receiver__username", "txid"]
-@admin.register(Profile)
-class UserProfileAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
+@admin.register(Robot)
+class UserRobotAdmin(AdminChangeLinksMixin, admin.ModelAdmin):
list_display = (
"avatar_tag",
"id",
diff --git a/api/lightning/node.py b/api/lightning/node.py
index 0e36d658..a1908c43 100644
--- a/api/lightning/node.py
+++ b/api/lightning/node.py
@@ -39,7 +39,7 @@ except Exception:
LND_GRPC_HOST = config("LND_GRPC_HOST")
DISABLE_ONCHAIN = config("DISABLE_ONCHAIN", cast=bool, default=True)
-MAX_SWAP_AMOUNT = config("MAX_SWAP_AMOUNT", cast=int, default=500000)
+MAX_SWAP_AMOUNT = config("MAX_SWAP_AMOUNT", cast=int, default=500_000)
class LNNode:
@@ -345,7 +345,7 @@ class LNNode:
)
else:
max_routing_fee_sats = int(
- float(num_satoshis) * float(routing_budget_ppm) / 1000000
+ float(num_satoshis) * float(routing_budget_ppm) / 1_000_000
)
if route_hints:
@@ -357,7 +357,7 @@ class LNNode:
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
+ hop_hint.fee_proportional_millionths * num_satoshis / 1_000_000
)
# ...and store the cost of the route to the array
diff --git a/api/logics.py b/api/logics.py
index 85d3b261..62fef2ae 100644
--- a/api/logics.py
+++ b/api/logics.py
@@ -4,11 +4,12 @@ from datetime import timedelta
import gnupg
from decouple import config
+from django.contrib.auth.models import User
from django.db.models import Q, Sum
from django.utils import timezone
from api.lightning.node import LNNode
-from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order, User
+from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order
from api.tasks import send_notification
from api.utils import validate_onchain_address
from chat.models import Message
@@ -266,9 +267,9 @@ class Logics:
price = exchange_rate * (1 + float(premium) / 100)
else:
amount = order.amount if not order.has_range else order.max_amount
- order_rate = float(amount) / (float(order.satoshis) / 100000000)
+ order_rate = float(amount) / (float(order.satoshis) / 100_000_000)
premium = order_rate / exchange_rate - 1
- premium = int(premium * 10000) / 100 # 2 decimals left
+ premium = int(premium * 10_000) / 100 # 2 decimals left
price = order_rate
significant_digits = 5
@@ -352,7 +353,7 @@ class Logics:
order.expiry_reason = Order.ExpiryReasons.NESCRO
order.save()
# Reward taker with part of the maker bond
- cls.add_slashed_rewards(order.maker_bond, order.taker.profile)
+ cls.add_slashed_rewards(order.maker_bond, order.taker.robot)
return True
# If maker is buyer, settle the taker's bond order goes back to public
@@ -371,7 +372,7 @@ class Logics:
cls.publish_order(order)
send_notification.delay(order_id=order.id, message="order_published")
# Reward maker with part of the taker bond
- cls.add_slashed_rewards(taker_bond, order.maker.profile)
+ cls.add_slashed_rewards(taker_bond, order.maker.robot)
return True
elif order.status == Order.Status.WFI:
@@ -388,7 +389,7 @@ class Logics:
order.expiry_reason = Order.ExpiryReasons.NINVOI
order.save()
# Reward taker with part of the maker bond
- cls.add_slashed_rewards(order.maker_bond, order.taker.profile)
+ cls.add_slashed_rewards(order.maker_bond, order.taker.robot)
return True
# If maker is seller settle the taker's bond, order goes back to public
@@ -402,7 +403,7 @@ class Logics:
cls.publish_order(order)
send_notification.delay(order_id=order.id, message="order_published")
# Reward maker with part of the taker bond
- cls.add_slashed_rewards(taker_bond, order.maker.profile)
+ cls.add_slashed_rewards(taker_bond, order.maker.robot)
return True
elif order.status in [Order.Status.CHA, Order.Status.FSE]:
@@ -417,11 +418,11 @@ class Logics:
"""The taker did not lock the taker_bond. Now he has to go"""
# Add a time out to the taker
if order.taker:
- profile = order.taker.profile
- profile.penalty_expiration = timezone.now() + timedelta(
+ robot = order.taker.robot
+ robot.penalty_expiration = timezone.now() + timedelta(
seconds=PENALTY_TIMEOUT
)
- profile.save()
+ robot.save()
# Make order public again
order.taker = None
@@ -467,14 +468,14 @@ class Logics:
cls.return_escrow(order)
cls.settle_bond(order.maker_bond)
cls.return_bond(order.taker_bond)
- cls.add_slashed_rewards(order.maker_bond, order.taker.profile)
+ cls.add_slashed_rewards(order.maker_bond, order.taker.robot)
order.status = Order.Status.MLD
elif num_messages_maker == 0:
cls.return_escrow(order)
cls.settle_bond(order.maker_bond)
cls.return_bond(order.taker_bond)
- cls.add_slashed_rewards(order.taker_bond, order.maker.profile)
+ cls.add_slashed_rewards(order.taker_bond, order.maker.robot)
order.status = Order.Status.TLD
else:
return False
@@ -525,15 +526,15 @@ class Logics:
# User could be None if a dispute is open automatically due to weird expiration.
if user is not None:
- profile = user.profile
- profile.num_disputes = profile.num_disputes + 1
- if profile.orders_disputes_started is None:
- profile.orders_disputes_started = [str(order.id)]
+ robot = user.robot
+ robot.num_disputes = robot.num_disputes + 1
+ if robot.orders_disputes_started is None:
+ robot.orders_disputes_started = [str(order.id)]
else:
- profile.orders_disputes_started = list(
- profile.orders_disputes_started
+ robot.orders_disputes_started = list(
+ robot.orders_disputes_started
).append(str(order.id))
- profile.save()
+ robot.save()
send_notification.delay(order_id=order.id, message="dispute_opened")
return True, None
@@ -546,9 +547,9 @@ class Logics:
"bad_request": "Only orders in dispute accept dispute statements"
}
- if len(statement) > 50000:
+ if len(statement) > 50_000:
return False, {
- "bad_statement": "The statement and chatlogs are longer than 50000 characters"
+ "bad_statement": "The statement and chat logs are longer than 50,000 characters"
}
if len(statement) < 100:
@@ -617,7 +618,7 @@ class Logics:
# Compute a safer available onchain liquidity: (confirmed_utxos - reserve - pending_outgoing_txs))
# Accounts for already committed outgoing TX for previous users.
confirmed = onchain_payment.balance.onchain_confirmed
- reserve = 300000 # We assume a reserve of 300K Sats (3 times higher than LND's default anchor reserve)
+ reserve = 300_000 # We assume a reserve of 300K Sats (3 times higher than LND's default anchor reserve)
pending_txs = OnchainPayment.objects.filter(
status__in=[OnchainPayment.Status.VALID, OnchainPayment.Status.QUEUE]
).aggregate(Sum("num_satoshis"))["num_satoshis__sum"]
@@ -668,7 +669,7 @@ class Logics:
fee_sats = order.last_satoshis * fee_fraction
- reward_tip = int(config("REWARD_TIP")) if user.profile.is_referred else 0
+ reward_tip = int(config("REWARD_TIP")) if user.robot.is_referred else 0
context = {}
# context necessary for the user to submit a LN invoice
@@ -677,8 +678,8 @@ class Logics:
) # Trading fee to buyer is charged here.
# context necessary for the user to submit an onchain address
- MIN_SWAP_AMOUNT = config("MIN_SWAP_AMOUNT", cast=int, default=20000)
- MAX_SWAP_AMOUNT = config("MAX_SWAP_AMOUNT", cast=int, default=500000)
+ MIN_SWAP_AMOUNT = config("MIN_SWAP_AMOUNT", cast=int, default=20_000)
+ MAX_SWAP_AMOUNT = config("MAX_SWAP_AMOUNT", cast=int, default=500_000)
if context["invoice_amount"] < MIN_SWAP_AMOUNT:
context["swap_allowed"] = False
@@ -728,7 +729,7 @@ class Logics:
fee_sats = order.last_satoshis * fee_fraction
- reward_tip = int(config("REWARD_TIP")) if user.profile.is_referred else 0
+ reward_tip = int(config("REWARD_TIP")) if user.robot.is_referred else 0
if cls.is_seller(order, user):
escrow_amount = round(
@@ -837,7 +838,7 @@ class Logics:
num_satoshis = cls.payout_amount(order, user)[1]["invoice_amount"]
routing_budget_sats = float(num_satoshis) * (
- float(routing_budget_ppm) / 1000000
+ float(routing_budget_ppm) / 1_000_000
)
num_satoshis = int(num_satoshis - routing_budget_sats)
payout = LNNode.validate_ln_invoice(invoice, num_satoshis, routing_budget_ppm)
@@ -910,33 +911,33 @@ class Logics:
order.save()
return True
- def add_profile_rating(profile, rating):
- """adds a new rating to a user profile"""
+ def add_robot_rating(robot, rating):
+ """adds a new rating to a user robot"""
# TODO Unsafe, does not update ratings, it adds more ratings everytime a new rating is clicked.
- profile.total_ratings += 1
- latest_ratings = profile.latest_ratings
+ robot.total_ratings += 1
+ latest_ratings = robot.latest_ratings
if latest_ratings is None:
- profile.latest_ratings = [rating]
- profile.avg_rating = rating
+ robot.latest_ratings = [rating]
+ robot.avg_rating = rating
else:
latest_ratings = ast.literal_eval(latest_ratings)
latest_ratings.append(rating)
- profile.latest_ratings = latest_ratings
- profile.avg_rating = sum(list(map(int, latest_ratings))) / len(
+ robot.latest_ratings = latest_ratings
+ robot.avg_rating = sum(list(map(int, latest_ratings))) / len(
latest_ratings
) # Just an average, but it is a list of strings. Has to be converted to int.
- profile.save()
+ robot.save()
def is_penalized(user):
"""Checks if a user that is not participant of orders
has a limit on taking or making a order"""
- if user.profile.penalty_expiration:
- if user.profile.penalty_expiration > timezone.now():
- time_out = (user.profile.penalty_expiration - timezone.now()).seconds
+ if user.robot.penalty_expiration:
+ if user.robot.penalty_expiration > timezone.now():
+ time_out = (user.robot.penalty_expiration - timezone.now()).seconds
return True, time_out
return False, None
@@ -1032,7 +1033,7 @@ class Logics:
order.status = Order.Status.UCA
order.save()
# Reward taker with part of the maker bond
- cls.add_slashed_rewards(order.maker_bond, order.taker.profile)
+ cls.add_slashed_rewards(order.maker_bond, order.taker.robot)
return True, None
# 4.b) When taker cancel after bond (before escrow)
@@ -1051,7 +1052,7 @@ class Logics:
cls.publish_order(order)
send_notification.delay(order_id=order.id, message="order_published")
# Reward maker with part of the taker bond
- cls.add_slashed_rewards(order.taker_bond, order.maker.profile)
+ cls.add_slashed_rewards(order.taker_bond, order.maker.robot)
return True, None
# 5) When trade collateral has been posted (after escrow)
@@ -1171,7 +1172,7 @@ class Logics:
order.last_satoshis_time = timezone.now()
bond_satoshis = int(order.last_satoshis * order.bond_size / 100)
- if user.profile.wants_stealth:
+ if user.robot.wants_stealth:
description = f"This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally. Payment reference: {order.reference}"
else:
description = f"RoboSats - Publishing '{str(order)}' - Maker bond - This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally."
@@ -1236,11 +1237,11 @@ class Logics:
order.status = Order.Status.WF2
order.save()
- # Both users profiles are added one more contract // Unsafe can add more than once.
- order.maker.profile.total_contracts += 1
- order.taker.profile.total_contracts += 1
- order.maker.profile.save()
- order.taker.profile.save()
+ # Both users robots are added one more contract // Unsafe can add more than once.
+ order.maker.robot.total_contracts += 1
+ order.taker.robot.total_contracts += 1
+ order.maker.robot.save()
+ order.taker.robot.save()
# Log a market tick
try:
@@ -1284,7 +1285,7 @@ class Logics:
order.last_satoshis_time = timezone.now()
bond_satoshis = int(order.last_satoshis * order.bond_size / 100)
pos_text = "Buying" if cls.is_buyer(order, user) else "Selling"
- if user.profile.wants_stealth:
+ if user.robot.wants_stealth:
description = f"This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally. Payment reference: {order.reference}"
else:
description = (
@@ -1381,7 +1382,7 @@ class Logics:
escrow_satoshis = cls.escrow_amount(order, user)[1][
"escrow_amount"
] # Amount was fixed when taker bond was locked, fee applied here
- if user.profile.wants_stealth:
+ if user.robot.wants_stealth:
description = f"This payment WILL FREEZE IN YOUR WALLET, check on the website if it was successful. It will automatically return unless you cheat or cancel unilaterally. Payment reference: {order.reference}"
else:
description = f"RoboSats - Escrow amount for '{str(order)}' - It WILL FREEZE IN YOUR WALLET. It will be released to the buyer once you confirm you received the fiat. It will automatically return if buyer does not confirm the payment."
@@ -1645,12 +1646,12 @@ class Logics:
if order.status in rating_allowed_status:
# if maker, rates taker
if order.maker == user and order.maker_rated is False:
- cls.add_profile_rating(order.taker.profile, rating)
+ cls.add_robot_rating(order.taker.robot, rating)
order.maker_rated = True
order.save()
# if taker, rates maker
if order.taker == user and order.taker_rated is False:
- cls.add_profile_rating(order.maker.profile, rating)
+ cls.add_robot_rating(order.maker.robot, rating)
order.taker_rated = True
order.save()
else:
@@ -1660,8 +1661,8 @@ class Logics:
@classmethod
def rate_platform(cls, user, rating):
- user.profile.platform_rating = rating
- user.profile.save()
+ user.robot.platform_rating = rating
+ user.robot.save()
return True, None
@classmethod
@@ -1671,27 +1672,27 @@ class Logics:
If participants of the order were referred, the reward is given to the referees.
"""
- if order.maker.profile.is_referred:
- profile = order.maker.profile.referred_by
- profile.pending_rewards += int(config("REWARD_TIP"))
- profile.save()
+ if order.maker.robot.is_referred:
+ robot = order.maker.robot.referred_by
+ robot.pending_rewards += int(config("REWARD_TIP"))
+ robot.save()
- if order.taker.profile.is_referred:
- profile = order.taker.profile.referred_by
- profile.pending_rewards += int(config("REWARD_TIP"))
- profile.save()
+ if order.taker.robot.is_referred:
+ robot = order.taker.robot.referred_by
+ robot.pending_rewards += int(config("REWARD_TIP"))
+ robot.save()
return
@classmethod
- def add_slashed_rewards(cls, bond, profile):
+ def add_slashed_rewards(cls, bond, robot):
"""
When a bond is slashed due to overtime, rewards the user that was waiting.
"""
reward_fraction = float(config("SLASHED_BOND_REWARD_SPLIT"))
reward = int(bond.num_satoshis * reward_fraction)
- profile.earned_rewards += reward
- profile.save()
+ robot.earned_rewards += reward
+ robot.save()
return
@@ -1700,10 +1701,10 @@ class Logics:
# only a user with positive withdraw balance can use this
- if user.profile.earned_rewards < 1:
+ if user.robot.earned_rewards < 1:
return False, {"bad_invoice": "You have not earned rewards"}
- num_satoshis = user.profile.earned_rewards
+ num_satoshis = user.robot.earned_rewards
routing_budget_sats = int(
max(
@@ -1712,7 +1713,7 @@ class Logics:
)
) # 1000 ppm or 10 sats
- routing_budget_ppm = (routing_budget_sats / float(num_satoshis)) * 1000000
+ routing_budget_ppm = (routing_budget_sats / float(num_satoshis)) * 1_000_000
reward_payout = LNNode.validate_ln_invoice(
invoice, num_satoshis, routing_budget_ppm
)
@@ -1738,21 +1739,21 @@ class Logics:
except Exception:
return False, {"bad_invoice": "Give me a new invoice"}
- user.profile.earned_rewards = 0
- user.profile.save()
+ user.robot.earned_rewards = 0
+ user.robot.save()
# Pays the invoice.
paid, failure_reason = LNNode.pay_invoice(lnpayment)
if paid:
- user.profile.earned_rewards = 0
- user.profile.claimed_rewards += num_satoshis
- user.profile.save()
+ user.robot.earned_rewards = 0
+ user.robot.claimed_rewards += num_satoshis
+ user.robot.save()
return True, None
# If fails, adds the rewards again.
else:
- user.profile.earned_rewards = num_satoshis
- user.profile.save()
+ user.robot.earned_rewards = num_satoshis
+ user.robot.save()
context = {}
context["bad_invoice"] = failure_reason
return False, context
@@ -1823,7 +1824,7 @@ class Logics:
platform_summary = {}
platform_summary["contract_exchange_rate"] = float(order.amount) / (
- float(order.last_satoshis) / 100000000
+ float(order.last_satoshis) / 100_000_000
)
if order.last_satoshis_time is not None:
platform_summary["contract_timestamp"] = order.last_satoshis_time
diff --git a/api/management/commands/telegram_watcher.py b/api/management/commands/telegram_watcher.py
index 0905999d..c937c4eb 100644
--- a/api/management/commands/telegram_watcher.py
+++ b/api/management/commands/telegram_watcher.py
@@ -5,7 +5,7 @@ from decouple import config
from django.core.management.base import BaseCommand
from django.db import transaction
-from api.models import Profile
+from api.models import Robot
from api.notifications import Telegram
from api.utils import get_session
@@ -52,12 +52,12 @@ class Command(BaseCommand):
if len(parts) < 2:
self.telegram.send_message(
chat_id=result["message"]["from"]["id"],
- text='You must enable the notifications bot using the RoboSats client. Click on your "Robot profile" -> "Enable Telegram" and follow the link or scan the QR code.',
+ text='You must enable the notifications bot using the RoboSats client. Click on your "Robot robot" -> "Enable Telegram" and follow the link or scan the QR code.',
)
continue
token = parts[-1]
- profile = Profile.objects.filter(telegram_token=token).first()
- if not profile:
+ robot = Robot.objects.filter(telegram_token=token).first()
+ if not robot:
self.telegram.send_message(
chat_id=result["message"]["from"]["id"],
text=f'Wops, invalid token! There is no Robot with telegram chat token "{token}"',
@@ -68,13 +68,13 @@ class Command(BaseCommand):
while attempts >= 0:
try:
with transaction.atomic():
- profile.telegram_chat_id = result["message"]["from"]["id"]
- profile.telegram_lang_code = result["message"]["from"][
+ robot.telegram_chat_id = result["message"]["from"]["id"]
+ robot.telegram_lang_code = result["message"]["from"][
"language_code"
]
- self.telegram.welcome(profile.user)
- profile.telegram_enabled = True
- profile.save()
+ self.telegram.welcome(robot.user)
+ robot.telegram_enabled = True
+ robot.save()
break
except Exception:
time.sleep(5)
diff --git a/api/models.py b/api/models.py
deleted file mode 100644
index 50ed0d95..00000000
--- a/api/models.py
+++ /dev/null
@@ -1,758 +0,0 @@
-import json
-import uuid
-from pathlib import Path
-
-from decouple import config
-from django.conf import settings
-from django.contrib.auth.models import User
-from django.core.validators import (
- MaxValueValidator,
- MinValueValidator,
- validate_comma_separated_integer_list,
-)
-from django.db import models
-from django.db.models.signals import post_save, pre_delete
-from django.dispatch import receiver
-from django.template.defaultfilters import truncatechars
-from django.utils import timezone
-from django.utils.html import mark_safe
-
-from control.models import BalanceLog
-
-MIN_TRADE = int(config("MIN_TRADE"))
-MAX_TRADE = int(config("MAX_TRADE"))
-MIN_SWAP_AMOUNT = int(config("MIN_SWAP_AMOUNT"))
-FEE = float(config("FEE"))
-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 = models.PositiveSmallIntegerField(
- choices=currency_choices, null=False, unique=True
- )
- exchange_rate = models.DecimalField(
- max_digits=18,
- decimal_places=4,
- default=None,
- null=True,
- validators=[MinValueValidator(0)],
- )
- timestamp = models.DateTimeField(default=timezone.now)
-
- def __str__(self):
- # returns currency label ( 3 letters code)
- return self.currency_dict[str(self.currency)]
-
- class Meta:
- verbose_name = "Cached market currency"
- verbose_name_plural = "Currencies"
-
-
-class LNPayment(models.Model):
- class Types(models.IntegerChoices):
- NORM = 0, "Regular invoice"
- HOLD = 1, "hold invoice"
-
- class Concepts(models.IntegerChoices):
- MAKEBOND = 0, "Maker bond"
- TAKEBOND = 1, "Taker bond"
- TRESCROW = 2, "Trade escrow"
- PAYBUYER = 3, "Payment to buyer"
- WITHREWA = 4, "Withdraw rewards"
-
- class Status(models.IntegerChoices):
- INVGEN = 0, "Generated"
- LOCKED = 1, "Locked"
- SETLED = 2, "Settled"
- RETNED = 3, "Returned"
- CANCEL = 4, "Cancelled"
- EXPIRE = 5, "Expired"
- VALIDI = 6, "Valid"
- FLIGHT = 7, "In flight"
- SUCCED = 8, "Succeeded"
- FAILRO = 9, "Routing failed"
-
- 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.",
- )
- NONRECO = 3, "A non-recoverable error has occurred."
- 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
- )
-
- # payment info
- payment_hash = models.CharField(
- max_length=100, unique=True, default=None, blank=True, primary_key=True
- )
- invoice = models.CharField(
- max_length=1200, unique=True, null=True, default=None, blank=True
- ) # Some invoices with lots of routing hints might be long
- preimage = models.CharField(
- max_length=64, unique=True, null=True, default=None, blank=True
- )
- description = models.CharField(
- max_length=500, unique=False, null=True, default=None, blank=True
- )
- num_satoshis = models.PositiveBigIntegerField(
- validators=[
- MinValueValidator(100),
- MaxValueValidator(1.5 * MAX_TRADE),
- ]
- )
- # Routing budget in PPM
- routing_budget_ppm = models.PositiveBigIntegerField(
- default=0,
- null=False,
- validators=[
- MinValueValidator(0),
- MaxValueValidator(100000),
- ],
- )
- # Routing budget in Sats. Only for reporting summaries.
- routing_budget_sats = models.DecimalField(
- max_digits=10, decimal_places=3, default=0, null=False, blank=False
- )
- # Fee in sats with mSats decimals fee_msat
- 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)
-
- # routing
- routing_attempts = models.PositiveSmallIntegerField(null=False, default=0)
- last_routing_time = models.DateTimeField(null=True, default=None, blank=True)
- in_flight = models.BooleanField(default=False, null=False, blank=False)
- # involved parties
- sender = models.ForeignKey(
- User, related_name="sender", on_delete=models.SET_NULL, null=True, default=None
- )
- receiver = models.ForeignKey(
- User,
- related_name="receiver",
- on_delete=models.SET_NULL,
- null=True,
- default=None,
- )
-
- def __str__(self):
- return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
-
- class Meta:
- verbose_name = "Lightning payment"
- verbose_name_plural = "Lightning payments"
-
- @property
- def hash(self):
- # Payment hash is the primary key of LNpayments
- # However it is too long for the admin panel.
- # We created a truncated property for display 'hash'
- return truncatechars(self.payment_hash, 10)
-
-
-class OnchainPayment(models.Model):
- class Concepts(models.IntegerChoices):
- PAYBUYER = 3, "Payment to buyer"
-
- class Status(models.IntegerChoices):
- CREAT = 0, "Created" # User was given platform fees and suggested mining fees
- VALID = 1, "Valid" # Valid onchain address and fee submitted
- MEMPO = 2, "In mempool" # Tx is sent to mempool
- CONFI = 3, "Confirmed" # Tx is confirmed +2 blocks
- CANCE = 4, "Cancelled" # Cancelled tx
- QUEUE = 5, "Queued" # Payment is queued to be sent out
-
- def get_balance():
- balance = BalanceLog.objects.create()
- return balance.time
-
- # payment use details
- concept = models.PositiveSmallIntegerField(
- choices=Concepts.choices, null=False, default=Concepts.PAYBUYER
- )
- status = models.PositiveSmallIntegerField(
- choices=Status.choices, null=False, default=Status.CREAT
- )
-
- broadcasted = models.BooleanField(default=False, null=False, blank=False)
-
- # payment info
- address = models.CharField(
- max_length=100, unique=False, default=None, null=True, blank=True
- )
-
- txid = models.CharField(
- max_length=64, unique=True, null=True, default=None, blank=True
- )
-
- num_satoshis = models.PositiveBigIntegerField(
- null=True,
- validators=[
- MinValueValidator(0.5 * MIN_SWAP_AMOUNT),
- MaxValueValidator(1.5 * MAX_TRADE),
- ],
- )
- sent_satoshis = models.PositiveBigIntegerField(
- null=True,
- validators=[
- MinValueValidator(0.5 * MIN_SWAP_AMOUNT),
- MaxValueValidator(1.5 * MAX_TRADE),
- ],
- )
- # fee in sats/vbyte with mSats decimals fee_msat
- suggested_mining_fee_rate = models.DecimalField(
- max_digits=6,
- decimal_places=3,
- default=2.05,
- null=False,
- blank=False,
- validators=[MinValueValidator(1), MaxValueValidator(999)],
- )
- mining_fee_rate = models.DecimalField(
- max_digits=6,
- decimal_places=3,
- default=2.05,
- null=False,
- blank=False,
- validators=[MinValueValidator(1), MaxValueValidator(999)],
- )
- mining_fee_sats = models.PositiveBigIntegerField(default=0, null=False, blank=False)
-
- # platform onchain/channels balance at creation, swap fee rate as percent of total volume
- balance = models.ForeignKey(
- BalanceLog,
- related_name="balance",
- on_delete=models.SET_NULL,
- null=True,
- default=get_balance,
- )
-
- swap_fee_rate = models.DecimalField(
- max_digits=4,
- decimal_places=2,
- default=float(config("MIN_SWAP_FEE")) * 100,
- null=False,
- blank=False,
- )
-
- created_at = models.DateTimeField(default=timezone.now)
-
- # involved parties
- receiver = models.ForeignKey(
- User,
- related_name="tx_receiver",
- on_delete=models.SET_NULL,
- null=True,
- default=None,
- )
-
- def __str__(self):
- return f"TX-{str(self.id)}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
-
- class Meta:
- verbose_name = "Onchain payment"
- verbose_name_plural = "Onchain payments"
-
- @property
- def hash(self):
- # Display txid as 'hash' truncated
- return truncatechars(self.txid, 10)
-
-
-class Order(models.Model):
- class Types(models.IntegerChoices):
- BUY = 0, "BUY"
- SELL = 1, "SELL"
-
- class Status(models.IntegerChoices):
- WFB = 0, "Waiting for maker bond"
- PUB = 1, "Public"
- PAU = 2, "Paused"
- TAK = 3, "Waiting for taker bond"
- UCA = 4, "Cancelled"
- EXP = 5, "Expired"
- WF2 = 6, "Waiting for trade collateral and buyer invoice"
- WFE = 7, "Waiting only for seller trade collateral"
- WFI = 8, "Waiting only for buyer invoice"
- CHA = 9, "Sending fiat - In chatroom"
- FSE = 10, "Fiat sent - In chatroom"
- DIS = 11, "In dispute"
- CCA = 12, "Collaboratively cancelled"
- PAY = 13, "Sending satoshis to buyer"
- SUC = 14, "Sucessful trade"
- FAI = 15, "Failed lightning network routing"
- WFR = 16, "Wait for dispute resolution"
- MLD = 17, "Maker lost dispute"
- TLD = 18, "Taker lost dispute"
-
- class ExpiryReasons(models.IntegerChoices):
- NTAKEN = 0, "Expired not taken"
- NMBOND = 1, "Maker bond not locked"
- NESCRO = 2, "Escrow not locked"
- NINVOI = 3, "Invoice not submitted"
- NESINV = 4, "Neither escrow locked or invoice submitted"
-
- # order info
- reference = models.UUIDField(default=uuid.uuid4, editable=False)
- status = models.PositiveSmallIntegerField(
- choices=Status.choices, null=False, default=Status.WFB
- )
- created_at = models.DateTimeField(default=timezone.now)
- expires_at = models.DateTimeField()
- 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)
- 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
- )
- # order pricing method. A explicit amount of sats, or a relative premium above/below market.
- is_explicit = models.BooleanField(default=False, null=False)
- # marked to market
- premium = models.DecimalField(
- max_digits=5,
- decimal_places=2,
- default=0,
- null=True,
- validators=[MinValueValidator(-100), MaxValueValidator(999)],
- blank=True,
- )
- # explicit
- satoshis = models.PositiveBigIntegerField(
- null=True,
- validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)],
- blank=True,
- )
- # optionally makers can choose the public order duration length (seconds)
- public_duration = models.PositiveBigIntegerField(
- default=60 * 60 * int(config("DEFAULT_PUBLIC_ORDER_DURATION")) - 1,
- 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
- ],
- blank=False,
- )
-
- # optionally makers can choose the escrow lock / invoice submission step length (seconds)
- escrow_duration = models.PositiveBigIntegerField(
- default=60 * int(config("INVOICE_AND_ESCROW_DURATION")) - 1,
- null=False,
- validators=[
- MinValueValidator(60 * 30), # Min is 30 minutes
- MaxValueValidator(60 * 60 * 8), # Max is 8 Hours
- ],
- blank=False,
- )
-
- # optionally makers can choose the fidelity bond size of the maker and taker (%)
- bond_size = models.DecimalField(
- max_digits=4,
- decimal_places=2,
- default=DEFAULT_BOND_SIZE,
- null=False,
- validators=[
- MinValueValidator(float(config("MIN_BOND_SIZE"))), # 1 %
- MaxValueValidator(float(config("MAX_BOND_SIZE"))), # 15 %
- ],
- blank=False,
- )
-
- # 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)],
- blank=True,
- ) # sats at creation
- last_satoshis = models.PositiveBigIntegerField(
- null=True,
- 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
- )
- # 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
- taker = models.ForeignKey(
- User,
- related_name="taker",
- on_delete=models.SET_NULL,
- null=True,
- default=None,
- blank=True,
- ) # unique = True, a taker can only take one order
- maker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
- taker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
-
- # When collaborative cancel is needed and one partner has cancelled.
- maker_asked_cancel = models.BooleanField(default=False, null=False)
- taker_asked_cancel = models.BooleanField(default=False, null=False)
-
- is_fiat_sent = models.BooleanField(default=False, null=False)
- reverted_fiat_sent = models.BooleanField(default=False, null=False)
-
- # in dispute
- is_disputed = models.BooleanField(default=False, null=False)
- maker_statement = models.TextField(
- max_length=50000, null=True, default=None, blank=True
- )
- taker_statement = models.TextField(
- max_length=50000, null=True, default=None, blank=True
- )
-
- # LNpayments
- # Order collateral
- maker_bond = models.OneToOneField(
- LNPayment,
- related_name="order_made",
- on_delete=models.SET_NULL,
- null=True,
- default=None,
- blank=True,
- )
- taker_bond = models.OneToOneField(
- LNPayment,
- related_name="order_taken",
- on_delete=models.SET_NULL,
- null=True,
- default=None,
- blank=True,
- )
- trade_escrow = models.OneToOneField(
- LNPayment,
- related_name="order_escrow",
- on_delete=models.SET_NULL,
- null=True,
- default=None,
- blank=True,
- )
- # is buyer payout a LN invoice (false) or on chain address (true)
- is_swap = models.BooleanField(default=False, null=False)
- # buyer payment LN invoice
- payout = models.OneToOneField(
- LNPayment,
- related_name="order_paid_LN",
- on_delete=models.SET_NULL,
- null=True,
- default=None,
- blank=True,
- )
- # buyer payment address
- payout_tx = models.OneToOneField(
- OnchainPayment,
- related_name="order_paid_TX",
- on_delete=models.SET_NULL,
- null=True,
- default=None,
- blank=True,
- )
-
- # ratings
- maker_rated = models.BooleanField(default=False, null=False)
- taker_rated = models.BooleanField(default=False, null=False)
- maker_platform_rated = models.BooleanField(default=False, null=False)
- taker_platform_rated = models.BooleanField(default=False, null=False)
-
- def __str__(self):
- if self.has_range and self.amount is None:
- amt = str(float(self.min_amount)) + "-" + str(float(self.max_amount))
- else:
- amt = float(self.amount)
- return f"Order {self.id}: {self.Types(self.type).label} BTC for {amt} {self.currency}"
-
- def t_to_expire(self, status):
-
- t_to_expire = {
- 0: int(config("EXP_MAKER_BOND_INVOICE")), # 'Waiting for maker bond'
- 1: self.public_duration, # 'Public'
- 2: 0, # 'Deleted'
- 3: int(config("EXP_TAKER_BOND_INVOICE")), # 'Waiting for taker bond'
- 4: 0, # 'Cancelled'
- 5: 0, # 'Expired'
- 6: int(
- self.escrow_duration
- ), # 'Waiting for trade collateral and buyer invoice'
- 7: int(self.escrow_duration), # 'Waiting only for seller trade collateral'
- 8: int(self.escrow_duration), # 'Waiting only for buyer invoice'
- 9: 60
- * 60
- * int(config("FIAT_EXCHANGE_DURATION")), # 'Sending fiat - In chatroom'
- 10: 60
- * 60
- * int(config("FIAT_EXCHANGE_DURATION")), # 'Fiat sent - In chatroom'
- 11: 1 * 24 * 60 * 60, # 'In dispute'
- 12: 0, # 'Collaboratively cancelled'
- 13: 100 * 24 * 60 * 60, # 'Sending satoshis to buyer'
- 14: 100 * 24 * 60 * 60, # 'Sucessful trade'
- 15: 100 * 24 * 60 * 60, # 'Failed lightning network routing'
- 16: 100 * 24 * 60 * 60, # 'Wait for dispute resolution'
- 17: 100 * 24 * 60 * 60, # 'Maker lost dispute'
- 18: 100 * 24 * 60 * 60, # 'Taker lost dispute'
- }
-
- return t_to_expire[status]
-
-
-@receiver(pre_delete, sender=Order)
-def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
- to_delete = (
- instance.maker_bond,
- instance.payout,
- instance.taker_bond,
- instance.trade_escrow,
- )
-
- for lnpayment in to_delete:
- try:
- lnpayment.delete()
- except Exception:
- pass
-
-
-class Profile(models.Model):
- user = models.OneToOneField(User, on_delete=models.CASCADE)
-
- # PGP keys, used for E2E chat encryption. Priv key is encrypted with user's passphrase (highEntropyToken)
- public_key = models.TextField(
- # Actualy only 400-500 characters for ECC, but other types might be longer
- max_length=2000,
- null=True,
- default=None,
- blank=True,
- )
- encrypted_private_key = models.TextField(
- max_length=2000,
- null=True,
- default=None,
- blank=True,
- )
-
- # Total trades
- total_contracts = models.PositiveIntegerField(null=False, default=0)
-
- # Ratings stored as a comma separated integer list
- total_ratings = models.PositiveIntegerField(null=False, default=0)
- latest_ratings = models.CharField(
- max_length=999,
- null=True,
- default=None,
- validators=[validate_comma_separated_integer_list],
- blank=True,
- ) # Will only store latest rating
- avg_rating = models.DecimalField(
- max_digits=4,
- decimal_places=1,
- default=None,
- null=True,
- 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)
-
- # Referral program
- is_referred = models.BooleanField(default=False, null=False)
- referred_by = models.ForeignKey(
- "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)
- # 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
- earned_rewards = models.PositiveIntegerField(null=False, default=0)
- # Total claimed rewards
- claimed_rewards = models.PositiveIntegerField(null=False, default=0)
-
- # Disputes
- num_disputes = models.PositiveIntegerField(null=False, default=0)
- lost_disputes = models.PositiveIntegerField(null=False, default=0)
- num_disputes_started = models.PositiveIntegerField(null=False, default=0)
- orders_disputes_started = models.CharField(
- max_length=999,
- null=True,
- default=None,
- validators=[validate_comma_separated_integer_list],
- blank=True,
- ) # Will only store ID of orders
-
- # RoboHash
- avatar = models.ImageField(
- default=("static/assets/avatars/" + "unknown_avatar.png"),
- verbose_name="Avatar",
- blank=True,
- )
-
- # 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)
-
- # Platform rate
- platform_rating = models.PositiveIntegerField(null=True, default=None, blank=True)
-
- # Stealth invoices
- wants_stealth = models.BooleanField(default=True, null=False)
-
- @receiver(post_save, sender=User)
- def create_user_profile(sender, instance, created, **kwargs):
- if created:
- Profile.objects.create(user=instance)
-
- @receiver(post_save, sender=User)
- def save_user_profile(sender, instance, **kwargs):
- instance.profile.save()
-
- @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.unlink()
- except Exception:
- pass
-
- def __str__(self):
- return self.user.username
-
- # to display avatars in admin panel
- def get_avatar(self):
- if not self.avatar:
- return settings.STATIC_ROOT + "unknown_avatar.png"
- return self.avatar.url
-
- # method to create a fake table field in read only mode
- def avatar_tag(self):
- return mark_safe('' % self.get_avatar())
-
-
-class MarketTick(models.Model):
- """
- Records tick by tick Non-KYC Bitcoin price.
- Data to be aggregated and offered via public API.
-
- It is checked against current CEX price for useful
- insight on the historical premium of Non-KYC BTC
-
- Price is set when taker bond is locked. Both
- maker and taker are commited with bonds (contract
- is finished and cancellation has a cost)
- """
-
- id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
- price = models.DecimalField(
- max_digits=16,
- decimal_places=2,
- default=None,
- null=True,
- validators=[MinValueValidator(0)],
- )
- volume = models.DecimalField(
- max_digits=8,
- decimal_places=8,
- default=None,
- null=True,
- validators=[MinValueValidator(0)],
- )
- premium = models.DecimalField(
- max_digits=5,
- decimal_places=2,
- default=None,
- null=True,
- validators=[MinValueValidator(-100), MaxValueValidator(999)],
- blank=True,
- )
- 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
- fee = models.DecimalField(
- max_digits=4,
- decimal_places=4,
- default=FEE,
- validators=[MinValueValidator(0), MaxValueValidator(1)],
- )
-
- def log_a_tick(order):
- """
- Creates a new tick
- """
-
- if not order.taker_bond:
- return None
-
- elif order.taker_bond.status == LNPayment.Status.LOCKED:
- volume = order.last_satoshis / 100000000
- price = float(order.amount) / volume # Amount Fiat / Amount BTC
- 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.save()
-
- def __str__(self):
- return f"Tick: {str(self.id)[:8]}"
-
- class Meta:
- verbose_name = "Market tick"
- verbose_name_plural = "Market ticks"
diff --git a/api/models/__init__.py b/api/models/__init__.py
new file mode 100644
index 00000000..d29ac0e4
--- /dev/null
+++ b/api/models/__init__.py
@@ -0,0 +1,8 @@
+from .currency import Currency
+from .ln_payment import LNPayment
+from .market_tick import MarketTick
+from .onchain_payment import OnchainPayment
+from .order import Order
+from .robot import Robot
+
+__all__ = ["Currency", "LNPayment", "MarketTick", "OnchainPayment", "Order", "Robot"]
diff --git a/api/models/currency.py b/api/models/currency.py
new file mode 100644
index 00000000..a6a3ffef
--- /dev/null
+++ b/api/models/currency.py
@@ -0,0 +1,31 @@
+import json
+
+from django.core.validators import MinValueValidator
+from django.db import models
+from django.utils import timezone
+
+
+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 = models.PositiveSmallIntegerField(
+ choices=currency_choices, null=False, unique=True
+ )
+ exchange_rate = models.DecimalField(
+ max_digits=18,
+ decimal_places=4,
+ default=None,
+ null=True,
+ validators=[MinValueValidator(0)],
+ )
+ timestamp = models.DateTimeField(default=timezone.now)
+
+ def __str__(self):
+ # returns currency label ( 3 letters code)
+ return self.currency_dict[str(self.currency)]
+
+ class Meta:
+ verbose_name = "Cached market currency"
+ verbose_name_plural = "Currencies"
diff --git a/api/models/ln_payment.py b/api/models/ln_payment.py
new file mode 100644
index 00000000..4d13feb6
--- /dev/null
+++ b/api/models/ln_payment.py
@@ -0,0 +1,132 @@
+from decouple import config
+from django.contrib.auth.models import User
+from django.core.validators import MaxValueValidator, MinValueValidator
+from django.db import models
+from django.template.defaultfilters import truncatechars
+
+
+class LNPayment(models.Model):
+ class Types(models.IntegerChoices):
+ NORM = 0, "Regular invoice"
+ HOLD = 1, "hold invoice"
+
+ class Concepts(models.IntegerChoices):
+ MAKEBOND = 0, "Maker bond"
+ TAKEBOND = 1, "Taker bond"
+ TRESCROW = 2, "Trade escrow"
+ PAYBUYER = 3, "Payment to buyer"
+ WITHREWA = 4, "Withdraw rewards"
+
+ class Status(models.IntegerChoices):
+ INVGEN = 0, "Generated"
+ LOCKED = 1, "Locked"
+ SETLED = 2, "Settled"
+ RETNED = 3, "Returned"
+ CANCEL = 4, "Cancelled"
+ EXPIRE = 5, "Expired"
+ VALIDI = 6, "Valid"
+ FLIGHT = 7, "In flight"
+ SUCCED = 8, "Succeeded"
+ FAILRO = 9, "Routing failed"
+
+ 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.",
+ )
+ NONRECO = 3, "A non-recoverable error has occurred."
+ 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
+ )
+
+ # payment info
+ payment_hash = models.CharField(
+ max_length=100, unique=True, default=None, blank=True, primary_key=True
+ )
+ invoice = models.CharField(
+ max_length=1200, unique=True, null=True, default=None, blank=True
+ ) # Some invoices with lots of routing hints might be long
+ preimage = models.CharField(
+ max_length=64, unique=True, null=True, default=None, blank=True
+ )
+ description = models.CharField(
+ max_length=500, unique=False, null=True, default=None, blank=True
+ )
+ num_satoshis = models.PositiveBigIntegerField(
+ validators=[
+ MinValueValidator(100),
+ MaxValueValidator(1.5 * config("MAX_TRADE", cast=int, default=1_000_000)),
+ ]
+ )
+ # Routing budget in PPM
+ routing_budget_ppm = models.PositiveBigIntegerField(
+ default=0,
+ null=False,
+ validators=[
+ MinValueValidator(0),
+ MaxValueValidator(100_000),
+ ],
+ )
+ # Routing budget in Sats. Only for reporting summaries.
+ routing_budget_sats = models.DecimalField(
+ max_digits=10, decimal_places=3, default=0, null=False, blank=False
+ )
+ # Fee in sats with mSats decimals fee_msat
+ 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)
+
+ # routing
+ routing_attempts = models.PositiveSmallIntegerField(null=False, default=0)
+ last_routing_time = models.DateTimeField(null=True, default=None, blank=True)
+ in_flight = models.BooleanField(default=False, null=False, blank=False)
+ # involved parties
+ sender = models.ForeignKey(
+ User, related_name="sender", on_delete=models.SET_NULL, null=True, default=None
+ )
+ receiver = models.ForeignKey(
+ User,
+ related_name="receiver",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ )
+
+ def __str__(self):
+ return f"LN-{str(self.payment_hash)[:8]}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
+
+ class Meta:
+ verbose_name = "Lightning payment"
+ verbose_name_plural = "Lightning payments"
+
+ @property
+ def hash(self):
+ # Payment hash is the primary key of LNpayments
+ # However it is too long for the admin panel.
+ # We created a truncated property for display 'hash'
+ return truncatechars(self.payment_hash, 10)
diff --git a/api/models/market_tick.py b/api/models/market_tick.py
new file mode 100644
index 00000000..73352b5d
--- /dev/null
+++ b/api/models/market_tick.py
@@ -0,0 +1,84 @@
+import uuid
+
+from decouple import config
+from django.core.validators import MaxValueValidator, MinValueValidator
+from django.db import models
+from django.utils import timezone
+
+FEE = float(config("FEE"))
+
+
+class MarketTick(models.Model):
+ """
+ Records tick by tick Non-KYC Bitcoin price.
+ Data to be aggregated and offered via public API.
+
+ It is checked against current CEX price for useful
+ insight on the historical premium of Non-KYC BTC
+
+ Price is set when taker bond is locked. Both
+ maker and taker are committed with bonds (contract
+ is finished and cancellation has a cost)
+ """
+
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
+ price = models.DecimalField(
+ max_digits=16,
+ decimal_places=2,
+ default=None,
+ null=True,
+ validators=[MinValueValidator(0)],
+ )
+ volume = models.DecimalField(
+ max_digits=8,
+ decimal_places=8,
+ default=None,
+ null=True,
+ validators=[MinValueValidator(0)],
+ )
+ premium = models.DecimalField(
+ max_digits=5,
+ decimal_places=2,
+ default=None,
+ null=True,
+ validators=[MinValueValidator(-100), MaxValueValidator(999)],
+ blank=True,
+ )
+ currency = models.ForeignKey("api.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
+ fee = models.DecimalField(
+ max_digits=4,
+ decimal_places=4,
+ default=config("FEE", cast=float, default=0),
+ validators=[MinValueValidator(0), MaxValueValidator(1)],
+ )
+
+ def log_a_tick(order):
+ """
+ Creates a new tick
+ """
+ from api.models import LNPayment
+
+ if not order.taker_bond:
+ return None
+
+ elif order.taker_bond.status == LNPayment.Status.LOCKED:
+ volume = order.last_satoshis / 100_000_000
+ price = float(order.amount) / volume # Amount Fiat / Amount BTC
+ 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.save()
+
+ def __str__(self):
+ return f"Tick: {str(self.id)[:8]}"
+
+ class Meta:
+ verbose_name = "Market tick"
+ verbose_name_plural = "Market ticks"
diff --git a/api/models/onchain_payment.py b/api/models/onchain_payment.py
new file mode 100644
index 00000000..f3efc947
--- /dev/null
+++ b/api/models/onchain_payment.py
@@ -0,0 +1,120 @@
+from decouple import config
+from django.contrib.auth.models import User
+from django.core.validators import MaxValueValidator, MinValueValidator
+from django.db import models
+from django.template.defaultfilters import truncatechars
+from django.utils import timezone
+
+from control.models import BalanceLog
+
+MAX_TRADE = config("MAX_TRADE", cast=int, default=1_000_000)
+MIN_SWAP_AMOUNT = config("MIN_SWAP_AMOUNT", cast=int, default=1_000_000)
+
+
+class OnchainPayment(models.Model):
+ class Concepts(models.IntegerChoices):
+ PAYBUYER = 3, "Payment to buyer"
+
+ class Status(models.IntegerChoices):
+ CREAT = 0, "Created" # User was given platform fees and suggested mining fees
+ VALID = 1, "Valid" # Valid onchain address and fee submitted
+ MEMPO = 2, "In mempool" # Tx is sent to mempool
+ CONFI = 3, "Confirmed" # Tx is confirmed +2 blocks
+ CANCE = 4, "Cancelled" # Cancelled tx
+ QUEUE = 5, "Queued" # Payment is queued to be sent out
+
+ def get_balance():
+ balance = BalanceLog.objects.create()
+ return balance.time
+
+ # payment use details
+ concept = models.PositiveSmallIntegerField(
+ choices=Concepts.choices, null=False, default=Concepts.PAYBUYER
+ )
+ status = models.PositiveSmallIntegerField(
+ choices=Status.choices, null=False, default=Status.CREAT
+ )
+
+ broadcasted = models.BooleanField(default=False, null=False, blank=False)
+
+ # payment info
+ address = models.CharField(
+ max_length=100, unique=False, default=None, null=True, blank=True
+ )
+
+ txid = models.CharField(
+ max_length=64, unique=True, null=True, default=None, blank=True
+ )
+
+ num_satoshis = models.PositiveBigIntegerField(
+ null=True,
+ validators=[
+ MinValueValidator(0.5 * MIN_SWAP_AMOUNT),
+ MaxValueValidator(1.5 * MAX_TRADE),
+ ],
+ )
+ sent_satoshis = models.PositiveBigIntegerField(
+ null=True,
+ validators=[
+ MinValueValidator(0.5 * MIN_SWAP_AMOUNT),
+ MaxValueValidator(1.5 * MAX_TRADE),
+ ],
+ )
+ # fee in sats/vbyte with mSats decimals fee_msat
+ suggested_mining_fee_rate = models.DecimalField(
+ max_digits=6,
+ decimal_places=3,
+ default=2.05,
+ null=False,
+ blank=False,
+ validators=[MinValueValidator(1), MaxValueValidator(999)],
+ )
+ mining_fee_rate = models.DecimalField(
+ max_digits=6,
+ decimal_places=3,
+ default=2.05,
+ null=False,
+ blank=False,
+ validators=[MinValueValidator(1), MaxValueValidator(999)],
+ )
+ mining_fee_sats = models.PositiveBigIntegerField(default=0, null=False, blank=False)
+
+ # platform onchain/channels balance at creation, swap fee rate as percent of total volume
+ balance = models.ForeignKey(
+ BalanceLog,
+ related_name="balance",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=get_balance,
+ )
+
+ swap_fee_rate = models.DecimalField(
+ max_digits=4,
+ decimal_places=2,
+ default=config("MIN_SWAP_FEE", cast=float, default=0.01) * 100,
+ null=False,
+ blank=False,
+ )
+
+ created_at = models.DateTimeField(default=timezone.now)
+
+ # involved parties
+ receiver = models.ForeignKey(
+ User,
+ related_name="tx_receiver",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ )
+
+ def __str__(self):
+ return f"TX-{str(self.id)}: {self.Concepts(self.concept).label} - {self.Status(self.status).label}"
+
+ class Meta:
+ verbose_name = "Onchain payment"
+ verbose_name_plural = "Onchain payments"
+
+ @property
+ def hash(self):
+ # Display txid as 'hash' truncated
+ return truncatechars(self.txid, 10)
diff --git a/api/models/order.py b/api/models/order.py
new file mode 100644
index 00000000..06fd71aa
--- /dev/null
+++ b/api/models/order.py
@@ -0,0 +1,285 @@
+import uuid
+
+from decouple import config
+from django.contrib.auth.models import User
+from django.core.validators import MaxValueValidator, MinValueValidator
+from django.db import models
+from django.db.models.signals import pre_delete
+from django.dispatch import receiver
+from django.utils import timezone
+
+MIN_TRADE = config("MIN_TRADE", cast=int, default=20_000)
+MAX_TRADE = config("MAX_TRADE", cast=int, default=1_000_000)
+FIAT_EXCHANGE_DURATION = config("FIAT_EXCHANGE_DURATION", cast=int, default=24)
+
+
+class Order(models.Model):
+ class Types(models.IntegerChoices):
+ BUY = 0, "BUY"
+ SELL = 1, "SELL"
+
+ class Status(models.IntegerChoices):
+ WFB = 0, "Waiting for maker bond"
+ PUB = 1, "Public"
+ PAU = 2, "Paused"
+ TAK = 3, "Waiting for taker bond"
+ UCA = 4, "Cancelled"
+ EXP = 5, "Expired"
+ WF2 = 6, "Waiting for trade collateral and buyer invoice"
+ WFE = 7, "Waiting only for seller trade collateral"
+ WFI = 8, "Waiting only for buyer invoice"
+ CHA = 9, "Sending fiat - In chatroom"
+ FSE = 10, "Fiat sent - In chatroom"
+ DIS = 11, "In dispute"
+ CCA = 12, "Collaboratively cancelled"
+ PAY = 13, "Sending satoshis to buyer"
+ SUC = 14, "Sucessful trade"
+ FAI = 15, "Failed lightning network routing"
+ WFR = 16, "Wait for dispute resolution"
+ MLD = 17, "Maker lost dispute"
+ TLD = 18, "Taker lost dispute"
+
+ class ExpiryReasons(models.IntegerChoices):
+ NTAKEN = 0, "Expired not taken"
+ NMBOND = 1, "Maker bond not locked"
+ NESCRO = 2, "Escrow not locked"
+ NINVOI = 3, "Invoice not submitted"
+ NESINV = 4, "Neither escrow locked or invoice submitted"
+
+ # order info
+ reference = models.UUIDField(default=uuid.uuid4, editable=False)
+ status = models.PositiveSmallIntegerField(
+ choices=Status.choices, null=False, default=Status.WFB
+ )
+ created_at = models.DateTimeField(default=timezone.now)
+ expires_at = models.DateTimeField()
+ 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("api.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
+ )
+ # order pricing method. A explicit amount of sats, or a relative premium above/below market.
+ is_explicit = models.BooleanField(default=False, null=False)
+ # marked to market
+ premium = models.DecimalField(
+ max_digits=5,
+ decimal_places=2,
+ default=0,
+ null=True,
+ validators=[MinValueValidator(-100), MaxValueValidator(999)],
+ blank=True,
+ )
+ # explicit
+ satoshis = models.PositiveBigIntegerField(
+ null=True,
+ validators=[MinValueValidator(MIN_TRADE), MaxValueValidator(MAX_TRADE)],
+ blank=True,
+ )
+ # optionally makers can choose the public order duration length (seconds)
+ public_duration = models.PositiveBigIntegerField(
+ default=60 * 60 * config("DEFAULT_PUBLIC_ORDER_DURATION", cast=int, default=24)
+ - 1,
+ null=False,
+ validators=[
+ MinValueValidator(
+ 60 * 60 * config("MIN_PUBLIC_ORDER_DURATION", cast=float, default=0.166)
+ ), # Min is 10 minutes
+ MaxValueValidator(
+ 60 * 60 * config("MAX_PUBLIC_ORDER_DURATION", cast=float, default=24)
+ ), # Max is 24 Hours
+ ],
+ blank=False,
+ )
+
+ # optionally makers can choose the escrow lock / invoice submission step length (seconds)
+ escrow_duration = models.PositiveBigIntegerField(
+ default=60 * int(config("INVOICE_AND_ESCROW_DURATION")) - 1,
+ null=False,
+ validators=[
+ MinValueValidator(60 * 30), # Min is 30 minutes
+ MaxValueValidator(60 * 60 * 8), # Max is 8 Hours
+ ],
+ blank=False,
+ )
+
+ # optionally makers can choose the fidelity bond size of the maker and taker (%)
+ bond_size = models.DecimalField(
+ max_digits=4,
+ decimal_places=2,
+ default=config("DEFAULT_BOND_SIZE", cast=float, default=3),
+ null=False,
+ validators=[
+ MinValueValidator(config("MIN_BOND_SIZE", cast=float, default=1)), # 1 %
+ MaxValueValidator(config("MAX_BOND_SIZE", cast=float, default=1)), # 15 %
+ ],
+ blank=False,
+ )
+
+ # 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)],
+ blank=True,
+ ) # sats at creation
+ last_satoshis = models.PositiveBigIntegerField(
+ null=True,
+ 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
+ )
+ # 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
+ taker = models.ForeignKey(
+ User,
+ related_name="taker",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ ) # unique = True, a taker can only take one order
+ maker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
+ taker_last_seen = models.DateTimeField(null=True, default=None, blank=True)
+
+ # When collaborative cancel is needed and one partner has cancelled.
+ maker_asked_cancel = models.BooleanField(default=False, null=False)
+ taker_asked_cancel = models.BooleanField(default=False, null=False)
+
+ is_fiat_sent = models.BooleanField(default=False, null=False)
+ reverted_fiat_sent = models.BooleanField(default=False, null=False)
+
+ # in dispute
+ is_disputed = models.BooleanField(default=False, null=False)
+ maker_statement = models.TextField(
+ max_length=50_000, null=True, default=None, blank=True
+ )
+ taker_statement = models.TextField(
+ max_length=50_000, null=True, default=None, blank=True
+ )
+
+ # LNpayments
+ # Order collateral
+ maker_bond = models.OneToOneField(
+ "api.LNPayment",
+ related_name="order_made",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ )
+ taker_bond = models.OneToOneField(
+ "api.LNPayment",
+ related_name="order_taken",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ )
+ trade_escrow = models.OneToOneField(
+ "api.LNPayment",
+ related_name="order_escrow",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ )
+ # is buyer payout a LN invoice (false) or on chain address (true)
+ is_swap = models.BooleanField(default=False, null=False)
+ # buyer payment LN invoice
+ payout = models.OneToOneField(
+ "api.LNPayment",
+ related_name="order_paid_LN",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ )
+ # buyer payment address
+ payout_tx = models.OneToOneField(
+ "api.OnchainPayment",
+ related_name="order_paid_TX",
+ on_delete=models.SET_NULL,
+ null=True,
+ default=None,
+ blank=True,
+ )
+
+ # ratings
+ maker_rated = models.BooleanField(default=False, null=False)
+ taker_rated = models.BooleanField(default=False, null=False)
+ maker_platform_rated = models.BooleanField(default=False, null=False)
+ taker_platform_rated = models.BooleanField(default=False, null=False)
+
+ def __str__(self):
+ if self.has_range and self.amount is None:
+ amt = str(float(self.min_amount)) + "-" + str(float(self.max_amount))
+ else:
+ amt = float(self.amount)
+ return f"Order {self.id}: {self.Types(self.type).label} BTC for {amt} {self.currency}"
+
+ def t_to_expire(self, status):
+
+ t_to_expire = {
+ 0: config(
+ "EXP_MAKER_BOND_INVOICE", cast=int, default=300
+ ), # 'Waiting for maker bond'
+ 1: self.public_duration, # 'Public'
+ 2: 0, # 'Deleted'
+ 3: config(
+ "EXP_TAKER_BOND_INVOICE", cast=int, default=150
+ ), # 'Waiting for taker bond'
+ 4: 0, # 'Cancelled'
+ 5: 0, # 'Expired'
+ 6: int(
+ self.escrow_duration
+ ), # 'Waiting for trade collateral and buyer invoice'
+ 7: int(self.escrow_duration), # 'Waiting only for seller trade collateral'
+ 8: int(self.escrow_duration), # 'Waiting only for buyer invoice'
+ 9: 60 * 60 * FIAT_EXCHANGE_DURATION, # 'Sending fiat - In chatroom'
+ 10: 60 * 60 * FIAT_EXCHANGE_DURATION, # 'Fiat sent - In chatroom'
+ 11: 1 * 24 * 60 * 60, # 'In dispute'
+ 12: 0, # 'Collaboratively cancelled'
+ 13: 100 * 24 * 60 * 60, # 'Sending satoshis to buyer'
+ 14: 100 * 24 * 60 * 60, # 'Successful trade'
+ 15: 100 * 24 * 60 * 60, # 'Failed lightning network routing'
+ 16: 100 * 24 * 60 * 60, # 'Wait for dispute resolution'
+ 17: 100 * 24 * 60 * 60, # 'Maker lost dispute'
+ 18: 100 * 24 * 60 * 60, # 'Taker lost dispute'
+ }
+
+ return t_to_expire[status]
+
+
+@receiver(pre_delete, sender=Order)
+def delete_lnpayment_at_order_deletion(sender, instance, **kwargs):
+ to_delete = (
+ instance.maker_bond,
+ instance.payout,
+ instance.taker_bond,
+ instance.trade_escrow,
+ )
+
+ for lnpayment in to_delete:
+ try:
+ lnpayment.delete()
+ except Exception:
+ pass
diff --git a/api/models/robot.py b/api/models/robot.py
new file mode 100644
index 00000000..fe718be1
--- /dev/null
+++ b/api/models/robot.py
@@ -0,0 +1,137 @@
+from pathlib import Path
+
+from django.conf import settings
+from django.contrib.auth.models import User
+from django.core.validators import (
+ MaxValueValidator,
+ MinValueValidator,
+ validate_comma_separated_integer_list,
+)
+from django.db import models
+from django.db.models.signals import post_save, pre_delete
+from django.dispatch import receiver
+from django.utils.html import mark_safe
+
+
+class Robot(models.Model):
+ user = models.OneToOneField(User, on_delete=models.CASCADE)
+
+ # PGP keys, used for E2E chat encryption. Priv key is encrypted with user's passphrase (highEntropyToken)
+ public_key = models.TextField(
+ # Actually only 400-500 characters for ECC, but other types might be longer
+ max_length=2000,
+ null=True,
+ default=None,
+ blank=True,
+ )
+ encrypted_private_key = models.TextField(
+ max_length=2000,
+ null=True,
+ default=None,
+ blank=True,
+ )
+
+ # Total trades
+ total_contracts = models.PositiveIntegerField(null=False, default=0)
+
+ # Ratings stored as a comma separated integer list
+ total_ratings = models.PositiveIntegerField(null=False, default=0)
+ latest_ratings = models.CharField(
+ max_length=999,
+ null=True,
+ default=None,
+ validators=[validate_comma_separated_integer_list],
+ blank=True,
+ ) # Will only store latest rating
+ avg_rating = models.DecimalField(
+ max_digits=4,
+ decimal_places=1,
+ default=None,
+ null=True,
+ 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)
+
+ # Referral program
+ is_referred = models.BooleanField(default=False, null=False)
+ referred_by = models.ForeignKey(
+ "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)
+ # Recent rewards from referred trades that will be "earned" at a later point to difficult espionage.
+ pending_rewards = models.PositiveIntegerField(null=False, default=0)
+ # Claimable rewards
+ earned_rewards = models.PositiveIntegerField(null=False, default=0)
+ # Total claimed rewards
+ claimed_rewards = models.PositiveIntegerField(null=False, default=0)
+
+ # Disputes
+ num_disputes = models.PositiveIntegerField(null=False, default=0)
+ lost_disputes = models.PositiveIntegerField(null=False, default=0)
+ num_disputes_started = models.PositiveIntegerField(null=False, default=0)
+ orders_disputes_started = models.CharField(
+ max_length=999,
+ null=True,
+ default=None,
+ validators=[validate_comma_separated_integer_list],
+ blank=True,
+ ) # Will only store ID of orders
+
+ # RoboHash
+ avatar = models.ImageField(
+ default=("static/assets/avatars/" + "unknown_avatar.png"),
+ verbose_name="Avatar",
+ blank=True,
+ )
+
+ # 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)
+
+ # Platform rate
+ platform_rating = models.PositiveIntegerField(null=True, default=None, blank=True)
+
+ # Stealth invoices
+ wants_stealth = models.BooleanField(default=True, null=False)
+
+ @receiver(post_save, sender=User)
+ def create_user_robot(sender, instance, created, **kwargs):
+ if created:
+ Robot.objects.create(user=instance)
+
+ @receiver(post_save, sender=User)
+ def save_user_robot(sender, instance, **kwargs):
+ instance.robot.save()
+
+ @receiver(pre_delete, sender=User)
+ def del_avatar_from_disk(sender, instance, **kwargs):
+ try:
+ avatar_file = Path(
+ settings.AVATAR_ROOT + instance.robot.avatar.url.split("/")[-1]
+ )
+ avatar_file.unlink()
+ except Exception:
+ pass
+
+ def __str__(self):
+ return self.user.username
+
+ # to display avatars in admin panel
+ def get_avatar(self):
+ if not self.avatar:
+ return settings.STATIC_ROOT + "unknown_avatar.png"
+ return self.avatar.url
+
+ # method to create a fake table field in read only mode
+ def avatar_tag(self):
+ return mark_safe('' % self.get_avatar())
diff --git a/api/nick_generator/dicts/en/nouns.py b/api/nick_generator/dicts/en/nouns.py
index 0f743bef..c55e1819 100755
--- a/api/nick_generator/dicts/en/nouns.py
+++ b/api/nick_generator/dicts/en/nouns.py
@@ -7540,7 +7540,7 @@ nouns = [
"Professorship",
"Profet",
"Proficiency",
- "Profile",
+ "Robot",
"Profit",
"Profitability",
"Profoundity",
@@ -12095,7 +12095,7 @@ nouns = [
"Profession",
"Professional",
"Professor",
- "Profile",
+ "Robot",
"Profit",
"Program",
"Progress",
diff --git a/api/nick_generator/nick_generator.py b/api/nick_generator/nick_generator.py
index bf40fa8a..dc9ea2cf 100755
--- a/api/nick_generator/nick_generator.py
+++ b/api/nick_generator/nick_generator.py
@@ -3,19 +3,18 @@ import time
from .utils import human_format
-"""
-Deterministic nick generator from SHA256 hash.
-
-It builds Nicknames as:
-Adverb + Adjective + Noun + Numeric(0-999)
-
-With the current English dictionaries there
-is a total of to 450*4800*12500*1000 =
-28 Trillion deterministic nicks
-"""
-
class NickGenerator:
+ """
+ Deterministic nick generator from SHA256 hash.
+
+ It builds Nicknames as:
+ Adverb + Adjective + Noun + Numeric(0-999)
+
+ With the current English dictionaries there
+ is a total of to 450*4800*12500*1000 =
+ 28 Trillion deterministic nicks"""
+
def __init__(
self,
lang="English",
@@ -128,7 +127,7 @@ class NickGenerator:
self,
primer_hash=None,
max_length=25,
- max_iter=10000,
+ max_iter=10_000,
):
"""
Generates Nicks that are short.
@@ -149,7 +148,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=1_000_000, num_runs=5000):
"""
Computes median an average loss of
nick pool diversity due to max_lenght
@@ -162,7 +161,7 @@ class NickGenerator:
attempts = []
for i in range(num_runs):
- string = str(random.uniform(0, 1000000))
+ string = str(random.uniform(0, 1_000_000))
hash = hashlib.sha256(str.encode(string)).hexdigest()
_, _, pool_size, tries = self.short_from_SHA256(hash, max_length)
@@ -188,7 +187,7 @@ if __name__ == "__main__":
nick_lang = "English" # Spanish
hash = hashlib.sha256(b"No one expected such cool nick!!").hexdigest()
max_length = 22
- max_iter = 100000000
+ max_iter = 100_000_000
# Initialized nick generator
GenNick = NickGenerator(lang=nick_lang)
@@ -215,7 +214,7 @@ if __name__ == "__main__":
random.seed(1)
for i in range(100):
- string = str(random.uniform(0, 1000000))
+ string = str(random.uniform(0, 1_000_000))
hash = hashlib.sha256(str.encode(string)).hexdigest()
print(
GenNick.short_from_SHA256(hash, max_length=max_length, max_iter=max_iter)[0]
diff --git a/api/notifications.py b/api/notifications.py
index 3358ec12..0f666f67 100644
--- a/api/notifications.py
+++ b/api/notifications.py
@@ -15,16 +15,16 @@ class Telegram:
def get_context(user):
"""returns context needed to enable TG notifications"""
context = {}
- if user.profile.telegram_enabled:
+ if user.robot.telegram_enabled:
context["tg_enabled"] = True
else:
context["tg_enabled"] = False
- if user.profile.telegram_token is None:
- user.profile.telegram_token = token_urlsafe(15)
- user.profile.save()
+ if user.robot.telegram_token is None:
+ user.robot.telegram_token = token_urlsafe(15)
+ user.robot.save()
- context["tg_token"] = user.profile.telegram_token
+ context["tg_token"] = user.robot.telegram_token
context["tg_bot_name"] = config("TELEGRAM_BOT_NAME")
return context
@@ -46,103 +46,103 @@ class Telegram:
def welcome(self, user):
"""User enabled Telegram Notifications"""
- lang = user.profile.telegram_lang_code
+ lang = user.robot.telegram_lang_code
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.profile.telegram_chat_id, text)
- user.profile.telegram_welcomed = True
- user.profile.save()
+ self.send_message(user.robot.telegram_chat_id, text)
+ user.robot.telegram_welcomed = True
+ user.robot.save()
return
def order_taken_confirmed(self, order):
- if order.maker.profile.telegram_enabled:
- lang = order.maker.profile.telegram_lang_code
+ if order.maker.robot.telegram_enabled:
+ lang = order.maker.robot.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."
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."
- self.send_message(order.maker.profile.telegram_chat_id, text)
+ self.send_message(order.maker.robot.telegram_chat_id, text)
- if order.taker.profile.telegram_enabled:
- lang = order.taker.profile.telegram_lang_code
+ if order.taker.robot.telegram_enabled:
+ lang = order.taker.robot.telegram_lang_code
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}."
- self.send_message(order.taker.profile.telegram_chat_id, text)
+ self.send_message(order.taker.robot.telegram_chat_id, text)
return
def fiat_exchange_starts(self, order):
for user in [order.maker, order.taker]:
- if user.profile.telegram_enabled:
- lang = user.profile.telegram_lang_code
+ if user.robot.telegram_enabled:
+ lang = user.robot.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."
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."
- self.send_message(user.profile.telegram_chat_id, text)
+ self.send_message(user.robot.telegram_chat_id, text)
return
def order_expired_untaken(self, order):
- if order.maker.profile.telegram_enabled:
- lang = order.maker.profile.telegram_lang_code
+ if order.maker.robot.telegram_enabled:
+ lang = order.maker.robot.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."
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."
- self.send_message(order.maker.profile.telegram_chat_id, text)
+ self.send_message(order.maker.robot.telegram_chat_id, text)
return
def trade_successful(self, order):
for user in [order.maker, order.taker]:
- if user.profile.telegram_enabled:
- lang = user.profile.telegram_lang_code
+ if user.robot.telegram_enabled:
+ lang = user.robot.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."
else:
text = f"🥳 Your order with ID {order.id} has finished successfully!⚡ Join us @robosats and help us improve."
- self.send_message(user.profile.telegram_chat_id, text)
+ self.send_message(user.robot.telegram_chat_id, text)
return
def public_order_cancelled(self, order):
- if order.maker.profile.telegram_enabled:
- lang = order.maker.profile.telegram_lang_code
+ if order.maker.robot.telegram_enabled:
+ lang = order.maker.robot.telegram_lang_code
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}."
- self.send_message(order.maker.profile.telegram_chat_id, text)
+ self.send_message(order.maker.robot.telegram_chat_id, text)
return
def collaborative_cancelled(self, order):
for user in [order.maker, order.taker]:
- if user.profile.telegram_enabled:
- lang = user.profile.telegram_lang_code
+ if user.robot.telegram_enabled:
+ lang = user.robot.telegram_lang_code
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."
- self.send_message(user.profile.telegram_chat_id, text)
+ self.send_message(user.robot.telegram_chat_id, text)
return
def dispute_opened(self, order):
for user in [order.maker, order.taker]:
- if user.profile.telegram_enabled:
- lang = user.profile.telegram_lang_code
+ if user.robot.telegram_enabled:
+ lang = user.robot.telegram_lang_code
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)}."
- self.send_message(user.profile.telegram_chat_id, text)
+ self.send_message(user.robot.telegram_chat_id, text)
return
def order_published(self, order):
- if order.maker.profile.telegram_enabled:
- lang = order.maker.profile.telegram_lang_code
+ if order.maker.robot.telegram_enabled:
+ lang = order.maker.robot.telegram_lang_code
# In weird cases the order cannot be found (e.g. it is cancelled)
queryset = Order.objects.filter(maker=order.maker)
if len(queryset) == 0:
@@ -152,7 +152,7 @@ class Telegram:
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.profile.telegram_chat_id, text)
+ self.send_message(order.maker.robot.telegram_chat_id, text)
return
def new_chat_message(self, order, chat_message):
@@ -180,8 +180,8 @@ class Telegram:
notification_reason = f"(You receive this notification because this was the first in-chat message. You will only be notified again if there is a gap bigger than {TIMEGAP} minutes between messages)"
user = chat_message.receiver
- if user.profile.telegram_enabled:
+ if user.robot.telegram_enabled:
text = f"💬 Hey {user.username}, a new chat message in-app was sent to you by {chat_message.sender.username} for order ID {str(order.id)}. {notification_reason}"
- self.send_message(user.profile.telegram_chat_id, text)
+ self.send_message(user.robot.telegram_chat_id, text)
return
diff --git a/api/serializers.py b/api/serializers.py
index b33d8314..d539cde4 100644
--- a/api/serializers.py
+++ b/api/serializers.py
@@ -484,7 +484,7 @@ class UpdateOrderSerializer(serializers.Serializer):
routing_budget_ppm = serializers.IntegerField(
default=0,
min_value=0,
- max_value=100001,
+ max_value=100_001,
allow_null=True,
required=False,
help_text="Max budget to allocate for routing in PPM",
@@ -493,7 +493,7 @@ class UpdateOrderSerializer(serializers.Serializer):
max_length=100, allow_null=True, allow_blank=True, default=None
)
statement = serializers.CharField(
- max_length=500000, allow_null=True, allow_blank=True, default=None
+ max_length=500_000, allow_null=True, allow_blank=True, default=None
)
action = serializers.ChoiceField(
choices=(
diff --git a/api/tasks.py b/api/tasks.py
index 20f5c50d..b9fba25c 100644
--- a/api/tasks.py
+++ b/api/tasks.py
@@ -22,16 +22,16 @@ def users_cleansing():
# And do not have an active trade, any past contract or any reward.
deleted_users = []
for user in queryset:
- # Try an except, due to unknown cause for users lacking profiles.
+ # Try an except, due to unknown cause for users lacking robots.
try:
if (
- user.profile.pending_rewards > 0
- or user.profile.earned_rewards > 0
- or user.profile.claimed_rewards > 0
- or user.profile.telegram_enabled is True
+ user.robot.pending_rewards > 0
+ or user.robot.earned_rewards > 0
+ or user.robot.claimed_rewards > 0
+ or user.robot.telegram_enabled is True
):
continue
- if not user.profile.total_contracts == 0:
+ if not user.robot.total_contracts == 0:
continue
valid, _, _ = Logics.validate_already_maker_or_taker(user)
if valid:
@@ -53,22 +53,22 @@ def give_rewards():
Referral rewards go from pending to earned.
Happens asynchronously so the referral program cannot be easily used to spy.
"""
- from api.models import Profile
+ from api.models import Robot
# Users who's last login has not been in the last 6 hours
- queryset = Profile.objects.filter(pending_rewards__gt=0)
+ queryset = Robot.objects.filter(pending_rewards__gt=0)
# And do not have an active trade, any past contract or any reward.
results = {}
- for profile in queryset:
- given_reward = profile.pending_rewards
- profile.earned_rewards += given_reward
- profile.pending_rewards = 0
- profile.save()
+ for robot in queryset:
+ given_reward = robot.pending_rewards
+ robot.earned_rewards += given_reward
+ robot.pending_rewards = 0
+ robot.save()
- results[profile.user.username] = {
+ results[robot.user.username] = {
"given_reward": given_reward,
- "earned_rewards": profile.earned_rewards,
+ "earned_rewards": robot.earned_rewards,
}
return results
@@ -93,7 +93,7 @@ def follow_send_payment(hash):
# Default is 0ppm. Set by the user over API. Client's default is 1000 ppm.
fee_limit_sat = int(
- float(lnpayment.num_satoshis) * float(lnpayment.routing_budget_ppm) / 1000000
+ float(lnpayment.num_satoshis) * float(lnpayment.routing_budget_ppm) / 1_000_000
)
timeout_seconds = config("PAYOUT_TIMEOUT_SECONDS", cast=int, default=90)
@@ -214,10 +214,8 @@ def send_notification(order_id=None, chat_message_id=None, message=None):
chat_message = Message.objects.get(id=chat_message_id)
order = chat_message.order
- taker_enabled = (
- False if order.taker is None else order.taker.profile.telegram_enabled
- )
- if not (order.maker.profile.telegram_enabled or taker_enabled):
+ taker_enabled = False if order.taker is None else order.taker.robot.telegram_enabled
+ if not (order.maker.robot.telegram_enabled or taker_enabled):
return
from api.notifications import Telegram
diff --git a/api/views.py b/api/views.py
index f7caffef..c9885896 100644
--- a/api/views.py
+++ b/api/views.py
@@ -19,7 +19,7 @@ from robohash import Robohash
from scipy.stats import entropy
from api.logics import Logics
-from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order, Profile
+from api.models import Currency, LNPayment, MarketTick, OnchainPayment, Order, Robot
from api.notifications import Telegram
from api.oas_schemas import (
BookViewSchema,
@@ -231,7 +231,7 @@ class OrderView(viewsets.ViewSet):
# if user is under a limit (penalty), inform him.
is_penalized, time_out = Logics.is_penalized(request.user)
if is_penalized:
- data["penalty"] = request.user.profile.penalty_expiration
+ data["penalty"] = request.user.robot.penalty_expiration
# Add booleans if user is maker, taker, partipant, buyer or seller
data["is_maker"] = order.maker == request.user
@@ -726,28 +726,28 @@ class UserView(APIView):
login(request, user)
context["referral_code"] = token_urlsafe(8)
- user.profile.referral_code = context["referral_code"]
- user.profile.avatar = "static/assets/avatars/" + nickname + ".webp"
+ user.robot.referral_code = context["referral_code"]
+ user.robot.avatar = "static/assets/avatars/" + nickname + ".webp"
# Noticed some PGP keys replaced at re-login. Should not happen.
- # Let's implement this sanity check "If profile has not keys..."
- if not user.profile.public_key:
- user.profile.public_key = public_key
- if not user.profile.encrypted_private_key:
- user.profile.encrypted_private_key = encrypted_private_key
+ # Let's implement this sanity check "If robot has not keys..."
+ if not user.robot.public_key:
+ user.robot.public_key = public_key
+ if not user.robot.encrypted_private_key:
+ user.robot.encrypted_private_key = encrypted_private_key
# If the ref_code was created by another robot, this robot was referred.
- queryset = Profile.objects.filter(referral_code=ref_code)
+ queryset = Robot.objects.filter(referral_code=ref_code)
if len(queryset) == 1:
- user.profile.is_referred = True
- user.profile.referred_by = queryset[0]
+ user.robot.is_referred = True
+ user.robot.referred_by = queryset[0]
- user.profile.save()
+ user.robot.save()
context = {**context, **Telegram.get_context(user)}
- context["public_key"] = user.profile.public_key
- context["encrypted_private_key"] = user.profile.encrypted_private_key
- context["wants_stealth"] = user.profile.wants_stealth
+ context["public_key"] = user.robot.public_key
+ context["encrypted_private_key"] = user.robot.encrypted_private_key
+ context["wants_stealth"] = user.robot.wants_stealth
return Response(context, status=status.HTTP_201_CREATED)
# log in user and return pub/priv keys if existing
@@ -755,11 +755,11 @@ class UserView(APIView):
user = authenticate(request, username=nickname, password=token_sha256)
if user is not None:
login(request, user)
- context["public_key"] = user.profile.public_key
- context["encrypted_private_key"] = user.profile.encrypted_private_key
- context["earned_rewards"] = user.profile.earned_rewards
- context["referral_code"] = str(user.profile.referral_code)
- context["wants_stealth"] = user.profile.wants_stealth
+ context["public_key"] = user.robot.public_key
+ context["encrypted_private_key"] = user.robot.encrypted_private_key
+ context["earned_rewards"] = user.robot.earned_rewards
+ context["referral_code"] = str(user.robot.referral_code)
+ context["wants_stealth"] = user.robot.wants_stealth
# Adds/generate telegram token and whether it is enabled
context = {**context, **Telegram.get_context(user)}
@@ -806,8 +806,8 @@ class UserView(APIView):
},
status.HTTP_400_BAD_REQUEST,
)
- # Check if has already a profile with
- if user.profile.total_contracts > 0:
+ # Check if has already a robot with
+ if user.robot.total_contracts > 0:
return Response(
{
"bad_request": "Maybe a mistake? User cannot be deleted as it has completed trades"
@@ -1017,8 +1017,8 @@ class LimitView(ListAPIView):
def get(self, request):
# Trade limits as BTC
- min_trade = float(config("MIN_TRADE")) / 100000000
- max_trade = float(config("MAX_TRADE")) / 100000000
+ min_trade = float(config("MIN_TRADE")) / 100_000_000
+ max_trade = float(config("MAX_TRADE")) / 100_000_000
payload = {}
queryset = Currency.objects.all().order_by("currency")
@@ -1070,7 +1070,7 @@ class StealthView(UpdateAPIView):
stealth = serializer.data.get("wantsStealth")
- request.user.profile.wants_stealth = stealth
- request.user.profile.save()
+ request.user.robot.wants_stealth = stealth
+ request.user.robot.save()
return Response({"wantsStealth": stealth})
diff --git a/chat/consumers.py b/chat/consumers.py
index 7dc0cfb4..5d4914ab 100644
--- a/chat/consumers.py
+++ b/chat/consumers.py
@@ -118,10 +118,10 @@ class ChatRoomConsumer(AsyncWebsocketConsumer):
order = Order.objects.get(id=self.order_id)
if order.maker == self.user:
- return order.taker.profile.public_key
+ return order.taker.robot.public_key
if order.taker == self.user:
- return order.maker.profile.public_key
+ return order.maker.robot.public_key
@database_sync_to_async
def get_all_PGP_messages(self):
diff --git a/chat/models.py b/chat/models.py
index 41c361fa..95b9ecdb 100644
--- a/chat/models.py
+++ b/chat/models.py
@@ -1,7 +1,8 @@
+from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone
-from api.models import Order, User
+from api.models import Order
class ChatRoom(models.Model):
diff --git a/chat/views.py b/chat/views.py
index fbc6ba4c..1186911a 100644
--- a/chat/views.py
+++ b/chat/views.py
@@ -2,11 +2,12 @@ from datetime import timedelta
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
+from django.contrib.auth.models import User
from django.utils import timezone
from rest_framework import status, viewsets
from rest_framework.response import Response
-from api.models import Order, User
+from api.models import Order
from api.tasks import send_notification
from chat.models import ChatRoom, Message
from chat.serializers import ChatSerializer, PostMessageSerializer
@@ -69,7 +70,7 @@ class ChatView(viewsets.ViewSet):
chatroom.maker_connected = True
chatroom.save()
peer_connected = chatroom.taker_connected
- peer_public_key = order.taker.profile.public_key
+ peer_public_key = order.taker.robot.public_key
elif chatroom.taker == request.user:
chatroom.maker_connected = order.maker_last_seen > (
timezone.now() - timedelta(minutes=1)
@@ -77,7 +78,7 @@ class ChatView(viewsets.ViewSet):
chatroom.taker_connected = True
chatroom.save()
peer_connected = chatroom.maker_connected
- peer_public_key = order.maker.profile.public_key
+ peer_public_key = order.maker.robot.public_key
messages = []
for message in queryset:
diff --git a/control/tasks.py b/control/tasks.py
index 8f7ddf3c..650661e1 100644
--- a/control/tasks.py
+++ b/control/tasks.py
@@ -13,7 +13,7 @@ def do_accounting():
from django.db.models import Sum
from django.utils import timezone
- from api.models import LNPayment, MarketTick, OnchainPayment, Order, Profile
+ from api.models import LNPayment, MarketTick, OnchainPayment, Order, Robot
from control.models import AccountingDay
all_payments = LNPayment.objects.all()
@@ -149,11 +149,11 @@ def do_accounting():
else:
outstanding_pending_disputes = 0
- accounted_day.outstanding_earned_rewards = Profile.objects.all().aggregate(
+ accounted_day.outstanding_earned_rewards = Robot.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(
+ accounted_day.lifetime_rewards_claimed = Robot.objects.all().aggregate(
Sum("claimed_rewards")
)["claimed_rewards__sum"]
if accounted_yesterday is not None: