xync-db 0.6.1.dev1__tar.gz → 0.7.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.7.0}/PKG-INFO +1 -1
  2. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/enums.py +11 -0
  3. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/0001_initial.py +77 -0
  4. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/0002_triggers.py +17 -0
  5. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/c_net_seed.py +18 -1
  6. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/models/__init__.py +62 -0
  7. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/.gitignore +0 -0
  8. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/README.md +0 -0
  9. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/pyproject.toml +0 -0
  10. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/__init__.py +0 -0
  11. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/config.py +0 -0
  12. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/exceptions.py +0 -0
  13. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/graph.py +0 -0
  14. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/i18n.py +0 -0
  15. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/login_store.py +0 -0
  16. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/logo.png +0 -0
  17. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/CLAUDE.md +0 -0
  18. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/__init__.py +0 -0
  19. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/0003_seed.py +0 -0
  20. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/__init__.py +0 -0
  21. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/__init__.py +0 -0
  22. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/a_actor_seed.py +0 -0
  23. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/a_bin_seed.py +0 -0
  24. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/a_city_seed.py +0 -0
  25. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/a_country_seed.py +0 -0
  26. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/a_tg_seed.py +0 -0
  27. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/b_htx_seed.py +0 -0
  28. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/cities.json +0 -0
  29. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/countries.json +0 -0
  30. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/ranges.csv +0 -0
  31. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/migrations/models/seed/xync_files.sql +0 -0
  32. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/models/CLAUDE.md +0 -0
  33. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/py.typed +0 -0
  34. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/shared.py +0 -0
  35. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/typs/__init__.py +0 -0
  36. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/typs/db/__init__.py +0 -0
  37. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/typs/db/actor.py +0 -0
  38. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/typs/db/ad.py +0 -0
  39. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/typs/db/common.py +0 -0
  40. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/typs/db/cred.py +0 -0
  41. {xync_db-0.6.1.dev1 → xync_db-0.7.0}/src/xync_db/typs/db/order.py +0 -0
  42. {xync_db-0.6.1.dev1 → xync_db-0.7.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.7.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,17 @@ 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
+
46
57
  class LinkType(IntEnum):
47
58
  ref = 0 # open or trusted
48
59
  request = 1 # open or personalized
@@ -39,6 +39,7 @@ from xync_db.enums import (
39
39
  TransactionStatus,
40
40
  UserStatus,
41
41
  W3SendStatus,
42
+ W3SwapStatus,
42
43
  )
43
44
 
44
45
 
@@ -1951,6 +1952,7 @@ class Migration(migrations.Migration):
1951
1952
  ),
1952
1953
  ("contract", fields.CharField(null=True, max_length=128)),
1953
1954
  ("decimals", UInt1Field()),
1955
+ ("scanned", fields.BigIntField(null=True)),
1954
1956
  ],
1955
1957
  options={
1956
1958
  "table": "coin_net",
@@ -2602,6 +2604,81 @@ class Migration(migrations.Migration):
2602
2604
  options={"table": "transaction", "app": "models", "pk_attr": "id"},
2603
2605
  bases=["Model"],
2604
2606
  ),
