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.
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/PKG-INFO +1 -1
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/enums.py +20 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/0001_initial.py +169 -2
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/0002_triggers.py +17 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/c_net_seed.py +26 -1
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/models/__init__.py +112 -15
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/.gitignore +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/README.md +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/pyproject.toml +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/__init__.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/config.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/exceptions.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/graph.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/i18n.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/login_store.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/logo.png +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/CLAUDE.md +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/__init__.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/0003_seed.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/__init__.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/__init__.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/a_actor_seed.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/a_bin_seed.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/a_city_seed.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/a_country_seed.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/a_tg_seed.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/b_htx_seed.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/cities.json +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/countries.json +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/ranges.csv +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/migrations/models/seed/xync_files.sql +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/models/CLAUDE.md +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/py.typed +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/shared.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/__init__.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/__init__.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/actor.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/ad.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/common.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/cred.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/order.py +0 -0
- {xync_db-0.6.1.dev1 → xync_db-0.8.0}/src/xync_db/typs/db/pm.py +0 -0
|
@@ -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=
|
|
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={
|
|
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(
|
|
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 = "
|
|
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 -
|
|
551
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|