mirror of
https://github.com/RoboSats/robosats.git
synced 2025-01-18 12:11:35 +00:00
Refactor models into a module (#481)
* Refactor models into a module * Rename Profile model as Robot * Underscore numeric literals
This commit is contained in:
parent
3a49902a7c
commit
75f04579ed
53
api/admin.py
53
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",
|
||||
|
@ -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
|
||||
|
151
api/logics.py
151
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
|
||||
|
@ -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)
|
||||
|
758
api/models.py
758
api/models.py
@ -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
8
api/models/__init__.py
Normal 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
31
api/models/currency.py
Normal 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
132
api/models/ln_payment.py
Normal 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
84
api/models/market_tick.py
Normal 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"
|
120
api/models/onchain_payment.py
Normal file
120
api/models/onchain_payment.py
Normal 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
285
api/models/order.py
Normal 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
137
api/models/robot.py
Normal 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())
|
@ -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",
|
||||
|
@ -3,6 +3,8 @@ import time
|
||||
|
||||
from .utils import human_format
|
||||
|
||||
|
||||
class NickGenerator:
|
||||
"""
|
||||
Deterministic nick generator from SHA256 hash.
|
||||
|
||||
@ -11,11 +13,8 @@ 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
|
||||
"""
|
||||
28 Trillion deterministic nicks"""
|
||||
|
||||
|
||||
class NickGenerator:
|
||||
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]
|
||||
|
@ -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
|
||||
|
@ -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=(
|
||||
|
38
api/tasks.py
38
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
|
||||
|
54
api/views.py
54
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})
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user