2607
+ ops.CreateModel(
2608
+ name="W3Swap",
2609
+ fields=[
2610
+ ("id", fields.IntField(generated=True, primary_key=True, unique=True, db_index=True)),
2611
+ ("created_at", DatetimeSecField(db_default=Now(), auto_now=False, auto_now_add=True)),
2612
+ ("updated_at", DatetimeSecField(db_default=Now(), auto_now=True, auto_now_add=False)),
2613
+ (
2614
+ "user",
2615
+ fields.ForeignKeyField(
2616
+ "models.User",
2617
+ source_field="user_id",
2618
+ null=True,
2619
+ db_constraint=True,
2620
+ to_field="id",
2621
+ related_name="w3_swaps",
2622
+ on_delete=OnDelete.CASCADE,
2623
+ ),
2624
+ ),
2625
+ (
2626
+ "coin_net",
2627
+ fields.ForeignKeyField(
2628
+ "models.CoinNet",
2629
+ source_field="coin_net_id",
2630
+ db_constraint=True,
2631
+ to_field="id",
2632
+ related_name="w3_swaps",
2633
+ on_delete=OnDelete.CASCADE,
2634
+ ),
2635
+ ),
2636
+ (
2637
+ "cur",
2638
+ fields.ForeignKeyField(
2639
+ "models.Cur",
2640
+ source_field="cur_id",
2641
+ null=True,
2642
+ db_constraint=True,
2643
+ to_field="id",
2644
+ related_name="w3_swaps",
2645
+ on_delete=OnDelete.CASCADE,
2646
+ ),
2647
+ ),
2648
+ ("frm", fields.CharField(max_length=128)),
2649
+ ("rate", fields.FloatField(null=True)),
2650
+ (
2651
+ "status",
2652
+ fields.IntEnumField(
2653
+ description="pending: 0\nreceived: 1\ndone: 2\nmanual: 3",
2654
+ db_default=W3SwapStatus.pending,
2655
+ enum_type=W3SwapStatus,
2656
+ generated=False,
2657
+ ),
2658
+ ),
2659
+ ("tx_hash", fields.CharField(null=True, unique=True, max_length=128)),
2660
+ ("amount", UInt16Field(null=True)),
2661
+ (
2662
+ "tx",
2663
+ fields.ForeignKeyField(
2664
+ "models.Transaction",
2665
+ source_field="tx_id",
2666
+ null=True,
2667
+ db_constraint=True,
2668
+ to_field="id",
2669
+ related_name="w3_swaps",
2670
+ on_delete=OnDelete.CASCADE,
2671
+ ),
2672
+ ),
2673
+ ],
2674
+ options={
2675
+ "table": "w3_swap",
2676
+ "app": "models",
2677
+ "pk_attr": "id",
2678
+ "table_description": "Web3 coin → Xync fiat swap",
2679
+ },
2680
+ bases=["Model", "TsTrait"],
2681
+ ),
2605
2682
  ops.CreateModel(
2606
2683
  name="Sign",
2607
2684
  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, CoinNet, 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,18 @@ 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
+
20
32
 
21
33
  async def net_seed(db: BaseDBAsyncClient) -> str:
22
34
  # ARB/BASE/POL (нативные токены arbitrum/base/polygon, 18 decimals) не заводятся в
@@ -31,4 +43,9 @@ async def net_seed(db: BaseDBAsyncClient) -> str:
31
43
  coins = dict(await Coin.all().values_list("ticker", "id"))
32
44
  for nid, name, ticker in NETS:
33
45
  await Net.update_or_create({"name": name, "native_coin_id": coins[ticker]}, id=nid)
46
+ nets = dict(await Net.all().values_list("name", "id"))
47
+ for ticker, net, contract, decimals in COIN_NETS:
48
+ await CoinNet.update_or_create(
49
+ {"contract": contract, "decimals": decimals}, coin_id=coins[ticker], net_id=nets[net]
50
+ )
34
51
  return ""
@@ -97,6 +97,7 @@ from xync_db.enums import (
97
97
  TaskType,
98
98
  UserStatus,
99
99
  W3SendStatus,
100
+ W3SwapStatus,
100
101
  )
101
102
  from xync_db.enums import (
102
103
  TransactionStatus as TS,
@@ -201,6 +202,9 @@ class CoinNet(Model):
201
202
  net_id: int
202
203
  contract: str | None = CharField(128, null=True)
203
204
  decimals: int = UInt1Field()
205
+ # курсор скана входящих на адрес Xync (W3Swap): блок (EVM) / мс (trx) / unix-время (gram) / слот (sol);
206
+ # None — ещё не сканировали (первый скан ставит «сейчас»)
207
+ scanned: int | None = BigIntField(null=True)
204
208
 
205
209
  class Meta:
206
210
  table = "coin_net"
@@ -373,6 +377,64 @@ class W3Send(Model, TsTrait):
373
377
  table_description = "Outbound web3 send (NetAddr → NetAddr)"
374
378
 
375
379
 
380
+ class W3Swap(Model, TsTrait):
381
+ """Своп web3-монеты в фиат Xync (web3.md): юзер шлёт токен `coin_net` с адреса `frm` на адрес Xync этой сети
382
+ (NetAddr user#0); external ловит входящий перевод (`tx_hash`, `amount` → received), api зачисляет `cur` от
383
+ user#0 по курсу `W3Swap.rates()` (курса нет — по котировке заявки `rate`) → done, `tx`. Перевод без заявки —
384
+ строка без user/cur со статусом manual (разбирает админ)."""
385
+
386
+ user: ForeignKeyNullableRelation[User] = ForeignKeyField("models.User", "w3_swaps", on_update=CASCADE, null=True)
387
+ user_id: int | None
388
+ coin_net: ForeignKeyRelation[CoinNet] = ForeignKeyField("models.CoinNet", "w3_swaps", on_update=CASCADE)
389
+ coin_net_id: int
390
+ cur: ForeignKeyNullableRelation[Cur] = ForeignKeyField("models.Cur", "w3_swaps", on_update=CASCADE, null=True)
391
+ cur_id: int | None
392
+ frm: str = CharField(128) # адрес отправителя, нормализованный (common.chains.norm)
393
+ rate: float | None = FloatField(null=True) # котировка при заявке: сколько cur за 1 монету
394
+ status: W3SwapStatus = IntEnumField(W3SwapStatus, db_default=W3SwapStatus.pending)
395
+ tx_hash: str | None = CharField(128, unique=True, null=True)
396
+ amount: int | None = UInt16Field(null=True) # пришло on-chain, /10^coin_net.decimals (у BSC-USDT 18 знаков)
397
+ tx: ForeignKeyNullableRelation[Transaction] = ForeignKeyField(
398
+ "models.Transaction", "w3_swaps", on_update=CASCADE, null=True
399
+ )
400
+ tx_id: UUID | None
401
+
402
+ class Meta:
403
+ table = "w3_swap"
404
+ table_description = "Web3 coin → Xync fiat swap"
405
+
406
+ @staticmethod
407
+ async def rates() -> dict[tuple[int, int], float]:
408
+ """{(coin_id, cur_id): сколько cur за 1 монету} — курс свопа монеты в фиат по активным hot-объявам (как их
409
+ видит тейкер), без оглядки на лимиты и объёмы: есть покупные — самая дорогая; нет — зеркало самой дешёвой
410
+ продажной: цена, которую ads_fresh дал бы покупной объяве её слота (`mid·price_k`). Нет объяв — нет свопа."""
411
+ mads = await MyAd.hot_mads_query([]).exclude(Q(hex__isnull=True, ad__maker__ex_id__in=SHARE_HEX_EX_IDS))
412
+ curexs = {
413
+ (cx.ex_id, cx.cur_id): cx
414
+ for cx in await CurEx.filter(
415
+ 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}
416
+ ).prefetch_related("cur")
417
+ }
418
+ rates: dict[tuple[int, int], float] = {} # сначала — лучшие покупные
419
+ sells: dict[tuple[int, int], tuple[float, MyAd, tuple[int, int]]] = {} # лучшая продажная + её слот
420
+ for m in mads:
421
+ pair = m.ad.pair_side.pair
422
+ cx = curexs[(m.ad.maker.ex_id, pair.cur_id)]
423
+ key, price = (pair.coin_id, pair.cur_id), m.ad.price * 10**-cx.scale
424
+ if not m.ad.pair_side.is_sell:
425
+ rates[key] = max(rates.get(key, 0), price)
426
+ elif (slot := Ad.slot_of(m.ad.min_fiat, m.ad.filtered, cx.minimum)) and (
427
+ key not in sells or price < sells[key][0]
428
+ ):
429
+ sells[key] = price, m, slot
430
+ for key, (_, m, (k_min, fltrd)) in sells.items():
431
+ if key not in rates:
432
+ ticker = curexs[(m.ad.maker.ex_id, key[1])].cur.ticker
433
+ k = m.ad.maker.ex.agent_cls().price_k(k_min, bool(fltrd), False, ticker)
434
+ rates[key] = m.ad.pair_side.pair.real() * k
435
+ return rates
436
+
437
+
376
438
  class PairEx(Model, TsTrait):
377
439
  pair: ForeignKeyRelation[Pair] = ForeignKeyField("models.Pair", "pairexs", on_update=CASCADE)
378
440
  pair_id: int
File without changes
File without changes
File without changes