xync-db 0.0.2__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 ADDED
@@ -0,0 +1,28 @@
1
+ from os import getenv as env
2
+
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ TORM = {
8
+ "connections": {"default": env("DB_URL")},
9
+ "apps": {"models": {"models": ["xync_db.models"]}},
10
+ "use_tz": False,
11
+ "timezone": "UTC",
12
+ }
13
+
14
+
15
+ async def main():
16
+ import logging
17
+ from logging import DEBUG
18
+
19
+ from x_model import init_db
20
+
21
+ logging.basicConfig(level=DEBUG)
22
+ await init_db(TORM, create_tables=True)
23
+
24
+
25
+ if __name__ == "__main__":
26
+ from asyncio import run
27
+
28
+ run(main())
xync_db/config.py ADDED
@@ -0,0 +1,18 @@
1
+ """Tortoise ORM configuration. Used by the CLI and by service lifecycle hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ POSTGRES_DSN = os.getenv("POSTGRES_DSN", "postgres://xync:xync@localhost:5432/xync")
8
+
9
+ TORTOISE_ORM: dict = {
10
+ "connections": {"default": POSTGRES_DSN},
11
+ "apps": {
12
+ "models": {
13
+ "models": ["xync_db.models", "xync_db.migrations.models"],
14
+ "default_connection": "default",
15
+ "migrations": "xync_db.migrations.models",
16
+ },
17
+ },
18
+ }
xync_db/enums.py ADDED
@@ -0,0 +1,311 @@
1
+ from enum import IntEnum
2
+
3
+ ZERO_TS = 1770000000 # Feb 02 2026 02:40:00 (UTC)
4
+
5
+
6
+ class PersonStatus(IntEnum):
7
+ VIP = 2
8
+ DEFAULT = 1
9
+ ANONYMOUS = 0
10
+ USER_DEF = -1
11
+ BLOCKED = -2
12
+
13
+
14
+ class Side(IntEnum):
15
+ BUY = 0
16
+ SALE = 1
17
+
18
+
19
+ class PriceType(IntEnum):
20
+ FIX = 0
21
+ FLOAT = 1
22
+
23
+
24
+ class TransactionStatus(IntEnum):
25
+ request = 0 # no proof
26
+ signed = 1 # in proof: sender sign for transfer
27
+ verified = 2 # in proof: validator sign for transfer is valid
28
+
29
+
30
+ class LinkType(IntEnum):
31
+ ref = 0 # open or trusted
32
+ request = 1 # open or personalized
33
+ receipt = 2 # signed or verified
34
+ contract = 3 # todo
35
+
36
+
37
+ class UserStatus(IntEnum):
38
+ SLEEP = 0
39
+ PAY = 1
40
+ HOT_PARTNER = 2 # partner
41
+ XYNC_TEAM = 3
42
+
43
+
44
+ class AdStatus(IntEnum):
45
+ defActive = 0
46
+ active = 1
47
+ soldOut = 2
48
+ old = 3
49
+ notFound = 9
50
+
51
+
52
+ class HotStatus(IntEnum):
53
+ closed = 0
54
+ opened = 1
55
+
56
+
57
+ class OrderStatus(IntEnum):
58
+ created = 1
59
+ paid = 2
60
+ completed = 3
61
+ cancel_requested_by_seller = 4
62
+ canceled = 5
63
+ appealed_by_seller = 6
64
+ appealed_by_buyer = 7
65
+ appeal_disputed = 8
66
+ # todo: 8T - один вирт экшн на бездействие обеих сторон, а 12T и 13T - по отдельности?
67
+ # COMPLETED, PENDING, TRADING, BUYER_PAYED, DISTRIBUTING, COMPLETED, IN_APPEAL, CANCELLED, CANCELLED_BY_SYSTEM
68
+
69
+
70
+ class OrderErr(IntEnum):
71
+ ok = 0
72
+ i_wait_for_release = 1
73
+ payments_exists_on_cancel = 2
74
+ taker_balance_deficit = 3
75
+ # negative results is for agent
76
+ maker_balance_deficit = -4
77
+ uncompleted_orders = 5
78
+ wrong_status_for_hot = -6
79
+ cant_cancel = 7
80
+
81
+
82
+ class OrderAction(IntEnum):
83
+ # requested
84
+ taker_create = 1
85
+ wait_for_cancel = 0
86
+ # created
87
+ buyer_pay = 2
88
+ seller_confirm = 3
89
+ buyer_cancel = 4
90
+ # paid
91
+ wait_for_pay = 5
92
+ seller_appeal = 6
93
+ buyer_appeal = 7
94
+ # appealed by seller
95
+ buyer_dispute_appeal_s = 8
96
+ buyer_agree_appeal_s = 10 # cancel
97
+ wait_appeal_s = 12 # -> cancel
98
+ # appealed by buyer
99
+ seller_dispute_appeal_b = 9
100
+ seller_agree_appeal_b = 11 # confirm
101
+ wait_appeal_b = 13 # -> confirm
102
+ wait_disputed_appeal = 14 # ?
103
+
104
+
105
+ class CoinType(IntEnum):
106
+ normal = 1
107
+ game = 2
108
+ corp = 3
109
+ stock = 4
110
+
111
+
112
+ class ExType(IntEnum):
113
+ p2p = 1
114
+ cex = 2
115
+ main = 3 # p2p+cex
116
+ dex = 4
117
+ tg = 5
118
+ futures = 8
119
+
120
+
121
+ class DepType(IntEnum):
122
+ earn = 1
123
+ stake = 2
124
+ beth = 3
125
+ lend = 4
126
+
127
+
128
+ class AddrExType(IntEnum):
129
+ spot = 1
130
+ earn = 2
131
+ found = 3
132
+
133
+
134
+ class TradeType(IntEnum):
135
+ BUY = 0
136
+ SELL = 1
137
+
138
+
139
+ class PmType(IntEnum):
140
+ common = 0
141
+ bank_acc = 1 # no. or IBAN
142
+ emoney = 2 # QR / phone-based
143
+ cash = 3 # ваучеры / агентские
144
+ card = 4 # fintech/processing
145
+ local = 5 # региональные: bank_routing-based: IFSC(Индия), CLABE(Мексика), банк.коды ЛатАм
146
+
147
+
148
+ class TaskType(IntEnum):
149
+ invite_approve = 1
150
+ order_user_define = 2
151
+
152
+
153
+ class FileType(IntEnum):
154
+ pdf = 1
155
+ jpg = 2
156
+ png = 3
157
+ webp = 4
158
+ mov = 5
159
+ mp4 = 6
160
+ gif = 7
161
+ svg = 8
162
+ tgs = 9
163
+ jpeg = 10
164
+
165
+
166
+ class AgentStatus(IntEnum):
167
+ off = 0
168
+ race = 1
169
+ listen = 2
170
+ race_listen = 3
171
+ # ... = 4
172
+
173
+
174
+ class SynonymType(IntEnum):
175
+ name = 1
176
+ ppo = 2
177
+ from_party = 3
178
+ to_party = 4
179
+ slip_req = 5
180
+ slip_send = 6
181
+ abuser = 7
182
+ scale = 8
183
+ slavic = 9
184
+ mtl_like = 10
185
+ bank = 11
186
+ bank_side = 12
187
+ sbp_strict = 13
188
+ contact = 14
189
+
190
+
191
+ class Boundary(IntEnum):
192
+ no = 0
193
+ left_word = 1
194
+ right_word = 2
195
+ both_word = 3
196
+ left_all = 4
197
+ right_all = 5
198
+ both_all = 6
199
+
200
+
201
+ class SbpStrict(IntEnum):
202
+ no = 0
203
+ sbp = 1
204
+ card = 2
205
+
206
+
207
+ class Party(IntEnum):
208
+ fst = 0
209
+ fam = 1
210
+ lk = 2 # lk-photo/video/comment
211
+ trd = 3
212
+
213
+
214
+ class Slip(IntEnum):
215
+ no = 0
216
+ screen = 1
217
+ pdf = 2
218
+ pdf_mail = 3
219
+
220
+
221
+ class NameType(IntEnum):
222
+ no_slavic = 0
223
+ slavic = 1
224
+ fake = 2
225
+
226
+
227
+ class AbuserType(IntEnum):
228
+ no = 0
229
+ rating = 1 # рейтинг: жмет "Оплачено" сразу, "Отмена" по апелляции
230
+ fake = 2 # реклама
231
+ cash = 3 # типа кэш обменник, но хз
232
+
233
+
234
+ class ExStatus(IntEnum):
235
+ plan = 0
236
+ parted = 1
237
+ full = 2
238
+
239
+
240
+ class ExAction(IntEnum):
241
+ """Order"""
242
+
243
+ get_orders = 0 # Получение заявок за заданное время, в статусе, по валюте, монете, направлению: `get_orders(status=OrderStatus.active, coin='USDT', cur='RUB', is_sell=False) => [order]`
244
+ order_request = 1 # [T] Запрос на старт сделки
245
+ order_request_ask = -1 # [M] - Запрос мейкеру на сделку
246
+ cancel_request = 2 # [T] Отмена запроса на сделку
247
+ request_canceled = -2 # [M] - Уведомление об отмене запроса на сделку
248
+ accept_request = 3 # [M] Одобрить запрос на сделку
249
+ request_accepted = -3 # [T] Уведомление об одобрении запроса на сделку
250
+ reject_request = 4 # [M] Отклонить запрос на сделку
251
+ request_rejected = -4 # [T] Уведомление об отклонении запроса на сделку
252
+ mark_payed = 5 # [B] Перевод сделки в состояние "оплачено", с отправкой чека
253
+ payed = -5 # [S] Уведомление продавца об оплате
254
+ cancel_order = 6 # [B] Отмена сделки
255
+ order_canceled = -6 # [S] Уведомление продавцу об отмене ордера покупателем
256
+ confirm = 7 # [S] Подтвердить получение оплаты
257
+ order_completed = -7 # [B] Уведомление покупателю об успешном завершении продавцом
258
+ appeal_available = -8 # [S, B] Уведомление о наступлении возможности подать апелляцию
259
+ start_appeal = 9 # , 10 # [S, B] Подать апелляцию со скриншотом/видео/файлом
260
+ appeal_started = -9 # , -10 # [S, B] Уведомление о поданной на меня апелляции
261
+ dispute_appeal = 11 # , 12 # [S, B] Встречное оспаривание полученной апелляции со скриншотом/видео/файлом
262
+ appeal_disputed = -11 # , -12 # [S, B] Уведомление о встречном оспаривание поданной апелляции
263
+ order_completed_by_appeal = -13 # [S, B] Уведомление о завершении сделки по апелляции
264
+ order_canceled_by_appeal = -14 # [B, S] Уведомление об отмене сделки по апелляции
265
+ cancel_appeal = 15 # [B, S] Отмена апелляции
266
+ appeal_canceled = -15 # [B, S] Уведомление об отмене апелляции против меня
267
+ send_order_msg = 16 # Отправка сообщения юзеру в чат по ордеру с приложенным файлом
268
+ got_order_msg = -16 # Получение сообщения в чате по ордеру
269
+ send_appeal_msg = 17 # Отправка сообщения по апелляции
270
+ got_appeal_msg = -17 # Получение сообщения по апелляции
271
+ """ Ex: Public """
272
+ # curs = 19 # Список поддерживаемых валют тейкера
273
+ pms = 20 # Список платежных методов по каждой валюте
274
+ cur_pms_map = 21 # Мэппинг валюта => платежные методы
275
+ coins = 22 # Список торгуемых монет (с ограничениями по валютам, если есть)
276
+ pairs = 23 # Список пар валюта/монет
277
+ ads = 24 # Список объяв по покупке/продаже, валюте, монете, платежному методу (buy/sell, cur, coin, pm)
278
+ set_coins = 44 # обновление монет биржи в бд
279
+ set_curs = 19 # обновление валют и платежек биржи в бд
280
+ set_pms = 45 # обновление валют и платежек биржи в бд
281
+ set_pairs = 46 # обновление пар биржи в бд
282
+ ad = 42 # Чужая объява по id
283
+ """ Agent: Fiat """
284
+ my_creds = 25 # Список реквизитов моих платежных методов
285
+ cred_new = 26 # Создание реквизита моего платежного метода
286
+ cred_upd = 27 # Редактирование реквизита моего платежного метода
287
+ cred_del = 28 # Удаление реквизита моего платежного метода
288
+ """ Agent: Ad """
289
+ my_ads = 29 # Список моих объявлений
290
+ my_ad = 43 # Моя объява по id
291
+ ad_new = 30 # Создание объявления
292
+ ad_upd = 31 # Редактирование объявления
293
+ ad_del = 32 # Удаление объявления
294
+ ad_switch = 33 # Вкл/выкл объявления
295
+ ads_switch = 34 # Вкл/выкл всех объявлений
296
+ """ Agent: Taker """
297
+ get_user = 35 # Получить объект юзера по его id
298
+ send_user_msg = 36 # Отправка сообщения юзеру с приложенным файлом
299
+ block_user = 37 # [Раз]Блокировать пользователя
300
+ rate_user = 38 # Поставить отзыв пользователю
301
+ """ Agent: Inbound """
302
+ got_user_msg = -36 # Получение сообщения от пользователя
303
+ got_blocked = -37 # Получение уведомления о [раз]блокировке пользователем
304
+ got_rated = -38 # Получение уведомления о полученном отзыве
305
+
306
+ """ Assets """
307
+ assets = 39 # Балансы моих монет
308
+ deposit = 40 # Получить реквизиты для депозита монеты
309
+ deposited = -40 # Получена монета
310
+ withdraw = 41 # Вывести монету
311
+ withdrew = -41 # Монета выведена
xync_db/exceptions.py ADDED
@@ -0,0 +1,18 @@
1
+ from enum import IntEnum
2
+ from json import loads
3
+
4
+ from asyncpg.exceptions import RaiseError
5
+
6
+
7
+ class ExcType(IntEnum):
8
+ InsufficientBalance = 1
9
+ TxIdPackMismatch = 2
10
+
11
+
12
+ class PgRaiseError(ValueError):
13
+ typ: ExcType
14
+
15
+ def __init__(self, e: RaiseError) -> None:
16
+ typ, msg = e.args[0].split(": ")
17
+ self.typ = ExcType[typ]
18
+ super().__init__(typ, loads(msg))
xync_db/graph.py ADDED
@@ -0,0 +1,65 @@
1
+ from xync_db.enums import OrderAction, OrderStatus
2
+
3
+ acts = (
4
+ { # buyer
5
+ # 1
6
+ OrderStatus.created: {
7
+ OrderAction.buyer_cancel: OrderStatus.canceled,
8
+ OrderAction.buyer_pay: OrderStatus.paid,
9
+ },
10
+ # 2
11
+ OrderStatus.paid: {
12
+ OrderAction.buyer_cancel: OrderStatus.canceled,
13
+ OrderAction.buyer_appeal: OrderStatus.appealed_by_buyer,
14
+ },
15
+ # 5
16
+ OrderStatus.appealed_by_seller: {
17
+ OrderAction.buyer_cancel: OrderStatus.canceled,
18
+ OrderAction.buyer_dispute_appeal_s: OrderStatus.appeal_disputed,
19
+ },
20
+ # 6
21
+ OrderStatus.appealed_by_buyer: {
22
+ OrderAction.buyer_cancel: OrderStatus.canceled,
23
+ },
24
+ # 7
25
+ OrderStatus.appeal_disputed: {
26
+ OrderAction.buyer_cancel: OrderStatus.canceled,
27
+ },
28
+ },
29
+ { # seller
30
+ # 2
31
+ OrderStatus.paid: {
32
+ OrderAction.seller_confirm: OrderStatus.completed,
33
+ OrderAction.seller_appeal: OrderStatus.appealed_by_seller,
34
+ },
35
+ # 5
36
+ OrderStatus.appealed_by_seller: {
37
+ OrderAction.seller_confirm: OrderStatus.completed,
38
+ },
39
+ # 6
40
+ OrderStatus.appealed_by_buyer: {
41
+ OrderAction.seller_dispute_appeal_b: OrderStatus.appeal_disputed,
42
+ OrderAction.seller_confirm: OrderStatus.completed,
43
+ },
44
+ # 7
45
+ OrderStatus.appeal_disputed: {
46
+ OrderAction.seller_confirm: OrderStatus.completed,
47
+ },
48
+ },
49
+ )
50
+
51
+
52
+ waits: dict[OrderStatus, dict[OrderAction, OrderStatus]] = {
53
+ # 1
54
+ OrderStatus.created: {
55
+ OrderAction.wait_for_cancel: OrderStatus.canceled,
56
+ },
57
+ # 5
58
+ OrderStatus.appealed_by_seller: {
59
+ OrderAction.wait_appeal_s: OrderStatus.canceled,
60
+ },
61
+ # 6
62
+ OrderStatus.appealed_by_buyer: {
63
+ OrderAction.wait_appeal_b: OrderStatus.completed,
64
+ },
65
+ }
xync_db/logo.png ADDED
Binary file
@@ -0,0 +1,125 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Overview
6
+
7
+ All Tortoise ORM models (~60) are defined in `__init__.py`. Models are organized into domain groups listed below.
8
+
9
+ ## Base Classes
10
+
11
+ - **`Model`** (from `x_auth.models`) — primary base with `_name`, `_icon`, `PydanticMeta` support
12
+ - **`BaseModel`** (tortoise) — used for through-tables (`CurEx`, `CoinEx`, `PmEx`, `PmExBank`) that don't need full model features
13
+ - **`TgUser`** (from `x_auth.models`) — Telegram user base, extended by `User`
14
+ - **`TsTrait`** (from `x_model.models`) — mixin adding auto `created_at`/`updated_at`
15
+
16
+ Re-exported models from `x_auth`: `Username`, `Proxy`, `Dc`, `Fcm`, `App`, `Session`, `Peer`, `UpdateState`, `Version`, `Country` (as `BaseCountry`).
17
+
18
+ ## Field Conventions
19
+
20
+ - **UInt fields**: `UInt1Field` (1 byte), `UInt2Field` (2), `UInt4Field` (4), `UInt8Field` (8) — custom unsigned integers from `x_model.field`
21
+ - **Monetary integers**: amounts stored as `int / 10^scale` where `scale` comes from the related `Cur` or `Coin`
22
+ - **Percentages**: stored as `/10_000` (basis points) — fields: `premium`, `fee`, `apr`
23
+ - **Binary**: `BinaryField` for keys/proofs, `UniqBinaryField` for unique binary (e.g., `File.ref`)
24
+ - **Timestamps**: `DatetimeSecField` for second-precision datetimes (`shared_at`, `payed_at`, etc.)
25
+ - **FK convention**: all `ForeignKeyField` / `OneToOneField` use `on_update=CASCADE`
26
+ - **Composite uniqueness**: via `Meta.unique_together`, commonly `(ex_id, exid)` for exchange-mapped entities
27
+
28
+ ## Model Attributes
29
+
30
+ - **`_name`**: set of field paths for string representation (supports `__` traversal, e.g. `"pair_side__pairex__coin__ticker"`)
31
+ - **`_icon`**: UI icon identifier (e.g. `"ad"`, `"seeding"`, `"file"`)
32
+ - **`repr()`**: human-readable formatting on select models (`Cred`, `Dep`, `Investment`, `ExStat`, `Vpn`, `Invite`, `Credit`)
33
+
34
+ ## Domain Groups
35
+
36
+ ### Currency & Exchange
37
+ `Cur` -> `CurEx` <- `Ex`, `Coin` -> `CoinEx` <- `Ex`, `Net`, `Pair` -> `PairSide` -> `PairEx`
38
+
39
+ - `Cur.id` = `UInt1Field` (max 255 fiat currencies)
40
+ - `Coin.id` = `SmallIntField`
41
+ - `Ex.id` = `UInt1Field` (max 255 exchanges)
42
+ - Through-tables (`CurEx`, `CoinEx`) carry `exid` (exchange's internal ID string) and exchange-specific `scale`/`minimum`
43
+
44
+ ### Users & Auth
45
+ `Person` -> `User` (extends `TgUser`), `Actor` -> `Agent`
46
+
47
+ - `Person` groups actors/credentials under a human identity; `status` via `PersonStatus`
48
+ - `User` holds Ed25519 keypair (`prv`/`pub` as raw bytes) for signing `Transaction`s
49
+ - `Actor` = exchange account `(ex_id, exid)` unique; links to `Person`
50
+ - `Agent` = authenticated session (one-to-one with `Actor`); holds `auth` JSON blob + expiry
51
+ - `Addr` = crypto deposit address `(coin_id, actor_id)` unique; linked to `Net`s via `NetAddr`
52
+ - `Asset` = balance snapshot `(addr_id, agent_id, typ)` unique; amounts `free`/`freeze`/`lock`/`target`
53
+
54
+ actor.set_user(username_id):
55
+ При создании юзера(User) в sql-триггере автоматически создается персона(Person) с его user_id.
56
+ А при создании актора(Actor) создается персона без user_id, но в actor.person_id = id этой персоны.
57
+ И когда юзер заходит в объявление одного из агентов(Agent) первый раз, создается его актор для биржи (на которой
58
+ размещено это объявление) и новая персона этого актора.
59
+ Если к персоне юзера еще не "привязано" ни одного актора, 2 персоны для юзера с одним актором избыточны, их нужно
60
+ смерджить: т.е старой персоне юзера назначить имя, креды(creds) и актора этой новой персоны, а новую "осиротевшую"
61
+ персону удалить.
62
+ Далее, если этот же юзер будет заходить с других акторов:
63
+ - других бирж, но с тем же именем: это та же персона нужно так же смерджить новую персону нового актора с существующей.
64
+ - с другим именем: значит это уже другая персона, и существующая персона(ы) уже "занята" существующим(и) актором(ами),
65
+ тогда уже не надо мержить новую персону нового актора, оставляем новую.
66
+
67
+ ### Payment Methods
68
+ `PmGroup` -> `Pm` -> `PmCur` <- `Cur`, `PmEx` <- `Ex`, `PmExBank`
69
+
70
+ - `Pm.norm` — normalized name (used in dynamic client path: `xync_client.Pms.{norm}.agent`)
71
+ - `Cred` = user's payment credential `(person_id, pmcur_id, ovr_pm_id)` unique
72
+ - `CredEx` = credential registered on exchange `(ex_id, exid)` unique
73
+ - `Fiat` = balance tracker (one-to-one with `Cred`): `amount`, `target`, `min_deposit`
74
+ - `PmAgent` = automation account for a PM `(pm_id, user_id)` unique
75
+
76
+ ### P2P Trading
77
+ `Ad` -> `MyAd` -> `Race` -> `RaceStat`, `Order` -> `Msg`/`Transfer`/`Task`, `Hot`
78
+
79
+ - `Ad` = exchange advertisement `(exid, maker_id)` unique; carries `price`, `premium`, `amount`, `min_fiat`/`max_fiat`, `status` (`AdStatus`), optional `Cond`
80
+ - `MyAd` = our managed ad (one-to-one with `Ad`); adds race tracking, share URLs, linked `CredEx`s
81
+ - `Order` = P2P trade `(exid)` unique; references `Ad`, `Cred`, taker/maker `Actor`s; `status` via `OrderStatus`
82
+ - `Hot` = warm-up trading session; many-to-many with `Actor`s and `Order`s
83
+ - `Msg` = order chat message with optional `File` attachment
84
+ - `Transfer` = order payment record with `amount`, optional `File` (receipt)
85
+
86
+ ### Conditions (Ad parsing)
87
+ `Cond` -> `CondParsed`, `Synonym`, `CondSim`
88
+
89
+ - `Cond.raw_txt` = original ad conditions text (4095 chars max, unique)
90
+ - `CondParsed` = structured parse result: `to_party`/`from_party` (`Party`), `ppo`, `slip_req`/`slip_send` (`Slip`), abuse flags, linked bank `Pm`s
91
+ - `Synonym` = condition term with `typ` (`SynonymType`), optional regex (`is_re`), boundary matching
92
+
93
+ ### Transactions (XyncPay)
94
+ `Transaction`, `Sign`
95
+
96
+ - `Transaction.id` = deterministic UUID packed from: `type(2bit) + ts(30bit) + cur_id(1byte) + receiver_id(3bytes) + amount(5bytes) + sender_id(3bytes)` = 16 bytes
97
+ - `Transaction.proof` = 128 bytes (64 sender signature + 64 validator signature), Ed25519
98
+ - `Transaction.status` progresses: `request` -> `signed` -> `verified`
99
+ - Key methods: `pack` (property), `sign(front_sign)`, `approve(prv)`, `check(vld_pub)`, `snapshot(ts)` (raw SQL)
100
+ - DB-level PL/pgSQL triggers enforce: balance checks on insert, immutability on update, no deletes
101
+
102
+ ### Financial Products
103
+ `Dep` -> `Investment`, `TopUpAble` -> `TopUp`
104
+
105
+ ### Management
106
+ `Limit`, `ExStat`, `Vpn`, `Invite`, `Credit`, `File`
107
+
108
+ ## Dynamic Client Loading
109
+
110
+ Models delegate exchange-specific logic to `xync_client` via runtime imports:
111
+
112
+ ```
113
+ Ex.client() -> xync_client.{ex.name}.ex.ExClient
114
+ Agent.client() -> xync_client.{ex.name}.agent.AgentClient
115
+ Actor.asset_client() -> xync_client.{ex.name}.asset.AssetClient
116
+ Order.client() -> xync_client.{ex.name}.order.OrderClient
117
+ Pm.client() -> xync_client.Pms.{pm.norm}
118
+ PmAgent.client() -> xync_client.Pms.{pm.norm}.agent
119
+ ```
120
+
121
+ ## Enums
122
+
123
+ All enums are in `xync_db/enums.py` (all `IntEnum`). Key ones: `AdStatus`, `OrderStatus`, `OrderAction`, `OrderErr`, `ExType`, `ExStatus`, `ExAction` (~50 actions), `PmType`, `TransactionStatus`, `PersonStatus`, `UserStatus`, `HotStatus`, `FileType`, `DepType`, `AgentStatus`.
124
+
125
+ `TransactionStatus` is imported as `TS` alias in this file.