xync-db 0.0.2.dev2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xync_db/__init__.py +26 -0
- xync_db/config.py +17 -0
- xync_db/enums.py +311 -0
- xync_db/exceptions.py +18 -0
- xync_db/graph.py +65 -0
- xync_db/logo.png +0 -0
- xync_db/models/CLAUDE.md +125 -0
- xync_db/models/__init__.py +1781 -0
- xync_db/shared.py +69 -0
- xync_db/typs/__init__.py +40 -0
- xync_db/typs/db/ad.py +76 -0
- xync_db/typs/db/common.py +24 -0
- xync_db/typs/db/order.py +38 -0
- xync_db-0.0.2.dev2.dist-info/METADATA +31 -0
- xync_db-0.0.2.dev2.dist-info/RECORD +16 -0
- xync_db-0.0.2.dev2.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1781 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import logging
|
|
3
|
+
import struct
|
|
4
|
+
import sys
|
|
5
|
+
from time import time
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from decimal import Decimal
|
|
9
|
+
|
|
10
|
+
from cryptography.exceptions import InvalidSignature
|
|
11
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
|
12
|
+
|
|
13
|
+
from tortoise.fields import (
|
|
14
|
+
SmallIntField,
|
|
15
|
+
BigIntField,
|
|
16
|
+
CharField,
|
|
17
|
+
BooleanField,
|
|
18
|
+
IntEnumField,
|
|
19
|
+
FloatField,
|
|
20
|
+
JSONField,
|
|
21
|
+
ForeignKeyField,
|
|
22
|
+
OneToOneField,
|
|
23
|
+
ManyToManyField,
|
|
24
|
+
ForeignKeyRelation,
|
|
25
|
+
OneToOneRelation,
|
|
26
|
+
ForeignKeyNullableRelation,
|
|
27
|
+
OneToOneNullableRelation,
|
|
28
|
+
ManyToManyRelation,
|
|
29
|
+
UUIDField,
|
|
30
|
+
CASCADE,
|
|
31
|
+
BinaryField,
|
|
32
|
+
IntField,
|
|
33
|
+
)
|
|
34
|
+
from tortoise.fields.relational import BackwardFKRelation, BackwardOneToOneRelation
|
|
35
|
+
from tortoise.functions import Sum
|
|
36
|
+
from tortoise import Model as BaseModel, BaseDBAsyncClient, connections
|
|
37
|
+
from tortoise.signals import pre_save
|
|
38
|
+
from tortoise.timezone import now
|
|
39
|
+
|
|
40
|
+
# noinspection PyUnresolvedReferences
|
|
41
|
+
from x_auth.models import (
|
|
42
|
+
Model,
|
|
43
|
+
Username as Username,
|
|
44
|
+
User as TgUser,
|
|
45
|
+
Proxy as Proxy,
|
|
46
|
+
Dc as Dc,
|
|
47
|
+
Fcm as Fcm,
|
|
48
|
+
App as App,
|
|
49
|
+
Session as Session,
|
|
50
|
+
Peer as Peer,
|
|
51
|
+
UpdateState as UpdateState,
|
|
52
|
+
Version as Version,
|
|
53
|
+
Country as BaseCountry,
|
|
54
|
+
)
|
|
55
|
+
from x_model.field import UInt1Field, UInt2Field, UInt4Field, UInt8Field, UniqBinaryField, DatetimeSecField
|
|
56
|
+
from x_model.models import TsTrait
|
|
57
|
+
|
|
58
|
+
from xync_db.enums import (
|
|
59
|
+
ExType,
|
|
60
|
+
AdStatus,
|
|
61
|
+
OrderStatus,
|
|
62
|
+
ExAction,
|
|
63
|
+
ExStatus,
|
|
64
|
+
PersonStatus,
|
|
65
|
+
UserStatus,
|
|
66
|
+
PmType,
|
|
67
|
+
FileType,
|
|
68
|
+
AddrExType,
|
|
69
|
+
DepType,
|
|
70
|
+
Party,
|
|
71
|
+
Slip,
|
|
72
|
+
SynonymType,
|
|
73
|
+
AbuserType,
|
|
74
|
+
Boundary,
|
|
75
|
+
SbpStrict,
|
|
76
|
+
HotStatus,
|
|
77
|
+
TaskType,
|
|
78
|
+
TransactionStatus as TS,
|
|
79
|
+
OrderErr,
|
|
80
|
+
LinkType,
|
|
81
|
+
CoinType,
|
|
82
|
+
)
|
|
83
|
+
from xync_db.exceptions import RaiseError, PgRaiseError
|
|
84
|
+
from xync_db.shared import qr_gen, typ_ts_pack
|
|
85
|
+
from xync_db.typs import OrderResult
|
|
86
|
+
from xync_db.typs.db import AdNewFlatRaw
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class Country(BaseCountry):
|
|
90
|
+
cur: ForeignKeyRelation[Cur] = ForeignKeyField("models.Cur", "countries", on_update=CASCADE)
|
|
91
|
+
curexs: ManyToManyRelation[CurEx]
|
|
92
|
+
forbidden_exs: ManyToManyRelation[Ex]
|
|
93
|
+
fiats: BackwardFKRelation[Fiat]
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class Cur(Model):
|
|
97
|
+
id = UInt1Field(True)
|
|
98
|
+
ticker: str = CharField(3, unique=True)
|
|
99
|
+
scale: int = UInt1Field(default=2)
|
|
100
|
+
|
|
101
|
+
pms: ManyToManyRelation[Pm] = ManyToManyField("models.Pm", "pmcur", on_update=CASCADE)
|
|
102
|
+
exs: ManyToManyRelation[Ex] = ManyToManyField("models.Ex", "curex", on_update=CASCADE)
|
|
103
|
+
pairs: BackwardFKRelation[Pair]
|
|
104
|
+
countries: BackwardFKRelation[Country]
|
|
105
|
+
synonyms: ManyToManyRelation[Synonym]
|
|
106
|
+
|
|
107
|
+
_name = {"ticker"}
|
|
108
|
+
|
|
109
|
+
class Meta:
|
|
110
|
+
table_description = "Fiat currencies"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Coin(Model):
|
|
114
|
+
id: int = SmallIntField(True)
|
|
115
|
+
ticker: str = CharField(15, unique=True)
|
|
116
|
+
rate: float | None = FloatField(default=0)
|
|
117
|
+
is_fiat: bool = BooleanField(default=False)
|
|
118
|
+
scale: int = UInt1Field()
|
|
119
|
+
typ: CoinType = IntEnumField(CoinType, default=CoinType.normal)
|
|
120
|
+
exs: ManyToManyRelation[Ex] = ManyToManyField("models.Ex", "coinex", on_update=CASCADE)
|
|
121
|
+
|
|
122
|
+
assets: BackwardFKRelation[Asset]
|
|
123
|
+
nets: BackwardFKRelation[Net]
|
|
124
|
+
pairs: BackwardFKRelation[Pair]
|
|
125
|
+
# deps: BackwardFKRelation[Dep]
|
|
126
|
+
# deps_reward: BackwardFKRelation[Dep]
|
|
127
|
+
# deps_bonus: ReverseRelation[Dep]
|
|
128
|
+
|
|
129
|
+
_name = {"ticker"}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class Net(Model):
|
|
133
|
+
id: int = UInt1Field(True)
|
|
134
|
+
name: str = CharField(63)
|
|
135
|
+
native_coin: ForeignKeyRelation[Coin] = ForeignKeyField("models.Coin", "nets", on_update=CASCADE)
|
|
136
|
+
native_coin_id: int
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Ex(Model):
|
|
140
|
+
id: int = UInt1Field(True)
|
|
141
|
+
name: str = CharField(31)
|
|
142
|
+
host: str | None = CharField(63, null=True, description="With no protocol 'https://'")
|
|
143
|
+
host_p2p: str | None = CharField(63, null=True, description="With no protocol 'https://'")
|
|
144
|
+
url_login: str | None = CharField(63, null=True, description="With no protocol 'https://'")
|
|
145
|
+
typ: ExType = IntEnumField(ExType)
|
|
146
|
+
status: ExStatus = IntEnumField(ExStatus, default=ExStatus.plan)
|
|
147
|
+
logo: str = CharField(511, default="")
|
|
148
|
+
|
|
149
|
+
ads: ManyToManyRelation[Ad]
|
|
150
|
+
pms: ManyToManyRelation[Pm]
|
|
151
|
+
curs: ManyToManyRelation[Cur]
|
|
152
|
+
# pmcurs: ManyToManyRelation[PmCur] = ManyToManyField("models.PmCur", through="pmcurex")
|
|
153
|
+
coins: ManyToManyRelation[Coin]
|
|
154
|
+
forbidden_countries: ManyToManyRelation[Country] = ManyToManyField(
|
|
155
|
+
"models.Country", related_name="forbidden_exs", on_update=CASCADE
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
actors: BackwardFKRelation[Actor]
|
|
159
|
+
pmexs: BackwardFKRelation[PmEx]
|
|
160
|
+
pm_reps: BackwardFKRelation[PmRep]
|
|
161
|
+
pairexs: BackwardFKRelation[PairEx]
|
|
162
|
+
deps: BackwardFKRelation[Dep]
|
|
163
|
+
stats: BackwardFKRelation[ExStat]
|
|
164
|
+
|
|
165
|
+
class Meta:
|
|
166
|
+
table_description = "Exchanges"
|
|
167
|
+
unique_together = (("name", "typ"),)
|
|
168
|
+
|
|
169
|
+
class PydanticMeta(Model.PydanticMeta):
|
|
170
|
+
include = "name", "logo"
|
|
171
|
+
|
|
172
|
+
def client(self, **kwargs):
|
|
173
|
+
module_name = f"xync_client.{self.name}.ex"
|
|
174
|
+
__import__(module_name)
|
|
175
|
+
client = sys.modules[module_name].ExClient
|
|
176
|
+
return client(self, **kwargs)
|
|
177
|
+
|
|
178
|
+
def etype(self):
|
|
179
|
+
module_name = f"xync_client.{self.name}.etype"
|
|
180
|
+
__import__(module_name)
|
|
181
|
+
return sys.modules[module_name]
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class CurEx(BaseModel):
|
|
185
|
+
cur: ForeignKeyRelation[Cur] = ForeignKeyField("models.Cur", on_update=CASCADE)
|
|
186
|
+
cur_id: int
|
|
187
|
+
ex: ForeignKeyRelation[Ex] = ForeignKeyField("models.Ex", on_update=CASCADE)
|
|
188
|
+
ex_id: int
|
|
189
|
+
exid: str = CharField(32)
|
|
190
|
+
minimum: int = UInt4Field(null=True) # /10^cur.scale
|
|
191
|
+
scale: int = UInt1Field(null=True)
|
|
192
|
+
|
|
193
|
+
# countries: ManyToManyRelation[Country] = ManyToManyField (
|
|
194
|
+
# "models.Country", through="curex_country", backward_key="curexs"
|
|
195
|
+
# )
|
|
196
|
+
|
|
197
|
+
class Meta:
|
|
198
|
+
table_description = "Currency in Exchange"
|
|
199
|
+
unique_together = (("ex_id", "cur_id"), ("ex_id", "exid"))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class CoinEx(BaseModel):
|
|
203
|
+
coin: ForeignKeyRelation[Coin] = ForeignKeyField("models.Coin", on_update=CASCADE)
|
|
204
|
+
coin_id: int
|
|
205
|
+
ex: ForeignKeyRelation[Ex] = ForeignKeyField("models.Ex", on_update=CASCADE)
|
|
206
|
+
ex_id: int
|
|
207
|
+
minimum: int = BigIntField(null=True) # /10^self.scale
|
|
208
|
+
scale: int = UInt1Field()
|
|
209
|
+
|
|
210
|
+
exid: str = CharField(32)
|
|
211
|
+
p2p: bool = BooleanField(default=True)
|
|
212
|
+
|
|
213
|
+
class Meta:
|
|
214
|
+
table_description = "Currency in Exchange"
|
|
215
|
+
unique_together = (("ex_id", "coin_id"), ("ex_id", "exid"))
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
class Pair(Model):
|
|
219
|
+
id = SmallIntField(True)
|
|
220
|
+
coin: ForeignKeyRelation[Coin] = ForeignKeyField("models.Coin", "pairs", on_update=CASCADE)
|
|
221
|
+
cur: ForeignKeyRelation[Cur] = ForeignKeyField("models.Cur", "pairs", on_update=CASCADE)
|
|
222
|
+
rate: int | None = BigIntField(null=True)
|
|
223
|
+
|
|
224
|
+
_name = {"coin__ticker", "cur__ticker"}
|
|
225
|
+
|
|
226
|
+
class Meta:
|
|
227
|
+
table_description = "Coin/Currency pairs"
|
|
228
|
+
unique_together = (("coin_id", "cur_id"),)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
class PairSide(Model): # Way
|
|
232
|
+
id = SmallIntField(True)
|
|
233
|
+
pair: ForeignKeyRelation[Pair] = ForeignKeyField("models.Pair", "pair_sides", on_update=CASCADE)
|
|
234
|
+
pair_id: int
|
|
235
|
+
is_sell: bool = BooleanField()
|
|
236
|
+
rate: int | None = BigIntField(null=True)
|
|
237
|
+
|
|
238
|
+
class Meta:
|
|
239
|
+
table = "pair_side"
|
|
240
|
+
unique_together = (("pair_id", "is_sell"),)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class NetAddr(Model): # Way
|
|
244
|
+
net: ForeignKeyRelation[Net] = ForeignKeyField("models.Net", "net_addrs", on_update=CASCADE)
|
|
245
|
+
net_id: int
|
|
246
|
+
addr: ForeignKeyRelation[Addr] = ForeignKeyField("models.Addr", "net_addrs", on_update=CASCADE)
|
|
247
|
+
addr_id: int
|
|
248
|
+
val: str = CharField(200)
|
|
249
|
+
memo: str | None = CharField(54, null=True)
|
|
250
|
+
qr: str | None = CharField(255, null=True)
|
|
251
|
+
|
|
252
|
+
class Meta:
|
|
253
|
+
table = "net_addr"
|
|
254
|
+
unique_together = (("net_id", "addr_id"),)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
class PairEx(Model, TsTrait): # todo: refact to PairSideEx?
|
|
258
|
+
# todo: различаются ли комиссии на buy/sell по одной паре хоть на одной бирже? если да, то переделать
|
|
259
|
+
pair: ForeignKeyRelation[Pair] = ForeignKeyField("models.Pair", "pairexs", on_update=CASCADE)
|
|
260
|
+
pair_id: int
|
|
261
|
+
fee: int = SmallIntField(default=0) # /10_000
|
|
262
|
+
ex: ForeignKeyRelation[Ex] = ForeignKeyField("models.Ex", "pairs", on_update=CASCADE)
|
|
263
|
+
ex_id: int
|
|
264
|
+
pair_sides: BackwardFKRelation[PairSide]
|
|
265
|
+
|
|
266
|
+
_name = {"pair__coin__ticker", "pair__cur__ticker", "ex__name"}
|
|
267
|
+
|
|
268
|
+
class Meta:
|
|
269
|
+
table_description = "Pairs on Exs"
|
|
270
|
+
unique_together = (("pair_id", "ex_id"),)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
class Person(Model, TsTrait):
|
|
274
|
+
status: PersonStatus = IntEnumField(PersonStatus, default=PersonStatus.DEFAULT)
|
|
275
|
+
name: str | None = CharField(127, null=True)
|
|
276
|
+
note: bool = CharField(255, null=True)
|
|
277
|
+
# name+user(where user is not null)=unique in raw migration!
|
|
278
|
+
user: ForeignKeyRelation[User] = ForeignKeyField("models.User", "persons", on_update=CASCADE, null=True)
|
|
279
|
+
user_id: int
|
|
280
|
+
creds: BackwardFKRelation[Cred]
|
|
281
|
+
actors: BackwardFKRelation[Actor]
|
|
282
|
+
pm_agents: BackwardFKRelation[PmAgent]
|
|
283
|
+
|
|
284
|
+
async def merge(self, new_person: Person):
|
|
285
|
+
"""Вмерджить new_person в self: перенести акторов и креды, удалить new_person."""
|
|
286
|
+
await new_person.fetch_related("creds", "actors")
|
|
287
|
+
for cred in new_person.creds:
|
|
288
|
+
cred.person = self
|
|
289
|
+
await cred.save(update_fields=["person_id"])
|
|
290
|
+
for actor in new_person.actors:
|
|
291
|
+
actor.person = self
|
|
292
|
+
await actor.save(update_fields=["person_id"])
|
|
293
|
+
self.name = self.name or new_person.name
|
|
294
|
+
await self.save(update_fields=["name"])
|
|
295
|
+
# после переноса у new_person не должно остаться связей
|
|
296
|
+
await new_person.fetch_related("creds", "actors")
|
|
297
|
+
if not len(new_person.creds) + len(new_person.actors) and new_person.user_id in (self.user_id, None):
|
|
298
|
+
await new_person.delete()
|
|
299
|
+
else:
|
|
300
|
+
raise ValueError(
|
|
301
|
+
f"Merge to person#{self.id} failed: new person#{new_person.id} still has "
|
|
302
|
+
f"{len(new_person.creds)} creds, {len(new_person.actors)} actors, user_id={new_person.user_id}"
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class User(TgUser, TsTrait):
|
|
307
|
+
status: UserStatus = IntEnumField(UserStatus, null=True)
|
|
308
|
+
ref: ForeignKeyNullableRelation[User] = ForeignKeyField("models.User", "proteges", on_update=CASCADE, null=True)
|
|
309
|
+
ref_id: int | None
|
|
310
|
+
bonus: int = UInt2Field(default=0)
|
|
311
|
+
tz: int = SmallIntField(default=3)
|
|
312
|
+
prv: bytes = UniqBinaryField(unique=True, null=True) # len=32
|
|
313
|
+
pub: bytes = UniqBinaryField(unique=True, null=True) # len=32
|
|
314
|
+
|
|
315
|
+
actors: BackwardFKRelation[Actor]
|
|
316
|
+
borrows: BackwardFKRelation[Credit]
|
|
317
|
+
contacts: BackwardFKRelation[User]
|
|
318
|
+
created_forums: BackwardFKRelation[Forum]
|
|
319
|
+
creds: BackwardFKRelation[Cred]
|
|
320
|
+
gmail: BackwardOneToOneRelation[Gmail]
|
|
321
|
+
forum: BackwardOneToOneRelation[Forum]
|
|
322
|
+
hots: BackwardFKRelation[Hot]
|
|
323
|
+
investments: BackwardFKRelation[Investment]
|
|
324
|
+
invite_requests: BackwardFKRelation[Invite]
|
|
325
|
+
invite_approvals: BackwardFKRelation[Invite]
|
|
326
|
+
lends: BackwardFKRelation[Credit]
|
|
327
|
+
limits: BackwardFKRelation[Limit]
|
|
328
|
+
msgs: BackwardFKRelation[Msg]
|
|
329
|
+
persons: BackwardFKRelation[Person]
|
|
330
|
+
pm_agents: BackwardFKRelation[PmAgent]
|
|
331
|
+
proteges: BackwardFKRelation[User]
|
|
332
|
+
sends: BackwardFKRelation[Transaction]
|
|
333
|
+
receives: BackwardFKRelation[Transaction]
|
|
334
|
+
validates: BackwardFKRelation[Transaction]
|
|
335
|
+
vpn: BackwardOneToOneRelation[Vpn]
|
|
336
|
+
hot_ads: ManyToManyRelation[MyAd]
|
|
337
|
+
|
|
338
|
+
# def __init__(self, **kwargs: Any) -> None:
|
|
339
|
+
# super().__init__(**kwargs)
|
|
340
|
+
# self.pm_clients = {pma.pm_id: pma.client() for pma in self.pm_agents}
|
|
341
|
+
# self.agent_clients = {a.id: a.client() for a in self.person.actor.agents}
|
|
342
|
+
|
|
343
|
+
async def free_assets(self):
|
|
344
|
+
assets = await Asset.filter(agent__actor__person__user__id=self.id).values("free", "addr__coin__rate")
|
|
345
|
+
return sum(asset["free"] * asset["addr__coin__rate"] for asset in assets)
|
|
346
|
+
|
|
347
|
+
async def fiats_sum(self):
|
|
348
|
+
fiats = await Fiat.filter(cred__person__user__id=self.id).values("amount", "cred__pmcur__cur__rate")
|
|
349
|
+
return sum(fiat["amount"] * Decimal(fiat["cred__pmcur__cur__rate"]) for fiat in fiats)
|
|
350
|
+
|
|
351
|
+
async def balance_sum(self) -> int:
|
|
352
|
+
return int(await self.free_assets()) + int(await self.fiats_sum())
|
|
353
|
+
|
|
354
|
+
async def balances(self, ctd: bool = True) -> dict[int, int | float]: # ctd - convert to decimal
|
|
355
|
+
scales: dict[int, int] = {1: 2, 2: 2, 3: 2}
|
|
356
|
+
|
|
357
|
+
def grab(rows):
|
|
358
|
+
scales.update({c: s for c, _, s in rows})
|
|
359
|
+
return {c: v for c, v, _ in rows}
|
|
360
|
+
|
|
361
|
+
dbt = {1: 0, 2: 0, 3: 0} | grab(
|
|
362
|
+
await Transaction.filter(receiver=self, status=TS.verified)
|
|
363
|
+
.group_by("cur_id", "cur__scale")
|
|
364
|
+
.annotate(debit=Sum("amount"))
|
|
365
|
+
.values_list("cur_id", "debit", "cur__scale")
|
|
366
|
+
)
|
|
367
|
+
crd = grab(
|
|
368
|
+
await Transaction.filter(sender=self, status__gte=TS.signed)
|
|
369
|
+
.group_by("cur_id", "cur__scale")
|
|
370
|
+
.annotate(credit=Sum("amount"))
|
|
371
|
+
.values_list("cur_id", "credit", "cur__scale")
|
|
372
|
+
)
|
|
373
|
+
top = grab(
|
|
374
|
+
await TopUp.filter(user=self, completed_at__isnull=False)
|
|
375
|
+
.group_by("cur_id", "cur__scale")
|
|
376
|
+
.annotate(topup=Sum("amount"))
|
|
377
|
+
.values_list("cur_id", "topup", "cur__scale")
|
|
378
|
+
)
|
|
379
|
+
raw = {c: dbt.get(c, 0) - crd.get(c, 0) + top.get(c, 0) for c in dbt.keys() | crd.keys() | top.keys()}
|
|
380
|
+
return {c: round(v * 10 ** -scales[c], scales[c]) for c, v in raw.items()} if ctd else raw
|
|
381
|
+
|
|
382
|
+
async def balance(self, cur_id: int, ts: int = None) -> int:
|
|
383
|
+
flt = dict(cur_id=cur_id)
|
|
384
|
+
if ts:
|
|
385
|
+
flt["ts__lte"] = ts
|
|
386
|
+
dbt = (
|
|
387
|
+
await Transaction.filter(receiver=self, status=TS.verified, **flt)
|
|
388
|
+
.group_by("cur_id")
|
|
389
|
+
.annotate(debit=Sum("amount"))
|
|
390
|
+
.values_list("debit", flat=True)
|
|
391
|
+
) or [0]
|
|
392
|
+
crd = (
|
|
393
|
+
await Transaction.filter(sender_id=self.id or None, status__in=[TS.signed, TS.verified], **flt)
|
|
394
|
+
.group_by("cur_id")
|
|
395
|
+
.annotate(credit=Sum("amount"))
|
|
396
|
+
.values_list("credit", flat=True)
|
|
397
|
+
) or [0]
|
|
398
|
+
return dbt[0] - crd[0]
|
|
399
|
+
|
|
400
|
+
def keygen(self): # moved to front
|
|
401
|
+
prv = Ed25519PrivateKey.generate()
|
|
402
|
+
self.prv = prv.private_bytes_raw()
|
|
403
|
+
self.pub = prv.public_key().public_bytes_raw()
|
|
404
|
+
|
|
405
|
+
async def get_validator(self, amount: int, cur_id: int, receiver_id: int) -> "User":
|
|
406
|
+
r = {
|
|
407
|
+
c: v
|
|
408
|
+
for c, v in await Transaction.filter( # todo: rm ` or receiver_id` - is hack for first trans
|
|
409
|
+
receiver_id__not_in={receiver_id, self.id or receiver_id}, status=TS.verified, cur_id=cur_id
|
|
410
|
+
)
|
|
411
|
+
.group_by("receiver_id")
|
|
412
|
+
.annotate(debit=Sum("amount"))
|
|
413
|
+
.values_list("receiver_id", "debit")
|
|
414
|
+
}
|
|
415
|
+
s = {
|
|
416
|
+
c: v
|
|
417
|
+
for c, v in await Transaction.filter(
|
|
418
|
+
sender_id__not_in={self.id, receiver_id}, status=TS.verified, cur_id=cur_id
|
|
419
|
+
)
|
|
420
|
+
.group_by("sender_id")
|
|
421
|
+
.annotate(credit=Sum("amount"))
|
|
422
|
+
.values_list("sender_id", "credit")
|
|
423
|
+
if c is not None # todo: rm `if c is not None` - is hack
|
|
424
|
+
}
|
|
425
|
+
val_ids = r.keys() | s.keys()
|
|
426
|
+
# noinspection PyUnboundLocalVariable
|
|
427
|
+
vals = {c: am for c in val_ids if c is not None and (am := r.get(c, 0) - s.get(c, 0)) > amount}
|
|
428
|
+
val_id, _ = sorted(vals.items(), key=lambda x: x[1], reverse=True)[0]
|
|
429
|
+
if val_id is None:
|
|
430
|
+
raise ValueError("No available validators:(")
|
|
431
|
+
validator = await User[val_id]
|
|
432
|
+
return validator
|
|
433
|
+
|
|
434
|
+
async def req(self, amount: int, cur_id: int, sender_id: int = None, ts: int = None) -> Transaction:
|
|
435
|
+
return await Transaction.create(
|
|
436
|
+
receiver=self,
|
|
437
|
+
sender_id=sender_id,
|
|
438
|
+
amount=amount,
|
|
439
|
+
cur_id=cur_id,
|
|
440
|
+
status=TS.request,
|
|
441
|
+
ts=ts,
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
async def send(
|
|
445
|
+
self, cur_id: int, rcv_id: int, amount: int, ts: int = None, sign: bytes = None
|
|
446
|
+
) -> Transaction | int:
|
|
447
|
+
assert sign or self.prv, "sign or user.prv is required"
|
|
448
|
+
# sender check balance himself befor send
|
|
449
|
+
if (b := await self.balance(cur_id)) < amount and self.id:
|
|
450
|
+
return b
|
|
451
|
+
# creating tx
|
|
452
|
+
tx = Transaction(
|
|
453
|
+
ts=ts or int(time()),
|
|
454
|
+
cur_id=cur_id,
|
|
455
|
+
receiver_id=rcv_id,
|
|
456
|
+
amount=amount,
|
|
457
|
+
sender=self,
|
|
458
|
+
proof=sign,
|
|
459
|
+
# status=TS.signed,
|
|
460
|
+
)
|
|
461
|
+
tx.id = UUID(bytes=tx.pack)
|
|
462
|
+
return await tx.sign(sign)
|
|
463
|
+
|
|
464
|
+
# доступные hot объявления для юзера с его текущими балансами
|
|
465
|
+
async def get_hot_ads(self, ex_ids: list[int] = None) -> tuple[list[MyAd], list[MyAd], dict[int, float]]:
|
|
466
|
+
adq = MyAd.hot_mads_query(ex_ids).filter(ad__maker__person__user_id__not=self.id)
|
|
467
|
+
bads = await adq.filter(ad__pair_side__is_sell=False)
|
|
468
|
+
|
|
469
|
+
sads = []
|
|
470
|
+
if balances := await self.balances():
|
|
471
|
+
curexs = await CurEx.filter(cur_id__in=balances.keys(), ex_id__in=[4]) # todo: fix hardcode cur mins
|
|
472
|
+
minimums = {cx.cur_id: cx.minimum for cx in curexs}
|
|
473
|
+
balances = {cur_id: b for cur_id, b in balances.items() if minimums.get(cur_id) and b >= minimums[cur_id]}
|
|
474
|
+
sads = await adq.filter(ad__pair_side__is_sell=True, ad__pair_side__pair__cur_id__in=balances.keys()).all()
|
|
475
|
+
|
|
476
|
+
return bads, sads, balances
|
|
477
|
+
|
|
478
|
+
async def get_last_hot(self, force_create: bool = False) -> Hot:
|
|
479
|
+
if not force_create and (
|
|
480
|
+
hot := await Hot.filter(status=HotStatus.opened, user=self).order_by("-updated_at").first()
|
|
481
|
+
):
|
|
482
|
+
await hot.update_from_dict({"updated_at": now()}).save()
|
|
483
|
+
return hot
|
|
484
|
+
return await Hot.create(user=self)
|
|
485
|
+
|
|
486
|
+
def name(self):
|
|
487
|
+
return f"{self.first_name} {self.last_name}".rstrip()
|
|
488
|
+
|
|
489
|
+
class PydanticMeta(Model.PydanticMeta):
|
|
490
|
+
max_recursion = 0
|
|
491
|
+
include = "role", "status"
|
|
492
|
+
# computed = ["balance"]
|
|
493
|
+
|
|
494
|
+
def qr_invite(self, bot_nick: str) -> bytes:
|
|
495
|
+
data = f"{bot_nick and 't.me/' + bot_nick + '?startapp='}{int.from_bytes(self.id.bytes + self.proof)}"
|
|
496
|
+
# Prepare text
|
|
497
|
+
typ = ("Personalized" if self.sender_id else "Opened") if self.status == TS.request else "Receipt"
|
|
498
|
+
text = f"{typ} {self.status.name}"
|
|
499
|
+
return qr_gen(data, text)
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
@pre_save(User) # keygen for custodian users
|
|
503
|
+
async def keys(_cls: type[User], user: User, _db: BaseDBAsyncClient, _updated: list[str]) -> None:
|
|
504
|
+
if not user.pub:
|
|
505
|
+
user.keygen()
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class Gmail(Model):
|
|
509
|
+
login: str = CharField(127)
|
|
510
|
+
auth: dict = JSONField(default={})
|
|
511
|
+
token: bytes = BinaryField(null=True)
|
|
512
|
+
updated_at: datetime | None = DatetimeSecField(auto_now=True)
|
|
513
|
+
|
|
514
|
+
user: OneToOneRelation[User] = OneToOneField("models.User", "gmail", on_update=CASCADE)
|
|
515
|
+
|
|
516
|
+
|
|
517
|
+
class Forum(Model, TsTrait):
|
|
518
|
+
id: int = BigIntField(True)
|
|
519
|
+
joined: bool = BooleanField(default=False)
|
|
520
|
+
user: OneToOneRelation[User] = OneToOneField("models.User", "forum", on_update=CASCADE)
|
|
521
|
+
user_id: int
|
|
522
|
+
# created_by: BackwardFKRelation[User] = ForeignKeyField("models.User", "created_forums")
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
class Actor(Model):
|
|
526
|
+
exid: int = UInt8Field()
|
|
527
|
+
name: str = CharField(63)
|
|
528
|
+
ex: ForeignKeyRelation[Ex] = ForeignKeyField("models.Ex", "actors", on_update=CASCADE)
|
|
529
|
+
ex_id: int
|
|
530
|
+
person: ForeignKeyRelation[Person] = ForeignKeyField("models.Person", "actors", on_update=CASCADE)
|
|
531
|
+
person_id: int
|
|
532
|
+
|
|
533
|
+
agent: BackwardOneToOneRelation[Agent]
|
|
534
|
+
conds: BackwardFKRelation[Cond]
|
|
535
|
+
my_ads: BackwardFKRelation[Ad]
|
|
536
|
+
taken_orders: BackwardFKRelation[Order]
|
|
537
|
+
|
|
538
|
+
hots: ManyToManyRelation[Hot]
|
|
539
|
+
|
|
540
|
+
class Meta:
|
|
541
|
+
table_description = "Actors"
|
|
542
|
+
unique_together = (("ex_id", "exid"),)
|
|
543
|
+
|
|
544
|
+
async def get_credexs_by(self, pm_ids: list[int], cur_id: int) -> list[CredEx]:
|
|
545
|
+
return await CredEx.filter(
|
|
546
|
+
ex_id=self.ex_id,
|
|
547
|
+
cred__pmcur__pm_id__in=pm_ids,
|
|
548
|
+
cred__person_id=self.person_id,
|
|
549
|
+
cred__pmcur__cur_id=cur_id,
|
|
550
|
+
)
|
|
551
|
+
|
|
552
|
+
async def set_user(self, username_id: int) -> tuple[int, str, User]:
|
|
553
|
+
"""Назначить актору персону юзера с username_id.
|
|
554
|
+
|
|
555
|
+
Логика мерджа персон:
|
|
556
|
+
- Если персона юзера все еще без акторов (базовая от триггера) — мерджим в неё.
|
|
557
|
+
- Если все персоны юзера заняты акторами, но имя совпадает — мерджим в ту персону.
|
|
558
|
+
- Если имена не совпадают — оставляем новую персону, и привязываем к юзеру.
|
|
559
|
+
"""
|
|
560
|
+
assert isinstance(self.person, Person), "person should be fetched"
|
|
561
|
+
err, txt = 0, ""
|
|
562
|
+
|
|
563
|
+
user_persons: list[Person] = (
|
|
564
|
+
await Person.filter(user__username_id=username_id).prefetch_related("user", "actors").all()
|
|
565
|
+
)
|
|
566
|
+
if not user_persons:
|
|
567
|
+
raise ValueError(f"Не найдено персон юзера:{username_id}! Персона создаётся триггером при создании User")
|
|
568
|
+
user = user_persons[0].user
|
|
569
|
+
|
|
570
|
+
# персоне этого актора уже назначен юзер
|
|
571
|
+
if self.person.user_id:
|
|
572
|
+
if (await self.person.user).username_id == username_id:
|
|
573
|
+
txt = f"У персоны актора:{self.id} уже назначен этот юзер"
|
|
574
|
+
logging.warning(txt)
|
|
575
|
+
return 1, txt, user
|
|
576
|
+
txt = f"У персоны актора:{self.id} уже назначен другой юзер"
|
|
577
|
+
logging.error(txt)
|
|
578
|
+
return 3, txt, user
|
|
579
|
+
|
|
580
|
+
merged = False
|
|
581
|
+
for i, person in enumerate(user_persons):
|
|
582
|
+
if [a for a in person.actors if a.ex_id]:
|
|
583
|
+
# персона юзера уже занята актором(ами) — мерджим только если имена совпадают
|
|
584
|
+
if self.person.name == person.name:
|
|
585
|
+
await person.merge(self.person)
|
|
586
|
+
merged = True
|
|
587
|
+
elif i:
|
|
588
|
+
# у юзера >1 персоны без акторов — аномалия
|
|
589
|
+
txt = (
|
|
590
|
+
f"У {i + 1}-й персоны #{person.id} нет акторов (юзер #{username_id}), но персон без акторов уже > 1"
|
|
591
|
+
)
|
|
592
|
+
logging.error(txt)
|
|
593
|
+
return 2, txt, user
|
|
594
|
+
else:
|
|
595
|
+
# первая персона юзера без акторов (базовая от триггера) — мерджим
|
|
596
|
+
await person.merge(self.person)
|
|
597
|
+
merged = True
|
|
598
|
+
|
|
599
|
+
if not merged:
|
|
600
|
+
# все персоны юзера заняты акторами с другими именами — оставляем новую, привязываем к юзеру
|
|
601
|
+
self.person.user = user
|
|
602
|
+
await self.person.save(update_fields=["user_id"])
|
|
603
|
+
|
|
604
|
+
txt = f"Юзер#{username_id} подтвердил что Actor#{self.id} его"
|
|
605
|
+
logging.info(txt)
|
|
606
|
+
return err, txt, user
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
class Agent(Model, TsTrait):
|
|
610
|
+
auth: dict = JSONField(default={})
|
|
611
|
+
actor: OneToOneRelation[Actor] = OneToOneField("models.Actor", "agent", on_update=CASCADE)
|
|
612
|
+
actor_id: int
|
|
613
|
+
expire_at: int | None = IntField(null=True)
|
|
614
|
+
status: int = SmallIntField(default=0)
|
|
615
|
+
same_dir_ad: int = UInt1Field(default=0)
|
|
616
|
+
|
|
617
|
+
assets: BackwardFKRelation[Asset]
|
|
618
|
+
|
|
619
|
+
_name = {"actor__name"}
|
|
620
|
+
|
|
621
|
+
# def balance(self) -> int:
|
|
622
|
+
# return sum(asset.free * (asset.coin.rate or 0) for asset in self.assets)
|
|
623
|
+
|
|
624
|
+
# class PydanticMeta(Model.PydanticMeta):
|
|
625
|
+
# max_recursion = 3
|
|
626
|
+
# include = "id", "actor__ex", "auth", "updated_at"
|
|
627
|
+
# computed = ["balance"]
|
|
628
|
+
|
|
629
|
+
def client(self, ex_cl, pm_clients=None, proxy=None):
|
|
630
|
+
module_name = f"xync_client.{self.actor.ex.name}.agent"
|
|
631
|
+
__import__(module_name)
|
|
632
|
+
client = sys.modules[module_name].AgentClient
|
|
633
|
+
return client(
|
|
634
|
+
self,
|
|
635
|
+
ex_cl,
|
|
636
|
+
pm_clients or {},
|
|
637
|
+
headers=self.auth.get("headers"),
|
|
638
|
+
cookies=self.auth.get("cookies"),
|
|
639
|
+
proxy=proxy,
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
async def coins_balance(self, to_float: bool = True):
|
|
643
|
+
await self.fetch_related("assets__addr")
|
|
644
|
+
scales = dict(to_float and await CoinEx.filter(ex_id=self.actor.ex_id).values_list("coin_id", "scale") or ())
|
|
645
|
+
return {a.addr.coin_id: round(a.free * 10 ** -(s := scales.get(a.addr.coin_id, 0)), s) for a in self.assets if a.free}
|
|
646
|
+
|
|
647
|
+
@classmethod
|
|
648
|
+
async def upsert(
|
|
649
|
+
cls, exid: int, host: str, auth: dict, exp: int, rname: str, nick: str, email: str
|
|
650
|
+
) -> tuple[int, Agent]:
|
|
651
|
+
err = 0
|
|
652
|
+
if host and (ex := await Ex.get_or_none(host=host)):
|
|
653
|
+
if not (actor := await Actor.get_or_none(exid=exid, ex=ex)): # такого актора еще нет
|
|
654
|
+
mail_tpl = email.replace("*", "_")
|
|
655
|
+
if person := await Person.get_or_none(name=rname):
|
|
656
|
+
err = 1 # уточнить что person c таким именем действительно тот
|
|
657
|
+
# если нашелся (и не больше 1) юзер по gmail-у
|
|
658
|
+
elif (gml := await Gmail.raw(f"SELECT * FROM gmail WHERE login LIKE '{mail_tpl}'")) and len(gml) == 1:
|
|
659
|
+
err = 2 # уточнить что почта правильная
|
|
660
|
+
if persons := await Person.filter(user_id=gml[0].user_id).all():
|
|
661
|
+
if len(persons) > 1:
|
|
662
|
+
err = 3 # уточнить какой именно персон нужен этому актору
|
|
663
|
+
person = persons[0]
|
|
664
|
+
if person.name != rname:
|
|
665
|
+
person.name = rname
|
|
666
|
+
await person.save(update_fields=["name"])
|
|
667
|
+
else:
|
|
668
|
+
raise ValueError(f"Это как так почта {gml[0]} с юзером есть, а персоны нет!?")
|
|
669
|
+
else: # если нет, то создаем пустой персон со статусом "определить юзера"
|
|
670
|
+
person = await Person.create(name=rname, status=PersonStatus.USER_DEF)
|
|
671
|
+
actor = await Actor.create(ex=ex, exid=exid, name=nick, person=person)
|
|
672
|
+
if ag := await Agent.get_or_none(actor=actor):
|
|
673
|
+
ag.auth |= auth
|
|
674
|
+
ag.expire_at = exp
|
|
675
|
+
await ag.save(update_fields=["auth", "expire_at"])
|
|
676
|
+
else:
|
|
677
|
+
ag = await Agent.create(auth=auth, actor=actor, expire_at=exp)
|
|
678
|
+
return err, ag
|
|
679
|
+
raise ValueError("Wrong ex: " + host)
|
|
680
|
+
|
|
681
|
+
|
|
682
|
+
class Cond(Model, TsTrait):
|
|
683
|
+
raw_txt: str = CharField(4095, unique=True)
|
|
684
|
+
last_ver: str = CharField(4095, null=True)
|
|
685
|
+
|
|
686
|
+
ads: BackwardFKRelation[Ad]
|
|
687
|
+
parsed: BackwardOneToOneRelation[CondParsed]
|
|
688
|
+
sim: BackwardOneToOneRelation[Cond]
|
|
689
|
+
|
|
690
|
+
async def sims(self):
|
|
691
|
+
return {self.raw_txt: await self.sim.sims()} if await self.sim else self.raw_txt
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
class Synonym(BaseModel):
|
|
695
|
+
typ: SynonymType = IntEnumField(SynonymType, db_index=True)
|
|
696
|
+
txt: str = CharField(255, unique=True)
|
|
697
|
+
boundary: Boundary = IntEnumField(Boundary, default=Boundary.no)
|
|
698
|
+
curs: ManyToManyRelation[Cur] = ManyToManyField(
|
|
699
|
+
"models.Cur", "synonym_cur", related_name="synonyms", on_update=CASCADE
|
|
700
|
+
)
|
|
701
|
+
val: int | None = UInt2Field(null=True) # SynonymType dependent (e.g., no-slavic for name, approx for ppo.)
|
|
702
|
+
is_re: bool = BooleanField(default=False)
|
|
703
|
+
|
|
704
|
+
|
|
705
|
+
class CondSim(BaseModel):
|
|
706
|
+
cond: OneToOneRelation[Cond] = OneToOneField("models.Cond", "sim", primary_key=True, on_update=CASCADE)
|
|
707
|
+
cond_id: int # new
|
|
708
|
+
similarity: int = UInt2Field(db_index=True) # /1000
|
|
709
|
+
cond_rel: ForeignKeyRelation[Cond] = ForeignKeyField("models.Cond", "sims_rel", on_update=CASCADE)
|
|
710
|
+
cond_rel_id: int # old
|
|
711
|
+
|
|
712
|
+
class Meta:
|
|
713
|
+
table = "cond_sim"
|
|
714
|
+
|
|
715
|
+
|
|
716
|
+
class PmGroup(Model):
|
|
717
|
+
id: int = SmallIntField(True)
|
|
718
|
+
name: str = CharField(127)
|
|
719
|
+
pms: BackwardFKRelation[Pm]
|
|
720
|
+
|
|
721
|
+
class Meta:
|
|
722
|
+
table = "pm_group"
|
|
723
|
+
|
|
724
|
+
|
|
725
|
+
class Pm(Model):
|
|
726
|
+
# name: str = CharField(63) # mv to pmex cause it diffs on each ex
|
|
727
|
+
norm: str | None = CharField(255)
|
|
728
|
+
acronym: str | None = CharField(7, null=True)
|
|
729
|
+
country: ForeignKeyNullableRelation[Country] = ForeignKeyField(
|
|
730
|
+
"models.Country", "pms", on_update=CASCADE, null=True
|
|
731
|
+
)
|
|
732
|
+
df_cur: ForeignKeyNullableRelation[Cur] = ForeignKeyField("models.Cur", "df_pms", on_update=CASCADE, null=True)
|
|
733
|
+
grp: ForeignKeyNullableRelation[PmGroup] = ForeignKeyField("models.PmGroup", "pms", on_update=CASCADE, null=True)
|
|
734
|
+
alias: str | None = CharField(63, null=True)
|
|
735
|
+
extra: str | None = CharField(63, null=True)
|
|
736
|
+
ok: bool = BooleanField(default=True)
|
|
737
|
+
bank: bool | None = BooleanField(null=True)
|
|
738
|
+
qr: bool = BooleanField(default=False)
|
|
739
|
+
fee: int | None = UInt2Field(null=True) # /10_000
|
|
740
|
+
|
|
741
|
+
typ: PmType | None = IntEnumField(PmType, null=True)
|
|
742
|
+
|
|
743
|
+
ads: ManyToManyRelation[Ad]
|
|
744
|
+
curs: ManyToManyRelation[Cur]
|
|
745
|
+
no_conds: ManyToManyRelation[Cond]
|
|
746
|
+
only_conds: ManyToManyRelation[Cond]
|
|
747
|
+
exs: ManyToManyRelation[Ex] = ManyToManyField("models.Ex", "pmex", on_update=CASCADE) # no need. use pmexs[.exid]
|
|
748
|
+
conds: BackwardFKRelation[CondParsed]
|
|
749
|
+
orders: BackwardFKRelation[Order]
|
|
750
|
+
pmcurs: BackwardFKRelation[PmCur] # no need. use curs
|
|
751
|
+
pmexs: BackwardFKRelation[PmEx]
|
|
752
|
+
agents: BackwardFKRelation[PmAgent]
|
|
753
|
+
topupable: BackwardFKRelation[TopUpAble]
|
|
754
|
+
|
|
755
|
+
class Meta:
|
|
756
|
+
table_description = "Payment methods"
|
|
757
|
+
unique_together = (("norm", "country_id"), ("alias", "country_id"))
|
|
758
|
+
|
|
759
|
+
# class PydanticMeta(Model.PydanticMeta):
|
|
760
|
+
# max_recursion = 3
|
|
761
|
+
# backward_relations = True
|
|
762
|
+
# include = "id", "name", "logo", "pmexs__sbp"
|
|
763
|
+
|
|
764
|
+
# def epyd(self):
|
|
765
|
+
# module_name = f"xync_client.{self.ex.name}.pyd"
|
|
766
|
+
# __import__(module_name)
|
|
767
|
+
# return sys.modules[module_name].PmEpyd
|
|
768
|
+
|
|
769
|
+
|
|
770
|
+
class CondParsed(Model, TsTrait):
|
|
771
|
+
cond: OneToOneNullableRelation[Cond] = OneToOneField("models.Cond", "parsed", on_update=CASCADE, null=True)
|
|
772
|
+
cond_id: int # new
|
|
773
|
+
to_party: Party = IntEnumField(Party, null=True)
|
|
774
|
+
from_party: Party = IntEnumField(Party, null=True)
|
|
775
|
+
ppo: int = UInt1Field(null=True) # Payments per order
|
|
776
|
+
slip_req: Slip = IntEnumField(Slip, null=True)
|
|
777
|
+
slip_send: Slip = IntEnumField(Slip, null=True)
|
|
778
|
+
abuser: AbuserType = IntEnumField(AbuserType, default=AbuserType.no)
|
|
779
|
+
slavic: bool = BooleanField(null=True)
|
|
780
|
+
mtl_like: bool = BooleanField(null=True)
|
|
781
|
+
scale: int = SmallIntField(null=True)
|
|
782
|
+
bank_side: bool = BooleanField(null=True) # False - except these banks, True - only this banks
|
|
783
|
+
sbp_strict: SbpStrict = IntEnumField(SbpStrict, default=SbpStrict.no)
|
|
784
|
+
contact: str | None = CharField(127, null=True)
|
|
785
|
+
done: bool = BooleanField(default=False, db_index=True)
|
|
786
|
+
banks: ManyToManyRelation[Pm] = ManyToManyField("models.Pm", "cond_banks", related_name="conds", on_update=CASCADE)
|
|
787
|
+
|
|
788
|
+
class Meta:
|
|
789
|
+
table = "cond_parsed"
|
|
790
|
+
|
|
791
|
+
|
|
792
|
+
class Ad(Model, TsTrait):
|
|
793
|
+
exid: int = UInt8Field(db_index=True) # todo: спарить уникальность с биржей, тк на разных биржах могут совпасть
|
|
794
|
+
pair_side: ForeignKeyRelation[PairSide] = ForeignKeyField("models.PairSide", "ads", on_update=CASCADE)
|
|
795
|
+
price: int = UInt4Field() # /10^cur.scale
|
|
796
|
+
premium: int = IntField(null=True) # /10_000
|
|
797
|
+
amount: int = UInt4Field() # /10^cur.scale
|
|
798
|
+
quantity: int = UInt8Field(null=True) # /10^coinex.scale
|
|
799
|
+
min_fiat: int = UInt4Field() # /10^cur.scale
|
|
800
|
+
max_fiat: int | None = UInt4Field(null=True) # /10^cur.scale
|
|
801
|
+
auto_msg: str | None = CharField(4095, null=True)
|
|
802
|
+
status: AdStatus = IntEnumField(AdStatus, default=AdStatus.active)
|
|
803
|
+
filtered: bool = BooleanField(default=False)
|
|
804
|
+
|
|
805
|
+
cond: ForeignKeyNullableRelation[Cond] = ForeignKeyField("models.Cond", "ads", on_update=CASCADE, null=True)
|
|
806
|
+
cond_id: int
|
|
807
|
+
maker: ForeignKeyRelation[Actor] = ForeignKeyField("models.Actor", "my_ads", on_update=CASCADE)
|
|
808
|
+
maker_id: int
|
|
809
|
+
|
|
810
|
+
pms: ManyToManyRelation[Pm] = ManyToManyField("models.Pm", "ad_pm", related_name="ads", on_update=CASCADE)
|
|
811
|
+
my_ad: BackwardOneToOneRelation[MyAd]
|
|
812
|
+
orders: BackwardFKRelation[Order]
|
|
813
|
+
|
|
814
|
+
_icon = "ad"
|
|
815
|
+
_name = {"pair_side__pairex__coin__ticker", "pair_side__pairex__cur__ticker", "pair_side__sell", "price"}
|
|
816
|
+
|
|
817
|
+
class Meta:
|
|
818
|
+
table_description = "P2P Advertisements"
|
|
819
|
+
unique_together = (("exid", "maker_id"),)
|
|
820
|
+
|
|
821
|
+
async def to_float(self, ex_id: int = None) -> Ad:
|
|
822
|
+
ex_id = ex_id or self.maker.ex_id
|
|
823
|
+
cur_scl = await CurEx.get(cur_id=self.pair_side.pair.cur_id, ex_id=ex_id).values_list('scale', flat=True)
|
|
824
|
+
coin_scl = await CoinEx.get(coin_id=self.pair_side.pair.coin_id, ex_id=ex_id).values_list('scale', flat=True)
|
|
825
|
+
cur_k = 10 ** -cur_scl
|
|
826
|
+
coin_k = 10 ** -coin_scl
|
|
827
|
+
self.amount = round(self.amount * cur_k, cur_scl)
|
|
828
|
+
self.price = round(self.price * cur_k, cur_scl)
|
|
829
|
+
self.quantity = self.quantity and round(self.quantity * coin_k, coin_scl)
|
|
830
|
+
return self
|
|
831
|
+
|
|
832
|
+
@classmethod
|
|
833
|
+
async def from_flat(cls, ad: AdNewFlatRaw, coin_scl: int = None, cur_scl: int = None) -> Ad:
|
|
834
|
+
cur_scl = cur_scl or await CurEx.get(exid=ad.curex_exid, ex_id=ad.ex_id).values_list('scale', flat=True)
|
|
835
|
+
coin_scl = coin_scl or await CoinEx.get(exid=ad.coinex_exid, ex_id=ad.ex_id).values_list('scale', flat=True)
|
|
836
|
+
cur_k = 10 ** cur_scl
|
|
837
|
+
coin_k = 10 ** coin_scl
|
|
838
|
+
|
|
839
|
+
self.amount = round(self.amount * cur_k, cur_scl)
|
|
840
|
+
self.price = round(self.price * cur_k, cur_scl)
|
|
841
|
+
self.quantity = self.quantity and round(self.quantity * coin_k, coin_scl)
|
|
842
|
+
return self
|
|
843
|
+
|
|
844
|
+
# def epyds(self) -> tuple[PydModel, PydModel, PydModel, PydModel, PydModel, PydModel]:
|
|
845
|
+
# module_name = f"xync_client.{self.maker.ex.name}.pyd"
|
|
846
|
+
# __import__(module_name)
|
|
847
|
+
# return (
|
|
848
|
+
# sys.modules[module_name].AdEpyd,
|
|
849
|
+
# sys.modules[module_name].AdFullEpyd,
|
|
850
|
+
# sys.modules[module_name].MyAdEpydPurchase,
|
|
851
|
+
# sys.modules[module_name].MyAdInEpydPurchase,
|
|
852
|
+
# sys.modules[module_name].MyAdEpydSale,
|
|
853
|
+
# sys.modules[module_name].MyAdInEpydSale,
|
|
854
|
+
# )
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
class MyAd(Model): # Road
|
|
858
|
+
WEB = "https://www.bybit.com/{lang}/p2p/{side}/{coin}/{cur}?share="
|
|
859
|
+
MOB = "https://app.bybit.com/inapp?by_dp=bybitapp://open/route?targetUrl=by-mini://p2p/home?share="
|
|
860
|
+
|
|
861
|
+
ad: OneToOneRelation[Ad] = OneToOneField("models.Ad", "my_ad", on_update=CASCADE)
|
|
862
|
+
ad_id: int # new
|
|
863
|
+
target_place: int = UInt1Field(default=1)
|
|
864
|
+
hex: bytes = BinaryField(null=True)
|
|
865
|
+
shared_at: datetime | None = DatetimeSecField(null=True)
|
|
866
|
+
blocked: bool = BooleanField(default=False)
|
|
867
|
+
|
|
868
|
+
pay_req: ForeignKeyNullableRelation[PayReq] = ForeignKeyField(
|
|
869
|
+
"models.PayReq", "maked_ads", on_update=CASCADE, null=True
|
|
870
|
+
)
|
|
871
|
+
credexs: ManyToManyRelation[CredEx] = ManyToManyField(
|
|
872
|
+
"models.CredEx", through="myad_cred", related_name="my_ads", on_update=CASCADE
|
|
873
|
+
)
|
|
874
|
+
hot_users: ManyToManyRelation[User] = ManyToManyField(
|
|
875
|
+
"models.User", "myad_user", related_name="hot_ads", on_update=CASCADE
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
class Meta:
|
|
879
|
+
table = "my_ad"
|
|
880
|
+
|
|
881
|
+
def get_url(self, lang: str = "en-US") -> tuple[str, str]:
|
|
882
|
+
# if not self.hex:
|
|
883
|
+
# return None
|
|
884
|
+
hx = self.hex.hex()
|
|
885
|
+
side = "buy" if self.ad.pair_side.is_sell else "sell"
|
|
886
|
+
coin, cur = self.ad.pair_side.pair.coin.ticker, self.ad.pair_side.pair.cur.ticker
|
|
887
|
+
mob_pref = self.WEB.format(lang=lang, side=side, coin=coin, cur=cur)
|
|
888
|
+
return mob_pref + hx, self.MOB + hx
|
|
889
|
+
|
|
890
|
+
@classmethod
|
|
891
|
+
# запрос для получения объяв всех агентов этой биржи для разогрева
|
|
892
|
+
def hot_mads_query(cls, ex_ids: list[int], active: bool = True):
|
|
893
|
+
fltr = dict(credexs__cred__ovr_pm_id=0, ad__maker__agent__status__gte=3)
|
|
894
|
+
if active:
|
|
895
|
+
fltr["ad__status"] = AdStatus.active
|
|
896
|
+
if ex_ids:
|
|
897
|
+
fltr["ad__maker__ex_id__in"] = ex_ids
|
|
898
|
+
return cls.filter(**fltr).prefetch_related("ad__pair_side__pair__coin", "ad__pair_side__pair__cur", "ad__maker")
|
|
899
|
+
|
|
900
|
+
@classmethod
|
|
901
|
+
#
|
|
902
|
+
def new(cls, coin_id: int, cur_id: int, min_fiat: int, is_hot: bool = False): ...
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
class Hot(Model, TsTrait):
|
|
906
|
+
user: ForeignKeyNullableRelation[User] = ForeignKeyField("models.User", "hots", on_update=CASCADE)
|
|
907
|
+
user_id: int
|
|
908
|
+
status: HotStatus = IntEnumField(HotStatus, default=HotStatus.opened)
|
|
909
|
+
msg_id: int | None = IntField(null=True)
|
|
910
|
+
actors: ManyToManyRelation[Actor] = ManyToManyField("models.Actor", related_name="hots", on_update=CASCADE)
|
|
911
|
+
|
|
912
|
+
orders: BackwardFKRelation[Order]
|
|
913
|
+
|
|
914
|
+
_name = {"id"}
|
|
915
|
+
|
|
916
|
+
|
|
917
|
+
class MyAdHotUser(Model):
|
|
918
|
+
user: ForeignKeyRelation[User] = ForeignKeyField("models.User", on_update=CASCADE)
|
|
919
|
+
user_id: int
|
|
920
|
+
my_ad: ForeignKeyRelation[MyAd] = ForeignKeyField("models.MyAd", on_update=CASCADE)
|
|
921
|
+
my_ad_id: int
|
|
922
|
+
updated_at: datetime | None = DatetimeSecField(auto_now=True)
|
|
923
|
+
|
|
924
|
+
class Meta:
|
|
925
|
+
table = "myad_user"
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
class Race(Model):
|
|
929
|
+
road: ForeignKeyRelation[MyAd] = ForeignKeyField("models.MyAd", "race", on_update=CASCADE)
|
|
930
|
+
road_id: int
|
|
931
|
+
ceil: int = UInt4Field(null=True) # /10^cur.scale
|
|
932
|
+
target_place: int = SmallIntField(default=1)
|
|
933
|
+
vm_filter: bool = BooleanField(default=True)
|
|
934
|
+
started: bool = BooleanField(default=True)
|
|
935
|
+
filter_amount: int = UInt4Field() # /10^cur.scale
|
|
936
|
+
updated_at: datetime | None = DatetimeSecField(auto_now=True)
|
|
937
|
+
|
|
938
|
+
stats: BackwardFKRelation[RaceStat]
|
|
939
|
+
|
|
940
|
+
# def overprice_filter(self, ads: list[BaseAd]):
|
|
941
|
+
# ads[0]
|
|
942
|
+
# # вырезаем ads с ценами выше потолка
|
|
943
|
+
# if ads and (self.ceil - Decimal(ads[0].price)) * k > 0:
|
|
944
|
+
# if int(ads[0].userId) != self.actor.exid:
|
|
945
|
+
# ads.pop(0)
|
|
946
|
+
# self.overprice_filter(ads, self.ceil, k)
|
|
947
|
+
|
|
948
|
+
|
|
949
|
+
class RaceStat(Model):
|
|
950
|
+
race: ForeignKeyRelation[Race] = ForeignKeyField("models.Race", "stats", on_update=CASCADE)
|
|
951
|
+
race_id: int
|
|
952
|
+
rivals: ManyToManyRelation[Ad] = ManyToManyField("models.Ad", "rivals", related_name="stats", on_update=CASCADE)
|
|
953
|
+
rival_id: int
|
|
954
|
+
price: int = UInt4Field() # /10^cur.scale
|
|
955
|
+
premium: int = SmallIntField(null=True) # /10_000
|
|
956
|
+
place: int = UInt1Field()
|
|
957
|
+
created_at: datetime | None = DatetimeSecField(auto_now_add=True)
|
|
958
|
+
|
|
959
|
+
class Meta:
|
|
960
|
+
table = "race_stat"
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
class PmRep(Model):
|
|
964
|
+
ex: ForeignKeyNullableRelation[Ex] = ForeignKeyField("models.Ex", "pm_reps", on_update=CASCADE, null=True)
|
|
965
|
+
ex_id: int
|
|
966
|
+
src: str | None = CharField(63, unique=True)
|
|
967
|
+
target: str | None = CharField(63)
|
|
968
|
+
used_at: datetime | None = DatetimeSecField(null=True)
|
|
969
|
+
|
|
970
|
+
class Meta:
|
|
971
|
+
table = "pm_rep"
|
|
972
|
+
# unique_together = (("src", "target"),)
|
|
973
|
+
|
|
974
|
+
|
|
975
|
+
class PmAgent(Model):
|
|
976
|
+
pm: ForeignKeyRelation[Pm] = ForeignKeyField("models.Pm", "agents", on_update=CASCADE)
|
|
977
|
+
pm_id: int
|
|
978
|
+
user: ForeignKeyRelation[User] = ForeignKeyField("models.User", "pm_agents", on_update=CASCADE)
|
|
979
|
+
user_id: int
|
|
980
|
+
auth: dict = JSONField(default={})
|
|
981
|
+
state: dict = JSONField(default={})
|
|
982
|
+
active: bool = BooleanField(null=True, default=False)
|
|
983
|
+
|
|
984
|
+
class Meta:
|
|
985
|
+
table = "pm_agent"
|
|
986
|
+
unique_together = (("pm_id", "user_id"),)
|
|
987
|
+
|
|
988
|
+
def client(self, browser=None):
|
|
989
|
+
module_name = f"xync_client.Pms.{self.pm.norm.capitalize()}.agent"
|
|
990
|
+
__import__(module_name)
|
|
991
|
+
kwargs = {}
|
|
992
|
+
if browser:
|
|
993
|
+
kwargs["browser"] = browser
|
|
994
|
+
return sys.modules[module_name].PmAgentClient(self, **kwargs)
|
|
995
|
+
|
|
996
|
+
|
|
997
|
+
class PmCur(Model): # for fiat with no exs tie
|
|
998
|
+
pm: ForeignKeyRelation[Pm] = ForeignKeyField("models.Pm", on_update=CASCADE)
|
|
999
|
+
pm_id: int
|
|
1000
|
+
cur: ForeignKeyRelation[Cur] = ForeignKeyField("models.Cur", on_update=CASCADE)
|
|
1001
|
+
cur_id: int
|
|
1002
|
+
|
|
1003
|
+
creds: BackwardFKRelation[Cred]
|
|
1004
|
+
sends: BackwardFKRelation[Transfer]
|
|
1005
|
+
exs: ManyToManyRelation[Ex]
|
|
1006
|
+
|
|
1007
|
+
class Meta:
|
|
1008
|
+
table_description = "Payment methods - Currencies"
|
|
1009
|
+
unique_together = (("pm_id", "cur_id"),)
|
|
1010
|
+
|
|
1011
|
+
class PydanticMeta(Model.PydanticMeta):
|
|
1012
|
+
max_recursion: int = 2 # default: 3
|
|
1013
|
+
include = "cur_id", "pm"
|
|
1014
|
+
|
|
1015
|
+
|
|
1016
|
+
class PmEx(BaseModel): # existence pm in ex with no cur tie
|
|
1017
|
+
ex: ForeignKeyRelation[Ex] = ForeignKeyField("models.Ex", "pmexs", on_update=CASCADE)
|
|
1018
|
+
ex_id: int
|
|
1019
|
+
pm: ForeignKeyRelation[Pm] = ForeignKeyField("models.Pm", "pmexs", on_update=CASCADE)
|
|
1020
|
+
pm_id: int
|
|
1021
|
+
logo: ForeignKeyNullableRelation[File] = ForeignKeyField("models.File", "pmex_logos", on_update=CASCADE, null=True)
|
|
1022
|
+
logo_id: int
|
|
1023
|
+
exid: str = CharField(63)
|
|
1024
|
+
name: str = CharField(255)
|
|
1025
|
+
|
|
1026
|
+
banks: BackwardFKRelation[PmExBank]
|
|
1027
|
+
|
|
1028
|
+
class Meta:
|
|
1029
|
+
unique_together = (("ex_id", "exid"),) # , ("ex", "pm"), ("ex", "name") # todo: tmp removed for HTX duplicates
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
class PmExBank(BaseModel): # banks for SBP
|
|
1033
|
+
pmex: ForeignKeyRelation[PmEx] = ForeignKeyField("models.PmEx", "banks", on_update=CASCADE)
|
|
1034
|
+
pmex_id: int
|
|
1035
|
+
exid: str = CharField(63)
|
|
1036
|
+
name: str = CharField(63)
|
|
1037
|
+
|
|
1038
|
+
creds: ManyToManyRelation[Cred]
|
|
1039
|
+
|
|
1040
|
+
class Meta:
|
|
1041
|
+
table = "pmex_bank"
|
|
1042
|
+
unique_together = (("pmex", "exid"),)
|
|
1043
|
+
|
|
1044
|
+
|
|
1045
|
+
# class PmCurEx(BaseModel): # existence pm in ex for exact cur, with "blocked" flag
|
|
1046
|
+
# pmcur: ForeignKeyRelation[PmCur] = ForeignKeyField("models.PmCur")
|
|
1047
|
+
# pmcur_id: int
|
|
1048
|
+
# ex: ForeignKeyRelation[Ex] = ForeignKeyField("models.Ex")
|
|
1049
|
+
# ex_id: int
|
|
1050
|
+
# blocked: bool = BooleanField(default=False) # todo: move to curex or pmex?
|
|
1051
|
+
#
|
|
1052
|
+
# # class Meta:
|
|
1053
|
+
# # unique_together = (("ex_id", "pmcur_id"),)
|
|
1054
|
+
|
|
1055
|
+
|
|
1056
|
+
class Cred(Model):
|
|
1057
|
+
pmcur: ForeignKeyRelation[PmCur] = ForeignKeyField("models.PmCur", on_update=CASCADE)
|
|
1058
|
+
pmcur_id: int
|
|
1059
|
+
detail: str = CharField(255)
|
|
1060
|
+
name: str | None = CharField(127, null=True)
|
|
1061
|
+
extra: str | None = CharField(255, null=True)
|
|
1062
|
+
person: ForeignKeyRelation[Person] = ForeignKeyField("models.Person", "creds")
|
|
1063
|
+
person_id: int
|
|
1064
|
+
from_chat: int # todo: ForeignKeyNullableRelation[Order] = ForeignKeyField("models.Order", null=True)
|
|
1065
|
+
|
|
1066
|
+
ovr_pm: ForeignKeyRelation[Pm] = ForeignKeyField("models.Pm", on_update=CASCADE, null=True)
|
|
1067
|
+
ovr_pm_id: int
|
|
1068
|
+
banks: ManyToManyRelation[PmExBank] = ManyToManyField("models.PmExBank", related_name="creds", on_update=CASCADE)
|
|
1069
|
+
|
|
1070
|
+
fiat: BackwardOneToOneRelation[Fiat]
|
|
1071
|
+
receives: BackwardOneToOneRelation[Transfer]
|
|
1072
|
+
credexs: BackwardFKRelation[CredEx]
|
|
1073
|
+
orders: BackwardFKRelation[Order]
|
|
1074
|
+
pay_reqs: BackwardFKRelation[PayReq]
|
|
1075
|
+
|
|
1076
|
+
_name = {"detail"}
|
|
1077
|
+
|
|
1078
|
+
def repr(self):
|
|
1079
|
+
xtr = f" ({self.extra})" if self.extra else ""
|
|
1080
|
+
name = f", имя: {self.name}" if self.name else ""
|
|
1081
|
+
return f"`{self.detail}`{name}{xtr}"
|
|
1082
|
+
|
|
1083
|
+
class Meta:
|
|
1084
|
+
table_description = "Currency accounts"
|
|
1085
|
+
unique_together = (("person_id", "pmcur_id", "ovr_pm_id"),)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
class CredEx(Model):
|
|
1089
|
+
exid: int = UInt8Field()
|
|
1090
|
+
cred: ForeignKeyRelation[Cred] = ForeignKeyField("models.Cred", "credexs", on_update=CASCADE)
|
|
1091
|
+
cred_id: int
|
|
1092
|
+
ex: ForeignKeyRelation[Ex] = ForeignKeyField("models.Ex", "credexs", on_update=CASCADE)
|
|
1093
|
+
ex_id: int
|
|
1094
|
+
|
|
1095
|
+
_name = {"exid"}
|
|
1096
|
+
|
|
1097
|
+
class Meta:
|
|
1098
|
+
table_description = "Credential on Exchange"
|
|
1099
|
+
unique_together = (("ex_id", "exid"),)
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
class Fiat(Model):
|
|
1103
|
+
cred: OneToOneRelation[Cred] = OneToOneField("models.Cred", "fiat", on_update=CASCADE)
|
|
1104
|
+
cred_id: int
|
|
1105
|
+
amount: int = UInt4Field() # /10^cur.scale
|
|
1106
|
+
target: int = UInt4Field(null=True) # /10^cur.scale
|
|
1107
|
+
min_deposit: int = UInt4Field(null=True) # /10^cur.scale
|
|
1108
|
+
|
|
1109
|
+
class Meta:
|
|
1110
|
+
table_description = "Currency balances"
|
|
1111
|
+
|
|
1112
|
+
class PydanticMeta(Model.PydanticMeta):
|
|
1113
|
+
# max_recursion: int = 2
|
|
1114
|
+
backward_relations = False
|
|
1115
|
+
include = "id", "cred__pmcur", "cred__detail", "cred__name", "amount"
|
|
1116
|
+
|
|
1117
|
+
@staticmethod
|
|
1118
|
+
def epyd(ex: Ex):
|
|
1119
|
+
module_name = f"xync_client.{ex.name}.pyd"
|
|
1120
|
+
__import__(module_name)
|
|
1121
|
+
return sys.modules[module_name].FiatEpyd
|
|
1122
|
+
|
|
1123
|
+
|
|
1124
|
+
class Limit(Model):
|
|
1125
|
+
pmcur: ForeignKeyRelation[PmCur] = ForeignKeyField("models.PmCur", on_update=CASCADE)
|
|
1126
|
+
pmcur_id: int
|
|
1127
|
+
amount: int = UInt4Field(null=True) # '$' if unit >= 0 else 'transactions count'
|
|
1128
|
+
unit: int = UInt1Field(default=30) # positive: $/days, 0: $/transaction, negative: transactions count / days
|
|
1129
|
+
# 0 - same group, 1 - to parent group, 2 - to grandparent # only for output trans, on input = None
|
|
1130
|
+
level: float | None = UInt1Field(default=0, null=True)
|
|
1131
|
+
income: bool = BooleanField(default=False)
|
|
1132
|
+
added_by: ForeignKeyRelation[User] = ForeignKeyField("models.User", "limits", on_update=CASCADE)
|
|
1133
|
+
added_by_id: int
|
|
1134
|
+
|
|
1135
|
+
_name = {"pmcur__pm__name", "pmcur__cur__ticker", "unit", "income", "amount"}
|
|
1136
|
+
|
|
1137
|
+
class Meta:
|
|
1138
|
+
table_description = "Currency accounts balance"
|
|
1139
|
+
|
|
1140
|
+
|
|
1141
|
+
class Addr(Model):
|
|
1142
|
+
coin: ForeignKeyRelation[Coin] = ForeignKeyField("models.Coin", "addrs", on_update=CASCADE)
|
|
1143
|
+
coin_id: int
|
|
1144
|
+
actor: ForeignKeyRelation[Actor] = ForeignKeyField("models.Actor", "addrs", on_update=CASCADE)
|
|
1145
|
+
actor_id: int
|
|
1146
|
+
|
|
1147
|
+
nets: ManyToManyRelation[Net] = ManyToManyField("models.Net", "net_addr", related_name="addrs", on_update=CASCADE)
|
|
1148
|
+
|
|
1149
|
+
pay_reqs: BackwardFKRelation[PayReq]
|
|
1150
|
+
|
|
1151
|
+
_name = {"coin__ticker", "free"}
|
|
1152
|
+
|
|
1153
|
+
class Meta:
|
|
1154
|
+
table_description = "Coin address on cex"
|
|
1155
|
+
unique_together = (("coin_id", "actor_id"),)
|
|
1156
|
+
|
|
1157
|
+
|
|
1158
|
+
class Asset(Model):
|
|
1159
|
+
addr: ForeignKeyRelation[Addr] = ForeignKeyField("models.Addr", "addrs", on_update=CASCADE)
|
|
1160
|
+
addr_id: int
|
|
1161
|
+
agent: ForeignKeyRelation[Agent] = ForeignKeyField("models.Agent", "assets", on_update=CASCADE)
|
|
1162
|
+
agent_id: int
|
|
1163
|
+
typ: AddrExType = IntEnumField(AddrExType, default=AddrExType.found)
|
|
1164
|
+
free: int = UInt8Field() # /10^coinex.scale
|
|
1165
|
+
freeze: int | None = UInt8Field(default=0) # /10^coinex.scale
|
|
1166
|
+
lock: int | None = UInt8Field(default=0) # /10^coinex.scale
|
|
1167
|
+
target: int | None = UInt8Field(default=0, null=True) # /10^coinex.scale
|
|
1168
|
+
|
|
1169
|
+
_name = {"asset__coin__ticker", "free"}
|
|
1170
|
+
|
|
1171
|
+
class Meta:
|
|
1172
|
+
table_description = "Coin balance"
|
|
1173
|
+
unique_together = (("addr_id", "agent_id", "typ"),)
|
|
1174
|
+
|
|
1175
|
+
def epyd(self):
|
|
1176
|
+
module_name = f"xync_client.{self.agent.ex.name}.pyd"
|
|
1177
|
+
__import__(module_name)
|
|
1178
|
+
return sys.modules[module_name].AssetEpyd
|
|
1179
|
+
|
|
1180
|
+
|
|
1181
|
+
class PayReq(Model, TsTrait):
|
|
1182
|
+
pay_until: datetime = DatetimeSecField()
|
|
1183
|
+
addr: ForeignKeyNullableRelation[Addr] = ForeignKeyField("models.Addr", "pay_reqs", on_update=CASCADE, null=True)
|
|
1184
|
+
addr_id: int
|
|
1185
|
+
cred: ForeignKeyNullableRelation[Cred] = ForeignKeyField("models.Cred", "pay_reqs", on_update=CASCADE, null=True)
|
|
1186
|
+
cred_id: int
|
|
1187
|
+
user: ForeignKeyRelation[User] = ForeignKeyField("models.User", "pay_reqs", on_update=CASCADE)
|
|
1188
|
+
user_id: int
|
|
1189
|
+
amount: float = UInt4Field()
|
|
1190
|
+
parts: int = UInt1Field(default=1)
|
|
1191
|
+
payed_at: datetime | None = DatetimeSecField(null=True)
|
|
1192
|
+
|
|
1193
|
+
maked_ads: BackwardFKRelation[MyAd]
|
|
1194
|
+
taken_orders: BackwardFKRelation[Order]
|
|
1195
|
+
|
|
1196
|
+
_icon = "pay"
|
|
1197
|
+
_name = {"ad_id"}
|
|
1198
|
+
|
|
1199
|
+
class Meta:
|
|
1200
|
+
table_description = "Payment request"
|
|
1201
|
+
unique_together = (("user_id", "cred_id", "addr_id"),)
|
|
1202
|
+
|
|
1203
|
+
|
|
1204
|
+
class Order(Model, TsTrait):
|
|
1205
|
+
exid: int = UInt8Field(unique=True) # todo: спарить уникальность с биржей, тк на разных биржах могут совпасть
|
|
1206
|
+
ad: ForeignKeyRelation[Ad] = ForeignKeyField("models.Ad", "orders", on_update=CASCADE)
|
|
1207
|
+
ad_id: int
|
|
1208
|
+
amount: int = UInt4Field() # /10^cur.scale
|
|
1209
|
+
quantity: int = UInt8Field(null=True) # /10^coinex.scale
|
|
1210
|
+
cred: ForeignKeyRelation[Cred] = ForeignKeyField("models.Cred", "orders", on_update=CASCADE, null=True)
|
|
1211
|
+
cred_id: int | None
|
|
1212
|
+
taker: ForeignKeyRelation[Actor] = ForeignKeyField("models.Actor", "taken_orders", on_update=CASCADE)
|
|
1213
|
+
taker_id: int
|
|
1214
|
+
payreq: ForeignKeyNullableRelation[PayReq] = ForeignKeyField(
|
|
1215
|
+
"models.PayReq", "taken_orders", on_update=CASCADE, null=True
|
|
1216
|
+
)
|
|
1217
|
+
payreq_id: int
|
|
1218
|
+
hot: ForeignKeyNullableRelation[Hot] = ForeignKeyField("models.Hot", "orders", on_update=CASCADE, null=True)
|
|
1219
|
+
hot_id: int
|
|
1220
|
+
maker_topic: int = UInt2Field(null=True) # todo: remove nullability
|
|
1221
|
+
taker_topic: int = UInt2Field(null=True)
|
|
1222
|
+
status: OrderStatus = IntEnumField(OrderStatus, default=OrderStatus.created)
|
|
1223
|
+
chat_parsed: bool = BooleanField(default=False)
|
|
1224
|
+
|
|
1225
|
+
hots: ManyToManyRelation[Hot]
|
|
1226
|
+
msgs: BackwardFKRelation[Msg]
|
|
1227
|
+
transfers: BackwardFKRelation[Transfer]
|
|
1228
|
+
|
|
1229
|
+
_name = {"cred__pmcur__pm__name"}
|
|
1230
|
+
|
|
1231
|
+
def ami_maker(self, actor_id: int) -> bool:
|
|
1232
|
+
return self.taker_id != actor_id
|
|
1233
|
+
|
|
1234
|
+
async def is_it_seller(self, actor_id: int) -> bool:
|
|
1235
|
+
await self.fetch_related("ad__pair_side") # todo: check and fetch if only need
|
|
1236
|
+
return self.ad.pair_side.is_sell == self.ami_maker(actor_id)
|
|
1237
|
+
|
|
1238
|
+
async def actors_bs(self) -> tuple[Actor, Actor]: # (buyer, seller)
|
|
1239
|
+
# ad_not_fetched = not isinstance(self.ad, Ad)
|
|
1240
|
+
# if ad_not_fetched or not isinstance(self.ad.pair_side, PairSide):
|
|
1241
|
+
# await self.fetch_related("ad__pair_side")
|
|
1242
|
+
# if ad_not_fetched or not isinstance(self.ad.maker, Actor):
|
|
1243
|
+
# await self.fetch_related("ad__maker")
|
|
1244
|
+
# if not isinstance(self.taker, Actor):
|
|
1245
|
+
# await self.fetch_related("taker")
|
|
1246
|
+
# todo: СУКАПИЗДЕЦБЛЯ! Один фетч перекрывает получений предыдущих связей фетчем!!!
|
|
1247
|
+
k = int(self.ad.pair_side.is_sell) * 2 - 1
|
|
1248
|
+
# noinspection PyTypeChecker
|
|
1249
|
+
return (self.taker, self.ad.maker)[::k]
|
|
1250
|
+
|
|
1251
|
+
async def users_bs(self) -> tuple[User, User]: # (buyer, seller)
|
|
1252
|
+
actors_bs = await self.actors_bs()
|
|
1253
|
+
return await User.get(persons__id=actors_bs[0].person_id), await User.get(persons__id=actors_bs[1].person_id)
|
|
1254
|
+
|
|
1255
|
+
def client(self, agent_client: BaseAgentClient = None): # noqa
|
|
1256
|
+
module_name = f"xync_client.{agent_client.ex_client.ex.name}.order"
|
|
1257
|
+
__import__(module_name)
|
|
1258
|
+
client = sys.modules[module_name].OrderClient
|
|
1259
|
+
return client(self, agent_client)
|
|
1260
|
+
|
|
1261
|
+
async def get_chat(self):
|
|
1262
|
+
await self.fetch_related("ad__pair_side", "msgs")
|
|
1263
|
+
return [
|
|
1264
|
+
f"{'b' if self.ad.pair_side.is_sell == m.to_maker else 's'}{m.to_maker and 't' or 'm'}: {m.txt or '<file>'}"
|
|
1265
|
+
for m in self.msgs
|
|
1266
|
+
]
|
|
1267
|
+
|
|
1268
|
+
async def is_hot_ad(self) -> bool:
|
|
1269
|
+
assert isinstance(self.ad.maker, Actor), "ad__maker should be fetched"
|
|
1270
|
+
assert isinstance(self.ad.pms[0], Pm), "ad__pms should be fetched"
|
|
1271
|
+
# проверяем что это именно DUMB ордер, а не COLD # todo: fox DRY: move to schema.model.fn:is_hot_ad()
|
|
1272
|
+
is_cold = await Cred.exists(
|
|
1273
|
+
pmcur__pm_id__in=[pm.id for pm in self.ad.pms], person=self.ad.maker.person_id, ovr_pm_id=0
|
|
1274
|
+
)
|
|
1275
|
+
return not is_cold
|
|
1276
|
+
|
|
1277
|
+
# когда прилетел новый ордер, и мы уже знаем юзера которому он принадлежит, и его последний hot
|
|
1278
|
+
async def hot_process(self) -> OrderResult:
|
|
1279
|
+
await self.fetch_related(
|
|
1280
|
+
"taker", "hot__user", "ad__pair_side__pair__cur", "ad__maker__person__user", "transfers"
|
|
1281
|
+
)
|
|
1282
|
+
# оптимистично сразу проставляем удачный результат, если ошибки - перезапишется
|
|
1283
|
+
res = OrderResult(err=OrderErr.ok if self.ad.pair_side.is_sell else OrderErr.i_wait_for_release)
|
|
1284
|
+
if self.status in (OrderStatus.created, OrderStatus.paid):
|
|
1285
|
+
# нет ли у тейкера незавершенных ордеров?
|
|
1286
|
+
if (
|
|
1287
|
+
incomplete_order := await Order.filter(
|
|
1288
|
+
id__not=self.id,
|
|
1289
|
+
taker__person__user_id=self.hot.user.id,
|
|
1290
|
+
ad__pair_side__is_sell=False,
|
|
1291
|
+
status__not_in={OrderStatus.completed, OrderStatus.canceled},
|
|
1292
|
+
)
|
|
1293
|
+
.first()
|
|
1294
|
+
.values("exid", "status", "amount")
|
|
1295
|
+
):
|
|
1296
|
+
res.err = OrderErr.uncompleted_orders
|
|
1297
|
+
res.data = OrderResult.Order.load(incomplete_order)
|
|
1298
|
+
return res
|
|
1299
|
+
# покупатель оплачивает ордер
|
|
1300
|
+
payment: Transfer | int = await self.pay() # -> self.status = paid
|
|
1301
|
+
if isinstance(payment, int): # если не хватило денег - там число=баланс
|
|
1302
|
+
# мы мейкер, и если это ad:sell значит покупатель - тейкер, иначе - мейкер
|
|
1303
|
+
res.err = self.ad.pair_side.is_sell and OrderErr.taker_balance_deficit or OrderErr.maker_balance_deficit
|
|
1304
|
+
res.data = (self.amount - payment) * 0.01
|
|
1305
|
+
elif self.ad.pair_side.is_sell: # успех и мы продавец - отпускаем
|
|
1306
|
+
self.status = OrderStatus.completed
|
|
1307
|
+
await self.save(update_fields=["status"])
|
|
1308
|
+
res.data = payment.amount * 0.01
|
|
1309
|
+
else: # успех и мы покупатель - ждем когда продавец отпустит
|
|
1310
|
+
res.data = OrderResult.Order.load(self)
|
|
1311
|
+
return res
|
|
1312
|
+
# статус ордера не "новый" и не "оплачен": мы не должны сюда попадать, чини!
|
|
1313
|
+
logging.error(f"Hot order status SHOULD NOT BE: {self.status.name}! Fix it!")
|
|
1314
|
+
res.data = OrderResult.Order.load(self)
|
|
1315
|
+
res.err = OrderErr.wrong_status_for_hot
|
|
1316
|
+
return res
|
|
1317
|
+
|
|
1318
|
+
async def pay(self, pm_id: int = 0) -> Transfer | int:
|
|
1319
|
+
if tsfs := self.transfers:
|
|
1320
|
+
if sum(t.amount for t in tsfs) < self.amount:
|
|
1321
|
+
ValueError(f"Оплачено меньше суммы ордера {self.exid}")
|
|
1322
|
+
if len(tsfs) > 1:
|
|
1323
|
+
logging.warning(f"Несколько оплат по ордеру {self.exid}")
|
|
1324
|
+
return tsfs[0]
|
|
1325
|
+
if pm_id == 0:
|
|
1326
|
+
buyer, seller = await self.users_bs()
|
|
1327
|
+
|
|
1328
|
+
# партнерские отчисления:
|
|
1329
|
+
if self.ad.maker.person.user.status == UserStatus.HOT_PARTNER:
|
|
1330
|
+
hspread = abs(self.ad.price - self.ad.pair_side.pair.cur.rate)
|
|
1331
|
+
earn = hspread * self.quantity * 0.0001
|
|
1332
|
+
fee = int(earn * (0.7 - self.ad.maker.person.user.bonus * 0.0001)) # 1% = 100
|
|
1333
|
+
b = await self.ad.maker.person.user.balance(self.ad.pair_side.pair.cur_id)
|
|
1334
|
+
if b < fee + (0 if self.ad.pair_side.is_sell else self.amount):
|
|
1335
|
+
return b
|
|
1336
|
+
_txf = await self.ad.maker.person.user.send(self.ad.pair_side.pair.cur_id, 0, fee)
|
|
1337
|
+
|
|
1338
|
+
tx = await buyer.send(self.ad.pair_side.pair.cur_id, seller.id, self.amount)
|
|
1339
|
+
if isinstance(tx, int):
|
|
1340
|
+
logging.error("КАК БЛЯТЬ МЫ СЮДА ПОПАЛИ? Its unreal!")
|
|
1341
|
+
return tx
|
|
1342
|
+
if not self.id:
|
|
1343
|
+
logging.warning("No ID order payment")
|
|
1344
|
+
await self.save()
|
|
1345
|
+
tsf = await Transfer.create(
|
|
1346
|
+
amount=tx.amount, order_id=self.id, pmid=tx.id.hex, sender_acc=buyer.username_id
|
|
1347
|
+
)
|
|
1348
|
+
self.status = OrderStatus.paid # todo: запрещать ставить paid, если у ордера нет трансферов на сумму amount
|
|
1349
|
+
await self.save(update_fields=["status", "updated_at"])
|
|
1350
|
+
return tsf
|
|
1351
|
+
|
|
1352
|
+
raise NotImplementedError("Pay method implemented now only for XyncPay pm")
|
|
1353
|
+
|
|
1354
|
+
async def cancel_request(self):
|
|
1355
|
+
if not (payments := await self.fetch_related("transfers")):
|
|
1356
|
+
self.status = OrderStatus.canceled
|
|
1357
|
+
await self.save(update_fields=["status", "updated_at"])
|
|
1358
|
+
return {"err": 0}
|
|
1359
|
+
return {"err": payments}
|
|
1360
|
+
|
|
1361
|
+
# def epyd(self): # todo: for who?
|
|
1362
|
+
# module_name = f"xync_client.{self.ex.name}.pyd"
|
|
1363
|
+
# __import__(module_name)
|
|
1364
|
+
# return sys.modules[module_name].OrderEpyd
|
|
1365
|
+
|
|
1366
|
+
class Meta:
|
|
1367
|
+
table_description = "P2P Orders"
|
|
1368
|
+
# unique_together = (("exid", "ad_id"),)
|
|
1369
|
+
|
|
1370
|
+
class PydanticMeta(Model.PydanticMeta):
|
|
1371
|
+
max_recursion: int = 0
|
|
1372
|
+
exclude_raw_fields: bool = False
|
|
1373
|
+
exclude = ("taker", "ad", "cred", "msgs")
|
|
1374
|
+
|
|
1375
|
+
|
|
1376
|
+
class Task(Model):
|
|
1377
|
+
order: ForeignKeyRelation[Order] = ForeignKeyField("models.Order", "tasks", on_update=CASCADE)
|
|
1378
|
+
typ: TaskType = IntEnumField(TaskType)
|
|
1379
|
+
data: dict = JSONField()
|
|
1380
|
+
|
|
1381
|
+
|
|
1382
|
+
class Transaction(Model):
|
|
1383
|
+
id: UUID = UUIDField(primary_key=True, default=None)
|
|
1384
|
+
amount: int = UInt4Field() # /10^cur.scale
|
|
1385
|
+
cur: ForeignKeyRelation[Cur] = ForeignKeyField("models.Cur", "transfers", on_update=CASCADE)
|
|
1386
|
+
cur_id: int
|
|
1387
|
+
sender: ForeignKeyRelation[User] = ForeignKeyField("models.User", "sends", on_update=CASCADE, null=True)
|
|
1388
|
+
sender_id: int | None
|
|
1389
|
+
receiver: ForeignKeyRelation[User] = ForeignKeyField("models.User", "receives", on_update=CASCADE)
|
|
1390
|
+
receiver_id: int
|
|
1391
|
+
# validator: ForeignKeyRelation[User] = ForeignKeyField("models.User", "validates", null=True)
|
|
1392
|
+
# validator_id: int
|
|
1393
|
+
status: TS = IntEnumField(TS)
|
|
1394
|
+
proof: bytes = UniqBinaryField(unique=True, null=True) # len=128
|
|
1395
|
+
ts: int | None = IntField(null=True, db_index=True)
|
|
1396
|
+
|
|
1397
|
+
def is_expired(self) -> bool:
|
|
1398
|
+
"""Проверка истёк ли запрос"""
|
|
1399
|
+
assert not self.proof and self.ts, "It is method for ttl requests only"
|
|
1400
|
+
return time() > self.ts
|
|
1401
|
+
|
|
1402
|
+
def is_valid_sender(self, sender_id: int) -> bool:
|
|
1403
|
+
"""Проверка может ли отправитель оплатить этот запрос"""
|
|
1404
|
+
assert self.proof is None, "It is method for requests only"
|
|
1405
|
+
return self.sender_id is None or self.sender_id == sender_id
|
|
1406
|
+
|
|
1407
|
+
@property
|
|
1408
|
+
def pack(self) -> bytes:
|
|
1409
|
+
"""Упаковка тип+время+валюта+получатель+сумма+отправитель в 4+(1+3+5)+3=16 байт"""
|
|
1410
|
+
|
|
1411
|
+
def tt(): # тип + ts (2+30=32bit=4byte)
|
|
1412
|
+
"""Упаковка тип+время в 4 байта"""
|
|
1413
|
+
typ: LinkType = LinkType.request if self.status == TS.request else LinkType.receipt
|
|
1414
|
+
return typ_ts_pack(typ.value, self.ts)
|
|
1415
|
+
|
|
1416
|
+
def cr(cur_id: int, rec_id: int):
|
|
1417
|
+
"""Упаковка валюта + получатель в 1+3=4 байт"""
|
|
1418
|
+
rcv = struct.pack(">I", rec_id)[1:4] # последние 3 байта от 4х-байтного идшника
|
|
1419
|
+
return struct.pack(">B", cur_id) + rcv
|
|
1420
|
+
|
|
1421
|
+
def cra(cur_id: int, rec_id: int, amount: int):
|
|
1422
|
+
"""Упаковка валюта + получатель + сумма в (1+3)+5=9 байт"""
|
|
1423
|
+
amt = struct.pack(">Q", amount)[3:8] # последние 5 байт от 8-байтной суммы (max 1_099_511_627_775)
|
|
1424
|
+
return cr(cur_id, rec_id) + amt
|
|
1425
|
+
|
|
1426
|
+
def ttcras():
|
|
1427
|
+
assert self.sender_id < 1 << 24
|
|
1428
|
+
sndr = struct.pack(">I", self.sender_id)[1:4] # последние 3 байта от 4х-байтного идшника
|
|
1429
|
+
return tt() + cra(self.cur_id, self.receiver_id, self.amount) + sndr
|
|
1430
|
+
|
|
1431
|
+
# если это запрос, то все, пруфа нет и не нужен
|
|
1432
|
+
proof_len = len(self.proof or b"")
|
|
1433
|
+
if self.status == TS.request:
|
|
1434
|
+
assert not proof_len, f"no proof `{self.proof}` for request"
|
|
1435
|
+
# return cra(
|
|
1436
|
+
# self.cur_id, # 1
|
|
1437
|
+
# self.receiver_id, # 4
|
|
1438
|
+
# self.amount, # 4
|
|
1439
|
+
# )
|
|
1440
|
+
# иначе это отправка
|
|
1441
|
+
elif not self.ts:
|
|
1442
|
+
raise ValueError("Time of sending is required for transaction")
|
|
1443
|
+
elif (self.status == TS.signed and proof_len != 64) or (self.status == TS.verified and proof_len != 128):
|
|
1444
|
+
raise ValueError(f"wrong proof length `{proof_len}` for `{self.status.name}` transaction")
|
|
1445
|
+
return ttcras()
|
|
1446
|
+
|
|
1447
|
+
async def can_be_send(self) -> bool:
|
|
1448
|
+
if not self.sender_id:
|
|
1449
|
+
return True
|
|
1450
|
+
assert isinstance(self.sender, User), "Sender is not fetched!"
|
|
1451
|
+
sender_balance = await self.sender.balance(self.cur_id, self.ts)
|
|
1452
|
+
is_in_db = await Transaction.exists(id=self.id, status__gte=TS.signed)
|
|
1453
|
+
if not (res := sender_balance >= (0 if is_in_db else self.amount)):
|
|
1454
|
+
for ttx in await Transaction.filter(id__not=self.id, ts=self.ts, receiver=self.sender, status=TS.signed):
|
|
1455
|
+
await ttx.approve()
|
|
1456
|
+
res = await self.can_be_send()
|
|
1457
|
+
return res
|
|
1458
|
+
|
|
1459
|
+
async def sign(self, front_sign: bytes = None) -> "Transaction | int":
|
|
1460
|
+
assert isinstance(self.sender, User), "Sender is not fetched!"
|
|
1461
|
+
back_sign = self.sender.prv and Ed25519PrivateKey.from_private_bytes(self.sender.prv).sign(self.pack)
|
|
1462
|
+
if self.sender.prv and front_sign: # if fronted sign passed, and sender has backend priv key, check the match
|
|
1463
|
+
if front_sign != back_sign:
|
|
1464
|
+
raise InvalidSignature(front_sign, "old_front_pub")
|
|
1465
|
+
elif not front_sign: # if fronted sign not passed, use back sign
|
|
1466
|
+
self.proof = back_sign
|
|
1467
|
+
self.status = TS.signed
|
|
1468
|
+
# xync verify tx
|
|
1469
|
+
await self.approve()
|
|
1470
|
+
# validator = await self.sender.get_validator(amount, cur_id, rcv_id)
|
|
1471
|
+
return self
|
|
1472
|
+
|
|
1473
|
+
async def approve(self, prv: bytes = None):
|
|
1474
|
+
assert self.status == TS.signed, "It is method for signed transfers only"
|
|
1475
|
+
# assert len(self.proof) == 64, "Sender did not signed yet"
|
|
1476
|
+
# 1. Проверяем подпись отправителя
|
|
1477
|
+
await self.fetch_related("sender")
|
|
1478
|
+
snd_pub = Ed25519PublicKey.from_public_bytes(self.sender.pub)
|
|
1479
|
+
# отправитель подписывал транзу когда в ней еще не было пруфа, поэтому проверяем обрезок пака до него
|
|
1480
|
+
snd_pub.verify(self.proof[:64], self.id.bytes)
|
|
1481
|
+
# 2. Проверяем что баланс отправителя не меньшы отправляемой суммы. # todo: remove origin hack
|
|
1482
|
+
assert await self.can_be_send(), "Less sender balance"
|
|
1483
|
+
# 3. Подписываем
|
|
1484
|
+
prv = prv or (await User[0]).prv # or (await self.receiver).prv
|
|
1485
|
+
pk = Ed25519PrivateKey.from_private_bytes(prv) # ключем Xync-a, если не был передан другой
|
|
1486
|
+
self.proof += pk.sign(
|
|
1487
|
+
self.id.bytes + self.proof
|
|
1488
|
+
) # ид транзы + пруф отправителя и добавляем эту подпись к пруфу 64+64=128
|
|
1489
|
+
self.status = TS.verified
|
|
1490
|
+
# Только две подписи: отправителя + валидатора (128 байт)
|
|
1491
|
+
try:
|
|
1492
|
+
await self.save(**dict(update_fields=["proof", "status"]) if self._saved_in_db else {})
|
|
1493
|
+
except RaiseError as e:
|
|
1494
|
+
re = PgRaiseError(e)
|
|
1495
|
+
raise re
|
|
1496
|
+
|
|
1497
|
+
def check(self, vld_pub: bytes = None) -> bool:
|
|
1498
|
+
"""Проверка доказательства"""
|
|
1499
|
+
if len(self.proof) != 128: # 64 + 64
|
|
1500
|
+
return False # wrong size
|
|
1501
|
+
sender_sig, vld_sig = self.proof[:64], self.proof[64:128]
|
|
1502
|
+
try:
|
|
1503
|
+
# Проверяем подпись отправителя
|
|
1504
|
+
sender_pub = Ed25519PublicKey.from_public_bytes(self.sender.pub)
|
|
1505
|
+
sender_pub.verify(sender_sig, self.id.bytes)
|
|
1506
|
+
# Проверяем подпись валидатора
|
|
1507
|
+
if vld_pub:
|
|
1508
|
+
vld_pub = Ed25519PublicKey.from_public_bytes(vld_pub)
|
|
1509
|
+
vld_pub.verify(vld_sig, self.id.bytes + sender_sig)
|
|
1510
|
+
return True
|
|
1511
|
+
except InvalidSignature as _:
|
|
1512
|
+
return False # wrong sign
|
|
1513
|
+
|
|
1514
|
+
def get_qr(self, bot_nick: str) -> bytes:
|
|
1515
|
+
data = f"{bot_nick and 't.me/' + bot_nick + '?startapp='}{int.from_bytes(self.id.bytes + self.proof)}"
|
|
1516
|
+
# Prepare text
|
|
1517
|
+
typ = ("Personalized" if self.sender_id else "Opened") if self.status == TS.request else "Receipt"
|
|
1518
|
+
text = f"{typ} {self.status.name}"
|
|
1519
|
+
return qr_gen(data, text)
|
|
1520
|
+
|
|
1521
|
+
@classmethod
|
|
1522
|
+
async def snapshot(cls, ts: int = None) -> dict[int, dict[int, int]]: # {user_id: {cur_id: balance}}
|
|
1523
|
+
conn = connections.get("default")
|
|
1524
|
+
r = await conn.execute_query_dict(
|
|
1525
|
+
"""
|
|
1526
|
+
SELECT user_id,
|
|
1527
|
+
cur_id::int,
|
|
1528
|
+
SUM(total)::int AS balance
|
|
1529
|
+
FROM (SELECT receiver_id AS user_id, t.cur_id, SUM(amount) AS total
|
|
1530
|
+
|
|
1531
|
+
FROM transaction t -- deposits
|
|
1532
|
+
WHERE status = 2 -- verified
|
|
1533
|
+
AND ts <= $1
|
|
1534
|
+
GROUP BY receiver_id, t.cur_id
|
|
1535
|
+
|
|
1536
|
+
UNION ALL
|
|
1537
|
+
|
|
1538
|
+
SELECT sender_id AS user_id, t.cur_id, -SUM(amount) AS total
|
|
1539
|
+
FROM transaction t -- credits
|
|
1540
|
+
WHERE status in (1, 2) -- signed+
|
|
1541
|
+
AND sender_id IS NOT NULL -- исключаем Request-ы денег
|
|
1542
|
+
AND ts <= $1
|
|
1543
|
+
GROUP BY sender_id, t.cur_id) a
|
|
1544
|
+
|
|
1545
|
+
GROUP BY user_id, cur_id
|
|
1546
|
+
HAVING SUM(total) != 0
|
|
1547
|
+
ORDER BY user_id, cur_id;
|
|
1548
|
+
""",
|
|
1549
|
+
[ts or int(time())],
|
|
1550
|
+
)
|
|
1551
|
+
result: dict[int, dict[int, int]] = {}
|
|
1552
|
+
for row in r:
|
|
1553
|
+
result.setdefault(row["user_id"], {})[row["cur_id"]] = row["balance"]
|
|
1554
|
+
return result
|
|
1555
|
+
|
|
1556
|
+
|
|
1557
|
+
class Sign(Model):
|
|
1558
|
+
transaction: ForeignKeyRelation[Transaction] = ForeignKeyField("models.Transaction", "signs", on_update=CASCADE)
|
|
1559
|
+
transaction_id: int
|
|
1560
|
+
validator: ForeignKeyRelation[User] = ForeignKeyField("models.User", "approves", on_update=CASCADE)
|
|
1561
|
+
validator_id: int
|
|
1562
|
+
created_at: datetime | None = DatetimeSecField(auto_now_add=True)
|
|
1563
|
+
|
|
1564
|
+
|
|
1565
|
+
# class Checkpoint(Model):
|
|
1566
|
+
# amount: int = BigIntField() # /10^cur.scale
|
|
1567
|
+
# cur: ForeignKeyRelation[Cur] = ForeignKeyField("models.Cur", related_name="transfers")
|
|
1568
|
+
# cur_id: int
|
|
1569
|
+
# user: ForeignKeyRelation[User] = ForeignKeyField("models.User", "checkpoints", null=True)
|
|
1570
|
+
# user_id: int | None
|
|
1571
|
+
#
|
|
1572
|
+
# class Meta:
|
|
1573
|
+
# unique_together = (("user_id", "cur_id"),)
|
|
1574
|
+
|
|
1575
|
+
|
|
1576
|
+
class Transfer(Model):
|
|
1577
|
+
amount: int = UInt4Field() # /10^cur.scale
|
|
1578
|
+
order: ForeignKeyRelation[Order] = ForeignKeyField("models.Order", "transfers", on_update=CASCADE, null=True)
|
|
1579
|
+
order_id: int
|
|
1580
|
+
file: OneToOneNullableRelation["File"] = OneToOneField("models.File", "transfer", on_update=CASCADE, null=True)
|
|
1581
|
+
file_id: int
|
|
1582
|
+
pmid: str = CharField(32, unique=True, null=True)
|
|
1583
|
+
sender_acc: str = CharField(63, null=True)
|
|
1584
|
+
# pm: ForeignKeyRelation["Pm"] = ForeignKeyField("models.Pm", "transfers", on_update=CASCADE)
|
|
1585
|
+
# pm_id: int
|
|
1586
|
+
updated_at: datetime | None = DatetimeSecField(auto_now=True)
|
|
1587
|
+
|
|
1588
|
+
# class Meta:
|
|
1589
|
+
# unique_together = (("pmid", "pm_id"),)
|
|
1590
|
+
|
|
1591
|
+
|
|
1592
|
+
class Msg(Model):
|
|
1593
|
+
tg_mid: int = UInt4Field(null=True)
|
|
1594
|
+
txt: str = CharField(4095, null=True)
|
|
1595
|
+
read: bool = BooleanField(default=False, db_index=True)
|
|
1596
|
+
to_maker: bool = BooleanField()
|
|
1597
|
+
file: OneToOneNullableRelation["File"] = OneToOneField("models.File", "msg", on_update=CASCADE, null=True)
|
|
1598
|
+
order: ForeignKeyRelation[Order] = ForeignKeyField("models.Order", "msgs", on_update=CASCADE)
|
|
1599
|
+
sent_at: datetime | None = DatetimeSecField()
|
|
1600
|
+
|
|
1601
|
+
# todo: required txt or file
|
|
1602
|
+
class PydanticMeta(Model.PydanticMeta):
|
|
1603
|
+
max_recursion: int = 0
|
|
1604
|
+
exclude_raw_fields: bool = False
|
|
1605
|
+
exclude = ("receiver", "order")
|
|
1606
|
+
|
|
1607
|
+
class Meta:
|
|
1608
|
+
unique_together = (
|
|
1609
|
+
("order", "txt"),
|
|
1610
|
+
# ("order", "sent_at"),
|
|
1611
|
+
)
|
|
1612
|
+
|
|
1613
|
+
|
|
1614
|
+
class TopUpAble(Model):
|
|
1615
|
+
pm: OneToOneRelation[Pm] = OneToOneField("models.Pm", "topupable", on_update=CASCADE)
|
|
1616
|
+
pm_id: int
|
|
1617
|
+
url: str = CharField(127)
|
|
1618
|
+
auth: dict = JSONField(default={})
|
|
1619
|
+
fee: int = SmallIntField(default=0) # /10_000
|
|
1620
|
+
|
|
1621
|
+
|
|
1622
|
+
class TopUp(Model):
|
|
1623
|
+
topupable: ForeignKeyRelation[TopUpAble] = ForeignKeyField("models.TopUpAble", "topup", on_update=CASCADE)
|
|
1624
|
+
topupable_id: int
|
|
1625
|
+
pmid: str = CharField(36, unique=True, null=True)
|
|
1626
|
+
tid: str = CharField(36, unique=True, null=True)
|
|
1627
|
+
amount: int = UInt4Field() # /10^cur.scale
|
|
1628
|
+
cur: ForeignKeyRelation[Cur] = ForeignKeyField("models.Cur", "topups", on_update=CASCADE)
|
|
1629
|
+
cur_id: int
|
|
1630
|
+
user: ForeignKeyRelation[User] = ForeignKeyField("models.User", "topups", on_update=CASCADE)
|
|
1631
|
+
user_id: int
|
|
1632
|
+
created_at: datetime | None = DatetimeSecField(auto_now_add=True)
|
|
1633
|
+
completed_at: datetime | None = DatetimeSecField(null=True)
|
|
1634
|
+
|
|
1635
|
+
|
|
1636
|
+
class Dep(Model, TsTrait):
|
|
1637
|
+
pid: str = CharField(31) # product_id
|
|
1638
|
+
apr: int = UInt2Field() # /10_000
|
|
1639
|
+
fee: int | None = UInt2Field(null=True) # /10_000
|
|
1640
|
+
apr_is_fixed: bool = BooleanField(default=False)
|
|
1641
|
+
duration: int | None = UInt2Field(null=True)
|
|
1642
|
+
early_redeem: bool | None = BooleanField(null=True)
|
|
1643
|
+
typ: DepType = IntEnumField(DepType)
|
|
1644
|
+
# mb: renewable?
|
|
1645
|
+
min_limit: float = UInt8Field()
|
|
1646
|
+
max_limit: float | None = UInt8Field(null=True)
|
|
1647
|
+
is_active: bool = BooleanField(default=True)
|
|
1648
|
+
|
|
1649
|
+
coin: ForeignKeyRelation[Coin] = ForeignKeyField("models.Coin", "deps", on_update=CASCADE)
|
|
1650
|
+
coin_id: int
|
|
1651
|
+
reward_coin: ForeignKeyRelation[Coin] = ForeignKeyField("models.Coin", "deps_reward", on_update=CASCADE, null=True)
|
|
1652
|
+
reward_coin_id: int | None = None
|
|
1653
|
+
bonus_coin: ForeignKeyRelation[Coin] = ForeignKeyField("models.Coin", "deps_bonus", on_update=CASCADE, null=True)
|
|
1654
|
+
bonus_coin_id: int | None = None
|
|
1655
|
+
ex: ForeignKeyRelation[Ex] = ForeignKeyField("models.Ex", "deps", on_update=CASCADE)
|
|
1656
|
+
ex_id: int
|
|
1657
|
+
investments: BackwardFKRelation[Investment]
|
|
1658
|
+
|
|
1659
|
+
_icon = "seeding"
|
|
1660
|
+
_name = {"pid"}
|
|
1661
|
+
|
|
1662
|
+
def repr(self):
|
|
1663
|
+
return (
|
|
1664
|
+
f"{self.coin.ticker}:{self.apr * 100:.3g}% "
|
|
1665
|
+
f"{f'{self.duration}d' if self.duration and self.duration > 0 else 'flex'}"
|
|
1666
|
+
)
|
|
1667
|
+
|
|
1668
|
+
class Meta:
|
|
1669
|
+
table_description = "Investment products"
|
|
1670
|
+
unique_together = (("pid", "typ", "ex_id"),)
|
|
1671
|
+
|
|
1672
|
+
|
|
1673
|
+
class Investment(Model, TsTrait):
|
|
1674
|
+
dep: ForeignKeyRelation[Dep] = ForeignKeyField("models.Dep", "investments", on_update=CASCADE)
|
|
1675
|
+
# dep_id: int
|
|
1676
|
+
amount: float = UInt8Field()
|
|
1677
|
+
is_active: bool = BooleanField(default=True)
|
|
1678
|
+
user: ForeignKeyRelation[User] = ForeignKeyField("models.User", "investments", on_update=CASCADE)
|
|
1679
|
+
|
|
1680
|
+
_icon = "trending-up"
|
|
1681
|
+
_name = {"dep__pid", "amount"}
|
|
1682
|
+
|
|
1683
|
+
def repr(self):
|
|
1684
|
+
return f"{self.amount:.3g} {self.dep.repr()}"
|
|
1685
|
+
|
|
1686
|
+
class Meta:
|
|
1687
|
+
table_description = "Investments"
|
|
1688
|
+
|
|
1689
|
+
|
|
1690
|
+
class ExStat(Model):
|
|
1691
|
+
ex: ForeignKeyRelation[Ex] = ForeignKeyField("models.Ex", "stats", on_update=CASCADE)
|
|
1692
|
+
ex_id: int
|
|
1693
|
+
action: ExAction = IntEnumField(ExAction)
|
|
1694
|
+
ok: bool | None = BooleanField(default=False, null=True)
|
|
1695
|
+
updated_at: datetime | None = DatetimeSecField(auto_now=True)
|
|
1696
|
+
|
|
1697
|
+
_icon = "test-pipe"
|
|
1698
|
+
_name = {"ex_id", "action", "ok"}
|
|
1699
|
+
|
|
1700
|
+
def repr(self):
|
|
1701
|
+
return f"{self.ex_id} {self.action.name} {self.ok}"
|
|
1702
|
+
|
|
1703
|
+
class Meta:
|
|
1704
|
+
table = "ex_stat"
|
|
1705
|
+
table_description = "Ex Stats"
|
|
1706
|
+
unique_together = (("action", "ex_id"),)
|
|
1707
|
+
|
|
1708
|
+
class PydanticMeta(Model.PydanticMeta):
|
|
1709
|
+
max_recursion: int = 2
|
|
1710
|
+
|
|
1711
|
+
|
|
1712
|
+
class Vpn(Model):
|
|
1713
|
+
user: OneToOneRelation[User] = OneToOneField("models.User", "vpn", on_update=CASCADE)
|
|
1714
|
+
user_id: int
|
|
1715
|
+
prv: str = CharField(63, unique=True)
|
|
1716
|
+
pub: str = CharField(63, unique=True)
|
|
1717
|
+
created_at: datetime | None = DatetimeSecField(auto_now_add=True)
|
|
1718
|
+
|
|
1719
|
+
_icon = "vpn"
|
|
1720
|
+
_name = {"pub"}
|
|
1721
|
+
|
|
1722
|
+
def repr(self):
|
|
1723
|
+
return self.user.username
|
|
1724
|
+
|
|
1725
|
+
class Meta:
|
|
1726
|
+
table_description = "VPNs"
|
|
1727
|
+
|
|
1728
|
+
|
|
1729
|
+
class File(Model):
|
|
1730
|
+
name: str = CharField(178, null=True, unique=True)
|
|
1731
|
+
typ: FileType = IntEnumField(FileType)
|
|
1732
|
+
ref: bytes = UniqBinaryField(unique=True)
|
|
1733
|
+
size: bytes = UInt4Field()
|
|
1734
|
+
created_at: datetime | None = DatetimeSecField(auto_now_add=True)
|
|
1735
|
+
|
|
1736
|
+
msg: BackwardOneToOneRelation[Msg]
|
|
1737
|
+
transfer: BackwardOneToOneRelation[Transfer]
|
|
1738
|
+
pmex_logos: BackwardFKRelation[PmEx]
|
|
1739
|
+
|
|
1740
|
+
_icon = "file"
|
|
1741
|
+
_name = {"name"}
|
|
1742
|
+
|
|
1743
|
+
class Meta:
|
|
1744
|
+
table_description = "Files"
|
|
1745
|
+
# Создаем индекс через raw SQL
|
|
1746
|
+
# indexes = [CREATEUNIQUE INDEX IF NOT EXISTS idx_bytea_unique ON file (encode(sha256(ref), 'hex'))"]
|
|
1747
|
+
|
|
1748
|
+
|
|
1749
|
+
class Invite(Model, TsTrait):
|
|
1750
|
+
ref: ForeignKeyRelation[User] = ForeignKeyField("models.User", "invite_approvals", on_update=CASCADE)
|
|
1751
|
+
ref_id: int
|
|
1752
|
+
protege: ForeignKeyRelation[User] = ForeignKeyField("models.User", "invite_requests", on_update=CASCADE)
|
|
1753
|
+
protege_id: int
|
|
1754
|
+
approved: str = BooleanField(default=False) # status
|
|
1755
|
+
|
|
1756
|
+
_icon = "invite"
|
|
1757
|
+
_name = {"ref__username", "protege__username", "approved"}
|
|
1758
|
+
|
|
1759
|
+
def repr(self):
|
|
1760
|
+
return self.protege.name
|
|
1761
|
+
|
|
1762
|
+
class Meta:
|
|
1763
|
+
table_description = "Invites"
|
|
1764
|
+
|
|
1765
|
+
|
|
1766
|
+
class Credit(Model, TsTrait):
|
|
1767
|
+
lender: ForeignKeyRelation[User] = ForeignKeyField("models.User", "lends", on_update=CASCADE)
|
|
1768
|
+
lender_id: int
|
|
1769
|
+
borrower: ForeignKeyRelation[User] = ForeignKeyField("models.User", "borrows", on_update=CASCADE)
|
|
1770
|
+
borrower_id: int
|
|
1771
|
+
borrower_priority: bool = BooleanField(default=True)
|
|
1772
|
+
amount: int = UInt4Field(default=None) # 0 - is all remain borrower balance
|
|
1773
|
+
|
|
1774
|
+
_icon = "credit"
|
|
1775
|
+
_name = {"lender__username", "borrower__username", "amount"}
|
|
1776
|
+
|
|
1777
|
+
def repr(self):
|
|
1778
|
+
return self.borrower.name
|
|
1779
|
+
|
|
1780
|
+
class Meta:
|
|
1781
|
+
table_description = "Credits"
|