Refactor models into a module (#481)

* Refactor models into a module

* Rename Profile model as Robot

* Underscore numeric literals
This commit is contained in:
Reckless_Satoshi 2023-05-01 10:30:53 +00:00 committed by GitHub
parent 3a49902a7c
commit 75f04579ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 1029 additions and 981 deletions

View File

@ -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",

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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('<img src="%s" width="50" height="50" />' % 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"

8
api/models/__init__.py Normal file
View File

@ -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"]

31
api/models/currency.py Normal file
View File

@ -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"

132
api/models/ln_payment.py Normal file
View File

@ -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)

84
api/models/market_tick.py Normal file
View File

@ -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"

View File

@ -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)

285
api/models/order.py Normal file
View File

@ -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

137
api/models/robot.py Normal file
View File

@ -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('<img src="%s" width="50" height="50" />' % self.get_avatar())

View File

@ -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",

View File

@ -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]

View File

@ -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

View File

@ -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=(

View File

@ -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

View File

@ -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})

View File

@ -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):

View File

@ -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):

View File

@ -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:

View File

@ -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: