xync-db 0.6.1.dev1__tar.gz → 0.8.0__tar.gz

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.
Files changed (42) hide show
  1. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/PKG-INFO +1 -1
  2. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/enums.py +20 -0
  3. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/0001_initial.py +169 -2
  4. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/0002_triggers.py +17 -0
  5. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/c_net_seed.py +26 -1
  6. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/models/__init__.py +112 -15
  7. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/.gitignore +0 -0
  8. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/README.md +0 -0
  9. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/pyproject.toml +0 -0
  10. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/__init__.py +0 -0
  11. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/config.py +0 -0
  12. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/exceptions.py +0 -0
  13. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/graph.py +0 -0
  14. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/i18n.py +0 -0
  15. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/login_store.py +0 -0
  16. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/logo.png +0 -0
  17. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/CLAUDE.md +0 -0
  18. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/__init__.py +0 -0
  19. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/0003_seed.py +0 -0
  20. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/__init__.py +0 -0
  21. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/__init__.py +0 -0
  22. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/a_actor_seed.py +0 -0
  23. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/a_bin_seed.py +0 -0
  24. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/a_city_seed.py +0 -0
  25. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/a_country_seed.py +0 -0
  26. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/a_tg_seed.py +0 -0
  27. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/b_htx_seed.py +0 -0
  28. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/cities.json +0 -0
  29. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/countries.json +0 -0
  30. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/ranges.csv +0 -0
  31. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/xync_files.sql +0 -0
  32. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/models/CLAUDE.md +0 -0
  33. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/py.typed +0 -0
  34. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/shared.py +0 -0
  35. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/__init__.py +0 -0
  36. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/__init__.py +0 -0
  37. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/actor.py +0 -0
  38. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/ad.py +0 -0
  39. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/common.py +0 -0
  40. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/cred.py +0 -0
  41. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/order.py +0 -0
  42. {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/pm.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: xync-db
3
- Version: 0.6.1.dev1
3
+ Version: 0.8.0
4
4
  Summary: Xync DB schema: Tortoise ORM models, enums, pydantic DTOs.
5
5
  Author-email: Mike Artemiev <mixartemev@gmail.com>
6
6
  License: LicenseRef-EULA
@@ -43,6 +43,26 @@ class W3SendStatus(IntEnum):
43
43
  done = 3
44
44
 
45
45
 
46
+ class W3SwapStatus(IntEnum):
47
+ """Своп web3-монеты в фиат Xync (W3Swap, web3.md): pending — заявка, ждём монету с `frm`; received — external
48
+ поймал перевод (tx_hash, amount), api зачисляет фиат; done — зачислено (W3Swap.tx); manual — разбирает админ
49
+ (перевод без заявки или зачисление упало)."""
50
+
51
+ pending = 0
52
+ received = 1
53
+ done = 2
54
+ manual = 3
55
+
56
+
57
+ class ExTopUpStatus(IntEnum):
58
+ """Пополнение монетой внутренним переводом на бирже (ExTopUp): pending — ждём перевод на UID актора Xync;
59
+ done — пришёл, зачислено (ExTopUp.tx); expired — за окно ожидания перевод не нашли."""
60
+
61
+ pending = 0
62
+ done = 1
63
+ expired = 2
64
+
65
+
46
66
  class LinkType(IntEnum):
47
67
  ref = 0 # open or trusted
48
68
  request = 1 # open or personalized
@@ -23,6 +23,7 @@ from xync_db.enums import (
23
23
  DepType,
24
24
  ExAction,
25
25
  ExStatus,
26
+ ExTopUpStatus,
26
27
  ExType,
27
28
  FileType,
28
29
  GeminiModel,
@@ -39,6 +40,7 @@ from xync_db.enums import (
39
40
  TransactionStatus,
40
41
  UserStatus,
41
42
  W3SendStatus,
43
+ W3SwapStatus,
42
44
  )
43
45
 
44
46
 
@@ -200,10 +202,27 @@ class Migration(migrations.Migration):
200
202
  name="Cur",
201
203
  fields=[
202
204
  ("id", UInt1Field(generated=True, primary_key=True, unique=True, db_index=True)),
203
- ("ticker", fields.CharField(unique=True, max_length=3)),
205
+ ("ticker", fields.CharField(unique=True, max_length=15)),
204
206
  ("scale", UInt1Field(db_default=2)),
207
+ (
208
+ "coin",
209
+ fields.OneToOneField(
210
+ "models.Coin",
211
+ source_field="coin_id",
212
+ null=True,
213
+ db_constraint=True,
214
+ to_field="id",
215
+ related_name="cur",
216
+ on_delete=OnDelete.CASCADE,
217
+ ),
218
+ ),
205
219
  ],
206
- options={"table": "cur", "app": "models", "pk_attr": "id", "table_description": "Fiat currencies"},
220
+ options={
221
+ "table": "cur",
222
+ "app": "models",
223
+ "pk_attr": "id",
224
+ "table_description": "Xync ledger units: fiat currencies and coins (coin_id)",
225
+ },
207
226
  bases=["Model"],
208
227
  ),
209
228
  ops.CreateModel(
@@ -1951,6 +1970,7 @@ class Migration(migrations.Migration):
1951
1970
  ),
1952
1971
  ("contract", fields.CharField(null=True, max_length=128)),
1953
1972
  ("decimals", UInt1Field()),
1973
+ ("scanned", fields.BigIntField(null=True)),
1954
1974
  ],
1955
1975
  options={
1956
1976
  "table": "coin_net",
@@ -2602,6 +2622,153 @@ class Migration(migrations.Migration):
2602
2622
  options={"table": "transaction", "app": "models", "pk_attr": "id"},
2603
2623
  bases=["Model"],
2604
2624
  ),
2625
+ ops.CreateModel(
2626
+ name="W3Swap",
2627
+ fields=[
2628
+ ("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
2629
+ ("created_at", DatetimeSecField(db_default=Now(), auto_now=False, auto_now_add=True)),
2630
+ ("updated_at", DatetimeSecField(db_default=Now(), auto_now=True, auto_now_add=False)),
2631
+ (
2632
+ "user",
2633
+ fields.ForeignKeyField(
2634
+ "models.User",
2635
+ source_field="user_id",
2636
+ null=True,
2637
+ db_constraint=True,
2638
+ to_field="id",
2639
+ related_name="w3_swaps",
2640
+ on_delete=OnDelete.CASCADE,
2641
+ ),
2642
+ ),
2643
+ (
2644
+ "coin_net",
2645
+ fields.ForeignKeyField(
2646
+ "models.CoinNet",
2647
+ source_field="coin_net_id",
2648
+ db_constraint=True,
2649
+ to_field="id",
2650
+ related_name="w3_swaps",
2651
+ on_delete=OnDelete.CASCADE,
2652
+ ),
2653
+ ),
2654
+ (
2655
+ "cur",
2656
+ fields.ForeignKeyField(
2657
+ "models.Cur",
2658
+ source_field="cur_id",
2659
+ null=True,
2660
+ db_constraint=True,
2661
+ to_field="id",
2662
+ related_name="w3_swaps",
2663
+ on_delete=OnDelete.CASCADE,
2664
+ ),
2665
+ ),
2666
+ ("frm", fields.CharField(max_length=128)),
2667
+ ("rate", fields.FloatField(null=True)),
2668
+ (
2669
+ "status",
2670
+ fields.IntEnumField(
2671
+ description="pending: 0\nreceived: 1\ndone: 2\nmanual: 3",
2672
+ db_default=W3SwapStatus.pending,
2673
+ enum_type=W3SwapStatus,
2674
+ generated=False,
2675
+ ),
2676
+ ),
2677
+ ("tx_hash", fields.CharField(null=True, unique=True, max_length=128)),
2678
+ ("amount", UInt16Field(null=True)),
2679
+ (
2680
+ "tx",
2681
+ fields.ForeignKeyField(
2682
+ "models.Transaction",
2683
+ source_field="tx_id",
2684
+ null=True,
2685
+ db_constraint=True,
2686
+ to_field="id",
2687
+ related_name="w3_swaps",
2688
+ on_delete=OnDelete.CASCADE,
2689
+ ),
2690
+ ),
2691
+ ],
2692
+ options={
2693
+ "table": "w3_swap",
2694
+ "app": "models",
2695
+ "pk_attr": "id",
2696
+ "table_description": "Web3 coin → Xync fiat swap",
2697
+ },
2698
+ bases=["Model", "TsTrait"],
2699
+ ),
2700
+ ops.CreateModel(
2701
+ name="ExTopUp",
2702
+ fields=[
2703
+ ("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
2704
+ ("created_at", DatetimeSecField(db_default=Now(), auto_now=False, auto_now_add=True)),
2705
+ ("updated_at", DatetimeSecField(db_default=Now(), auto_now=True, auto_now_add=False)),
2706
+ (
2707
+ "user",
2708
+ fields.ForeignKeyField(
2709
+ "models.User",
2710
+ source_field="user_id",
2711
+ db_constraint=True,
2712
+ to_field="id",
2713
+ related_name="ex_topups",
2714
+ on_delete=OnDelete.CASCADE,
2715
+ ),
2716
+ ),
2717
+ (
2718
+ "actor",
2719
+ fields.ForeignKeyField(
2720
+ "models.Actor",
2721
+ source_field="actor_id",
2722
+ db_constraint=True,
2723
+ to_field="id",
2724
+ related_name="ex_topups",
2725
+ on_delete=OnDelete.CASCADE,
2726
+ ),
2727
+ ),
2728
+ (
2729
+ "cur",
2730
+ fields.ForeignKeyField(
2731
+ "models.Cur",
2732
+ source_field="cur_id",
2733
+ db_constraint=True,
2734
+ to_field="id",
2735
+ related_name="ex_topups",
2736
+ on_delete=OnDelete.CASCADE,
2737
+ ),
2738
+ ),
2739
+ ("amount", UInt4Field()),
2740
+ (
2741
+ "status",
2742
+ fields.IntEnumField(
2743
+ description="pending: 0\ndone: 1\nexpired: 2",
2744
+ db_default=ExTopUpStatus.pending,
2745
+ enum_type=ExTopUpStatus,
2746
+ generated=False,
2747
+ ),
2748
+ ),
2749
+ ("dep_id", fields.CharField(null=True, unique=True, max_length=64)),
2750
+ ("frm", fields.CharField(null=True, max_length=64)),
2751
+ (
2752
+ "tx",
2753
+ fields.ForeignKeyField(
2754
+ "models.Transaction",
2755
+ source_field="tx_id",
2756
+ null=True,
2757
+ db_constraint=True,
2758
+ to_field="id",
2759
+ related_name="ex_topups",
2760
+ on_delete=OnDelete.CASCADE,
2761
+ ),
2762
+ ),
2763
+ ],
2764
+ options={
2765
+ "table": "ex_topup",
2766
+ "app": "models",
2767
+ "pk_attr": "id",
2768
+ "table_description": "Coin top-up by internal exchange transfer to a Xync actor",
2769
+ },
2770
+ bases=["Model", "TsTrait"],
2771
+ ),
2605
2772
  ops.CreateModel(
2606
2773
  name="Sign",
2607
2774
  fields=[
@@ -16,6 +16,7 @@ Channels emitted:
16
16
  ads_upd / mads_upd -- ad row changed (diff payload)
17
17
  creds_new/creds_del/creds_upd, credexs_del, credexs_orphan -- cred/credex lifecycle
18
18
  w3addr_wanted / w3addr_filled -- web3 address handshake
19
+ w3swap_received -- w3_swap: монета свопа пришла (received) → api зачисляет фиат
19
20
 
20
21
  Guards (RAISE, без NOTIFY): tx_before (баланс/иммутабельность/монотонность), tx_no_delete,
21
22
  ad_exid_ex_uniq (exid уникален per-exchange), ad_cond_side_uniq/cond_txt_side_uniq (unique
@@ -475,6 +476,21 @@ $net_addr_upd$ LANGUAGE plpgsql;
475
476
  CREATE OR REPLACE TRIGGER net_addr_upd AFTER UPDATE ON "net_addr" FOR EACH ROW EXECUTE FUNCTION net_addr_upd();
476
477
  """
477
478
 
479
+ # своп web3→фиат: external пишет пришедший перевод в заявку (status → received) — api зачисляет фиат. Каждый
480
+ # UPDATE received-строки шлёт заново: так external рекикает потерянный NOTIFY (рестарт api), а зачисление
481
+ # идемпотентно (claim received→done).
482
+ W3SWAP_SQL = """
483
+ CREATE OR REPLACE FUNCTION w3_swap_upd() RETURNS trigger AS $w3_swap_upd$
484
+ BEGIN
485
+ IF NEW.status = 1 THEN
486
+ PERFORM pg_notify('w3swap_received', NEW.id::varchar);
487
+ END IF;
488
+ RETURN NULL;
489
+ END
490
+ $w3_swap_upd$ LANGUAGE plpgsql;
491
+ CREATE OR REPLACE TRIGGER w3_swap_upd AFTER UPDATE ON "w3_swap" FOR EACH ROW EXECUTE FUNCTION w3_swap_upd();
492
+ """
493
+
478
494
 
479
495
  class Migration(BaseMigration):
480
496
  dependencies: ClassVar = [("models", "0001_initial")]
@@ -495,4 +511,5 @@ class Migration(BaseMigration):
495
511
  RunSQL(COND_SIDE_UNIQ_SQL),
496
512
  RunSQL(COND_GC_SQL),
497
513
  RunSQL(NET_ADDR_TRIGGERS_SQL),
514
+ RunSQL(W3SWAP_SQL),
498
515
  ]
@@ -1,6 +1,6 @@
1
1
  from tortoise import BaseDBAsyncClient
2
2
 
3
- from xync_db.models import Coin, Net
3
+ from xync_db.models import Coin, CoinEx, CoinNet, Cur, Net
4
4
 
5
5
  # Сети web3-модуля фронта (ChainId, см. xync-web/src/web3/types.ts) → тикер их нативной монеты.
6
6
  # `Net.name` = идентификатор сети во фронте. id=0 — домашняя сеть Xync (как Coin/Ex id=0).
@@ -17,6 +17,21 @@ NETS: list[tuple[int, str, str]] = [
17
17
  (9, "btc", "BTC"),
18
18
  ]
19
19
 
20
+ # Токены в сетях (CoinNet, `GET /public/coin-nets`, своп web3→фиат): (тикер, сеть, контракт, decimals) — как
21
+ # в xync-pay/src/web3/tokens.ts.
22
+ COIN_NETS: list[tuple[str, str, str, int]] = [
23
+ ("USDT", "eth", "0xdAC17F958D2ee523a2206206994597C13D831ec7", 6),
24
+ ("USDT", "bsc", "0x55d398326f99059fF775485246999027B3197955", 18),
25
+ ("USDT", "polygon", "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", 6),
26
+ ("USDT", "arbitrum", "0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9", 6),
27
+ ("USDT", "gram", "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", 6),
28
+ ("USDT", "sol", "Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB", 6),
29
+ ("USDT", "trx", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", 6),
30
+ ]
31
+
32
+ # Монеты в книге Xync (кастодиальный баланс, Cur.coin): пополнение внутренним переводом на бирже (ExTopUp).
33
+ LEDGER_COINS = ("USDT",)
34
+
20
35
 
21
36
  async def net_seed(db: BaseDBAsyncClient) -> str:
22
37
  # ARB/BASE/POL (нативные токены arbitrum/base/polygon, 18 decimals) не заводятся в
@@ -31,4 +46,14 @@ async def net_seed(db: BaseDBAsyncClient) -> str:
31
46
  coins = dict(await Coin.all().values_list("ticker", "id"))
32
47
  for nid, name, ticker in NETS:
33
48
  await Net.update_or_create({"name": name, "native_coin_id": coins[ticker]}, id=nid)
49
+ nets = dict(await Net.all().values_list("name", "id"))
50
+ for ticker, net, contract, decimals in COIN_NETS:
51
+ await CoinNet.update_or_create(
52
+ {"contract": contract, "decimals": decimals}, coin_id=coins[ticker], net_id=nets[net]
53
+ )
54
+ for ticker in LEDGER_COINS: # scale = CoinEx(ex_id=0): сырые суммы книги и Asset ex0 в одних единицах
55
+ if not await Cur.exists(coin_id=coins[ticker]):
56
+ scale = (await CoinEx.get(coin_id=coins[ticker], ex_id=0)).scale
57
+ cid = (await Cur.all().order_by("-id").first()).id + 1
58
+ await Cur.create(id=cid, ticker=ticker, scale=scale, coin_id=coins[ticker])
34
59
  return ""
@@ -81,6 +81,7 @@ from xync_db.enums import (
81
81
  DepType,
82
82
  ExAction,
83
83
  ExStatus,
84
+ ExTopUpStatus,
84
85
  ExType,
85
86
  GeminiModel,
86
87
  GeminiRes,
@@ -97,6 +98,7 @@ from xync_db.enums import (
97
98
  TaskType,
98
99
  UserStatus,
99
100
  W3SendStatus,
101
+ W3SwapStatus,
100
102
  )
101
103
  from xync_db.enums import (
102
104
  TransactionStatus as TS,
@@ -135,9 +137,13 @@ class City(Model):
135
137
 
136
138
 
137
139
  class Cur(Model):
140
+ """Единица книги Xync (`Transaction.cur`, 1 байт в pack): фиат, а с `coin` — монета (её кастодиальный баланс)."""
141
+
138
142
  id = UInt1Field(True)
139
- ticker: str = CharField(3, unique=True)
143
+ ticker: str = CharField(15, unique=True)
140
144
  scale: int = UInt1Field(db_default=2)
145
+ coin: OneToOneNullableRelation[Coin] = OneToOneField("models.Coin", "cur", on_update=CASCADE, null=True)
146
+ coin_id: int | None
141
147
 
142
148
  def real(self, amount: int) -> float:
143
149
  """Скейленый int в этой валюте (/10^scale) → реальное значение — на отдачу юзеру."""
@@ -153,7 +159,7 @@ class Cur(Model):
153
159
  _name: ClassVar = {"ticker"}
154
160
 
155
161
  class Meta:
156
- table_description = "Fiat currencies"
162
+ table_description = "Xync ledger units: fiat currencies and coins (coin_id)"
157
163
 
158
164
 
159
165
  class Coin(Model):
@@ -201,6 +207,9 @@ class CoinNet(Model):
201
207
  net_id: int
202
208
  contract: str | None = CharField(128, null=True)
203
209
  decimals: int = UInt1Field()
210
+ # курсор скана входящих на адрес Xync (W3Swap): блок (EVM) / мс (trx) / unix-время (gram) / слот (sol);
211
+ # None — ещё не сканировали (первый скан ставит «сейчас»)
212
+ scanned: int | None = BigIntField(null=True)
204
213
 
205
214
  class Meta:
206
215
  table = "coin_net"
@@ -373,6 +382,89 @@ class W3Send(Model, TsTrait):
373
382
  table_description = "Outbound web3 send (NetAddr → NetAddr)"
374
383
 
375
384
 
385
+ class W3Swap(Model, TsTrait):
386
+ """Своп web3-монеты в фиат Xync (web3.md): юзер шлёт токен `coin_net` с адреса `frm` на адрес Xync этой сети
387
+ (NetAddr user#0); external ловит входящий перевод (`tx_hash`, `amount` → received), api зачисляет `cur` от
388
+ user#0 по курсу `W3Swap.rates()` (курса нет — по котировке заявки `rate`) → done, `tx`. Перевод без заявки —
389
+ строка без user/cur со статусом manual (разбирает админ)."""
390
+
391
+ user: ForeignKeyNullableRelation[User] = ForeignKeyField("models.User", "w3_swaps", on_update=CASCADE, null=True)
392
+ user_id: int | None
393
+ coin_net: ForeignKeyRelation[CoinNet] = ForeignKeyField("models.CoinNet", "w3_swaps", on_update=CASCADE)
394
+ coin_net_id: int
395
+ cur: ForeignKeyNullableRelation[Cur] = ForeignKeyField("models.Cur", "w3_swaps", on_update=CASCADE, null=True)
396
+ cur_id: int | None
397
+ frm: str = CharField(128) # адрес отправителя, нормализованный (common.chains.norm)
398
+ rate: float | None = FloatField(null=True) # котировка при заявке: сколько cur за 1 монету
399
+ status: W3SwapStatus = IntEnumField(W3SwapStatus, db_default=W3SwapStatus.pending)
400
+ tx_hash: str | None = CharField(128, unique=True, null=True)
401
+ amount: int | None = UInt16Field(null=True) # пришло on-chain, /10^coin_net.decimals (у BSC-USDT 18 знаков)
402
+ tx: ForeignKeyNullableRelation[Transaction] = ForeignKeyField(
403
+ "models.Transaction", "w3_swaps", on_update=CASCADE, null=True
404
+ )
405
+ tx_id: UUID | None
406
+
407
+ class Meta:
408
+ table = "w3_swap"
409
+ table_description = "Web3 coin → Xync fiat swap"
410
+
411
+ @staticmethod
412
+ async def rates() -> dict[tuple[int, int], float]:
413
+ """{(coin_id, cur_id): сколько cur за 1 монету} — курс свопа монеты в фиат по активным hot-объявам (как их
414
+ видит тейкер), без оглядки на лимиты и объёмы: есть покупные — самая дорогая; нет — зеркало самой дешёвой
415
+ продажной: цена, которую ads_fresh дал бы покупной объяве её слота (`mid·price_k`). Нет объяв — нет свопа."""
416
+ mads = await MyAd.hot_mads_query([]).exclude(Q(hex__isnull=True, ad__maker__ex_id__in=SHARE_HEX_EX_IDS))
417
+ curexs = {
418
+ (cx.ex_id, cx.cur_id): cx
419
+ for cx in await CurEx.filter(
420
+ ex_id__in={m.ad.maker.ex_id for m in mads}, cur_id__in={m.ad.pair_side.pair.cur_id for m in mads}
421
+ ).prefetch_related("cur")
422
+ }
423
+ rates: dict[tuple[int, int], float] = {} # сначала — лучшие покупные
424
+ sells: dict[tuple[int, int], tuple[float, MyAd, tuple[int, int]]] = {} # лучшая продажная + её слот
425
+ for m in mads:
426
+ pair = m.ad.pair_side.pair
427
+ cx = curexs[(m.ad.maker.ex_id, pair.cur_id)]
428
+ key, price = (pair.coin_id, pair.cur_id), m.ad.price * 10**-cx.scale
429
+ if not m.ad.pair_side.is_sell:
430
+ rates[key] = max(rates.get(key, 0), price)
431
+ elif (slot := Ad.slot_of(m.ad.min_fiat, m.ad.filtered, cx.minimum)) and (
432
+ key not in sells or price < sells[key][0]
433
+ ):
434
+ sells[key] = price, m, slot
435
+ for key, (_, m, (k_min, fltrd)) in sells.items():
436
+ if key not in rates:
437
+ ticker = curexs[(m.ad.maker.ex_id, key[1])].cur.ticker
438
+ k = m.ad.maker.ex.agent_cls().price_k(k_min, bool(fltrd), False, ticker)
439
+ rates[key] = m.ad.pair_side.pair.real() * k
440
+ return rates
441
+
442
+
443
+ class ExTopUp(Model, TsTrait):
444
+ """Пополнение монетой внутренним переводом на бирже: юзер шлёт ровно `amount` монеты `cur.coin` на UID актора
445
+ Xync `actor`; api поллит его входящие внутренние депозиты (`AgentClient.internal_deposits`) и на совпадении суммы
446
+ зачисляет её в книгу от user#0 → done (`dep_id` — id депозита на бирже, `frm` — UID отправителя, `tx`)."""
447
+
448
+ user: ForeignKeyRelation[User] = ForeignKeyField("models.User", "ex_topups", on_update=CASCADE)
449
+ user_id: int
450
+ actor: ForeignKeyRelation[Actor] = ForeignKeyField("models.Actor", "ex_topups", on_update=CASCADE)
451
+ actor_id: int
452
+ cur: ForeignKeyRelation[Cur] = ForeignKeyField("models.Cur", "ex_topups", on_update=CASCADE) # монетный (coin)
453
+ cur_id: int
454
+ amount: int = UInt4Field() # /10^cur.scale
455
+ status: ExTopUpStatus = IntEnumField(ExTopUpStatus, db_default=ExTopUpStatus.pending)
456
+ dep_id: str | None = CharField(64, unique=True, null=True)
457
+ frm: str | None = CharField(64, null=True)
458
+ tx: ForeignKeyNullableRelation[Transaction] = ForeignKeyField(
459
+ "models.Transaction", "ex_topups", on_update=CASCADE, null=True
460
+ )
461
+ tx_id: UUID | None
462
+
463
+ class Meta:
464
+ table = "ex_topup"
465
+ table_description = "Coin top-up by internal exchange transfer to a Xync actor"
466
+
467
+
376
468
  class PairEx(Model, TsTrait):
377
469
  pair: ForeignKeyRelation[Pair] = ForeignKeyField("models.Pair", "pairexs", on_update=CASCADE)
378
470
  pair_id: int
@@ -547,37 +639,40 @@ class User(TgUser, TsTrait):
547
639
  async def balance_sum(self) -> int:
548
640
  return int(await self.free_assets()) + int(await self.fiats_sum())
549
641
 
550
- async def balances(self, ctd: bool = True) -> dict[int, int | float]: # ctd - convert to decimal
551
- scales: dict[int, int] = {1: 2, 2: 2, 3: 2}
642
+ async def balances(self, ctd: bool = True, coins: bool = False) -> dict[int, int | float]: # ctd - to decimal
643
+ """{cur_id: баланс} по фиату книги; coins → {coin_id: баланс} по монетным Cur (Cur.coin)."""
644
+ scales: dict[int, int] = {} if coins else {1: 2, 2: 2, 3: 2}
645
+ key, flt = ("cur__coin_id" if coins else "cur_id"), {"cur__coin_id__isnull": not coins}
552
646
 
553
647
  def grab(rows):
554
648
  scales.update({c: s for c, _, s in rows})
555
649
  return {c: v for c, v, _ in rows}
556
650
 
557
- dbt = {1: 0, 2: 0, 3: 0} | grab(
558
- await Transaction.filter(receiver=self, status=TS.verified)
559
- .group_by("cur_id", "cur__scale")
651
+ dbt = ({} if coins else {1: 0, 2: 0, 3: 0}) | grab(
652
+ await Transaction.filter(receiver=self, status=TS.verified, **flt)
653
+ .group_by(key, "cur__scale")
560
654
  .annotate(debit=Sum("amount"))
561
- .values_list("cur_id", "debit", "cur__scale")
655
+ .values_list(key, "debit", "cur__scale")
562
656
  )
563
657
  crd = grab(
564
- await Transaction.filter(sender=self, status__gte=TS.signed)
565
- .group_by("cur_id", "cur__scale")
658
+ await Transaction.filter(sender=self, status__gte=TS.signed, **flt)
659
+ .group_by(key, "cur__scale")
566
660
  .annotate(credit=Sum("amount"))
567
- .values_list("cur_id", "credit", "cur__scale")
661
+ .values_list(key, "credit", "cur__scale")
568
662
  )
569
663
  top = grab(
570
- await TopUp.filter(user=self, completed_at__isnull=False)
571
- .group_by("cur_id", "cur__scale")
664
+ await TopUp.filter(user=self, completed_at__isnull=False, **flt)
665
+ .group_by(key, "cur__scale")
572
666
  .annotate(topup=Sum("amount"))
573
- .values_list("cur_id", "topup", "cur__scale")
667
+ .values_list(key, "topup", "cur__scale")
574
668
  )
575
669
  raw = {c: dbt.get(c, 0) - crd.get(c, 0) + top.get(c, 0) for c in dbt.keys() | crd.keys() | top.keys()}
576
670
  return {c: round(v * 10 ** -scales[c], scales[c]) for c, v in raw.items()} if ctd else raw
577
671
 
578
672
  async def coin_balances(self, ctd: bool = True) -> dict[int, int | float]: # ctd - convert to decimal
579
673
  """Плоский {coin_id: amount} — суммарный ВНУТРЕННИЙ (ex_id=0) баланс юзера по каждой монете:
580
- person → actors(ex_id=0) → addr → assets, суммируя Asset.free. ctd → /10^CoinEx(ex_id=0).scale."""
674
+ person → actors(ex_id=0) → addr → assets, суммируя Asset.free, плюс книга (монетные Cur, их scale =
675
+ CoinEx(ex_id=0).scale). ctd → /10^CoinEx(ex_id=0).scale."""
581
676
  rows = (
582
677
  await Asset.filter(addr__actor__person__user_id=self.id, addr__actor__ex_id=0)
583
678
  .group_by("addr__coin_id")
@@ -585,6 +680,8 @@ class User(TgUser, TsTrait):
585
680
  .values_list("addr__coin_id", "total")
586
681
  )
587
682
  raw = {coin_id: total for coin_id, total in rows}
683
+ for coin_id, amount in (await self.balances(False, coins=True)).items():
684
+ raw[coin_id] = raw.get(coin_id, 0) + amount
588
685
  if not ctd:
589
686
  return raw
590
687
  scales = dict(await CoinEx.filter(ex_id=0).values_list("coin_id", "scale"))
File without changes
File without changes
File without changes