From 3758049dcb1be8b8a8573569af657882558c3a7e Mon Sep 17 00:00:00 2001
From: ZHU MINCHAO <48154958+MinchaoZhu@users.noreply.github.com>
Date: Thu, 5 Nov 2026 07:09:05 +0800
Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E5=8D=81=E9=98=B6=E6=AE=B5=20stage=20?=
=?UTF-8?q?2=EF=BC=88=E4=B8=80=EF=BC=89=EF=BC=9A=E4=BA=A4=E6=98=93?=
=?UTF-8?q?=E5=AF=B9=E6=94=B9=E4=B8=BA=20X/CRC=20=E6=B1=A0=EF=BC=8C?=
=?UTF-8?q?=E4=BB=BB=E6=84=8F=E5=B8=81=E4=BA=92=E6=8D=A2=E7=BB=8F=20CRC=20?=
=?UTF-8?q?=E4=B8=A4=E8=B7=B3=E8=B7=AF=E7=94=B1=EF=BC=8C=E5=B8=81=E4=BB=B7?=
=?UTF-8?q?=E8=A1=A8=E6=94=B9=E4=B8=BA=20CRC=20=E4=BB=B7=E6=A0=BC?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/Decentralized.Domain/Market.cs | 44 ++++++++-
src/Decentralized.Domain/Tuning.cs | 2 +-
src/Decentralized.Domain/TuningTable.cs | 1 +
.../ModFiles/Localization/de.csv | 8 +-
.../ModFiles/Localization/en.csv | 8 +-
.../ModFiles/Localization/es.csv | 8 +-
.../ModFiles/Localization/fr.csv | 8 +-
.../ModFiles/Localization/ja.csv | 8 +-
.../ModFiles/Localization/pl.csv | 8 +-
.../ModFiles/Localization/ru.csv | 8 +-
.../ModFiles/Localization/zh.csv | 8 +-
src/Decentralized/Session.cs | 3 +-
src/Decentralized/TerminalWindow.cs | 97 +++++++++++++++----
tests/Decentralized.Domain.Tests/Program.cs | 33 +++++--
14 files changed, 196 insertions(+), 48 deletions(-)
diff --git a/src/Decentralized.Domain/Market.cs b/src/Decentralized.Domain/Market.cs
index c3ef71c..939b7d1 100644
--- a/src/Decentralized.Domain/Market.cs
+++ b/src/Decentralized.Domain/Market.cs
@@ -4,6 +4,14 @@ using System.Linq;
namespace Decentralized.Domain
{
+ /// 一次互换的报价:得到数量、经过几个池、相对中间价的损失(手续费加滑点)。
+ public sealed class SwapQuote
+ {
+ public string From, To;
+ public double In, Out, Impact;
+ public int Hops;
+ }
+
///
/// 影响行情的游戏事件种类。接入层把游戏的新闻事件与派系行动映射到这里(例如全站停电、电力短缺 → Blackout)。
///
@@ -64,14 +72,43 @@ namespace Decentralized.Domain
var m = new Market();
double depth = Tuning.PoolDepth(day), p = Tuning.PscStartPrice;
m.Pools.Add(Pool.Create(Venue.Exchange, Assets.PSC, Assets.CRC, p, depth, Tuning.ExchangeFee));
- m.Pools.Add(Pool.Create(Venue.Dex, Assets.PSC, Assets.CRT, p, depth * SideDepthShare, Tuning.DexFee));
- m.Pools.Add(Pool.Create(Venue.Dex, Assets.PSC, Assets.CRA, p, depth * SideDepthShare, Tuning.DexFee));
+ m.Pools.Add(Pool.Create(Venue.Dex, Assets.PSC, Assets.CRC, p, depth * SideDepthShare, Tuning.DexFee));
m.Pools.Add(Pool.Create(Venue.Dex, Assets.CRT, Assets.CRC, 1, depth * SideDepthShare, Tuning.DexFee));
+ m.Pools.Add(Pool.Create(Venue.Dex, Assets.CRA, Assets.CRC, 1, depth * SideDepthShare, Tuning.DexFee));
+ m.Pools.Add(Pool.Create(Venue.Dex, Assets.SPARK, Assets.CRC, Tuning.SparkStart, depth * Tuning.SparkDepthShare, Tuning.DexFee));
return m;
}
public Pool Main => Pools[0];
+ /// DEX 上 X/CRC 的池(所有币都以 CRC 计价)。
+ public Pool DexPool(string asset) => Find(Venue.Dex, asset, Assets.CRC);
+
+ ///
+ /// 任意两币互换的报价:CRC 在一端走一个池,否则 X → CRC → Y 两跳(两次手续费、滑点叠加)。
+ /// 不改池子;没有路由(同币、没有池)返回 null。
+ ///
+ public SwapQuote Quote(string from, string to, double amount)
+ {
+ if (from == to || amount <= 0) return null;
+ var a = from == Assets.CRC ? null : DexPool(from);
+ var b = to == Assets.CRC ? null : DexPool(to);
+ if ((from != Assets.CRC && a == null) || (to != Assets.CRC && b == null)) return null;
+ double crc = a == null ? amount : a.QuoteOut(amount);
+ double mid = a == null ? amount : amount * a.Price;
+ double outAmt = b == null ? crc : b.BaseOut(crc);
+ double ideal = b == null ? mid : mid / b.Price;
+ return new SwapQuote { From = from, To = to, In = amount, Out = outAmt, Hops = (a != null ? 1 : 0) + (b != null ? 1 : 0), Impact = ideal > 0 ? Math.Max(0, 1 - outAmt / ideal) : 0 };
+ }
+
+ /// 成交:按 的路由改动池子,返回得到的数量(没有路由返回 0)。
+ public double Swap(string from, string to, double amount)
+ {
+ if (Quote(from, to, amount) == null) return 0;
+ double crc = from == Assets.CRC ? amount : DexPool(from).SellBase(amount);
+ return to == Assets.CRC ? crc : DexPool(to).BuyBase(crc);
+ }
+
public Pool Find(Venue venue, string baseAsset, string quote) =>
Pools.FirstOrDefault(x => x.Venue == venue && x.Base == baseAsset && x.Quote == quote);
@@ -152,7 +189,8 @@ namespace Decentralized.Domain
double baseCredits = pool.Base == Assets.PSC ? p : stablePrice(pool.Base);
double quoteCredits = pool.Quote == Assets.PSC ? p : stablePrice(pool.Quote);
if (quoteCredits <= 0) continue;
- pool.Revert(baseCredits / quoteCredits, ArbRate, depth * SideDepthShare / quoteCredits);
+ double share = pool.Base == Assets.SPARK ? Tuning.SparkDepthShare : SideDepthShare;
+ pool.Revert(baseCredits / quoteCredits, ArbRate, depth * share / quoteCredits);
}
}
}
diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs
index 8dc059b..57694aa 100644
--- a/src/Decentralized.Domain/Tuning.cs
+++ b/src/Decentralized.Domain/Tuning.cs
@@ -97,7 +97,7 @@ namespace Decentralized.Domain
/// 外观折价(占价值的比例):叶片积灰 / 缺角,金手指发暗 / 氧化,挡板划痕,封条缺失。
public const double CardFanDust = 0.04, CardFanChipped = 0.12, CardFingersDark = 0.04, CardFingersOxidized = 0.12, CardBracketScratch = 0.02, CardSealMissing = 0.06;
/// 非主交易对每晚被拉回参考价的比例,及其深度占主交易对的比例。
- public const double SideArbRate = 0.75, SideDepthShare = 0.3;
+ public const double SideArbRate = 0.75, SideDepthShare = 0.3, SparkDepthShare = 0.15;
/// Web3 黑市:开放所需黑市声望,中 / 高档货所需声望,现货每周刷新的天数与基础件数(每高 BmStockPerRep 声望多一件),现货溢价。
public const int BmOpenRep = 10, BmMidRep = 40, BmHighRep = 80, BmRefreshDays = 7, BmStockBase = 4;
public const double BmStockPerRep = 40, BmPremium = 0.30;
diff --git a/src/Decentralized.Domain/TuningTable.cs b/src/Decentralized.Domain/TuningTable.cs
index 35dad1b..d65c34f 100644
--- a/src/Decentralized.Domain/TuningTable.cs
+++ b/src/Decentralized.Domain/TuningTable.cs
@@ -324,6 +324,7 @@ namespace Decentralized.Domain
["SentimentVol"] = Tuning.SentimentVol,
["SideArbRate"] = Tuning.SideArbRate,
["SideDepthShare"] = Tuning.SideDepthShare,
+ ["SparkDepthShare"] = Tuning.SparkDepthShare,
["SparkFee"] = Tuning.SparkFee,
["SparkMaxMult"] = Tuning.SparkMaxMult,
["SparkMinMult"] = Tuning.SparkMinMult,
diff --git a/src/Decentralized/ModFiles/Localization/de.csv b/src/Decentralized/ModFiles/Localization/de.csv
index 9ee0bfb..788cccd 100644
--- a/src/Decentralized/ModFiles/Localization/de.csv
+++ b/src/Decentralized/ModFiles/Localization/de.csv
@@ -149,8 +149,6 @@ bank.ops,Ein-/Auszahlung
poin.account.h,Poinbase-Konto
trade.h,Handel
web3.wallet.h,On-Chain-Wallet
-col.peg,Preis
-col.status,Status
stable.ok,Gekoppelt
stable.depeg,Entkoppelt
stable.warn,Warnung: Staking-Subventionspool knapp
@@ -902,3 +900,9 @@ rig.st.power,Verbrauch
rig.st.hash,Hashrate
rig.st.kj,{0} kJ/Tag
lbl.point.model,Modell beanstanden
+col.crcprice,CRC-Preis
+swap.pay,Zahlen
+swap.get,Erhalten
+swap.preview,"Ca. {0} {1} {2} Pool(s), Verlust {3} %"
+swap.go,Tauschen
+swap.same,Zahlungs- und Empfangswährung müssen verschieden sein.
diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv
index 193f3dc..ed8a0bd 100644
--- a/src/Decentralized/ModFiles/Localization/en.csv
+++ b/src/Decentralized/ModFiles/Localization/en.csv
@@ -149,8 +149,6 @@ bank.ops,Deposit / withdraw
poin.account.h,Poinbase account
trade.h,Trade
web3.wallet.h,On-chain wallet
-col.peg,Price
-col.status,Status
stable.ok,Pegged
stable.depeg,Depegged
stable.warn,Warning: staking subsidy pool low
@@ -902,3 +900,9 @@ rig.st.power,Power
rig.st.hash,Hashrate
rig.st.kj,{0} kJ/day
lbl.point.model,Point out model
+col.crcprice,CRC price
+swap.pay,Pay
+swap.get,Receive
+swap.preview,"Receive about {0} {1} {2} pool(s), loss {3}%"
+swap.go,Swap
+swap.same,Pay and receive must be different coins.
diff --git a/src/Decentralized/ModFiles/Localization/es.csv b/src/Decentralized/ModFiles/Localization/es.csv
index 9bb25b6..3d948c4 100644
--- a/src/Decentralized/ModFiles/Localization/es.csv
+++ b/src/Decentralized/ModFiles/Localization/es.csv
@@ -149,8 +149,6 @@ bank.ops,Depósito / retiro
poin.account.h,Cuenta de Poinbase
trade.h,Comercio
web3.wallet.h,Billetera en cadena
-col.peg,Precio
-col.status,Estado
stable.ok,Anclado
stable.depeg,Desanclado
stable.warn,Advertencia: fondo de subsidio de staking bajo
@@ -902,3 +900,9 @@ rig.st.power,Consumo
rig.st.hash,Tasa de hash
rig.st.kj,{0} kJ/día
lbl.point.model,Señalar modelo
+col.crcprice,Precio en CRC
+swap.pay,Pagar
+swap.get,Recibir
+swap.preview,"Aprox. {0} {1} {2} pool(s), pérdida {3} %"
+swap.go,Canjear
+swap.same,Las monedas a pagar y recibir deben ser distintas.
diff --git a/src/Decentralized/ModFiles/Localization/fr.csv b/src/Decentralized/ModFiles/Localization/fr.csv
index 957b3b9..9428b72 100644
--- a/src/Decentralized/ModFiles/Localization/fr.csv
+++ b/src/Decentralized/ModFiles/Localization/fr.csv
@@ -149,8 +149,6 @@ bank.ops,Dépôt / Retrait
poin.account.h,Compte Poinbase
trade.h,Trading
web3.wallet.h,Portefeuille on-chain
-col.peg,Prix
-col.status,Statut
stable.ok,Arrimé
stable.depeg,Désarrimé
stable.warn,Attention : pool de subvention de staking au plus bas
@@ -902,3 +900,9 @@ rig.st.power,Conso.
rig.st.hash,Hachage
rig.st.kj,{0} kJ/j
lbl.point.model,Signaler le modèle
+col.crcprice,Prix en CRC
+swap.pay,Payer
+swap.get,Recevoir
+swap.preview,"Environ {0} {1} {2} pool(s), perte {3} %"
+swap.go,Échanger
+swap.same,Les monnaies payée et reçue doivent différer.
diff --git a/src/Decentralized/ModFiles/Localization/ja.csv b/src/Decentralized/ModFiles/Localization/ja.csv
index 761a2c5..e5cf5bf 100644
--- a/src/Decentralized/ModFiles/Localization/ja.csv
+++ b/src/Decentralized/ModFiles/Localization/ja.csv
@@ -149,8 +149,6 @@ bank.ops,入出金
poin.account.h,Poinbase口座
trade.h,取引
web3.wallet.h,オンチェーンウォレット
-col.peg,価格
-col.status,状態
stable.ok,ペグ正常
stable.depeg,デペグ
stable.warn,警告:ステーキング補助金プール残高低下
@@ -902,3 +900,9 @@ rig.st.power,消費電力
rig.st.hash,ハッシュレート
rig.st.kj,{0} kJ/日
lbl.point.model,型番を指摘する
+col.crcprice,CRC 価格
+swap.pay,支払い
+swap.get,受取
+swap.preview,約 {0} {1} プール {2} 個、損失 {3}%
+swap.go,スワップ
+swap.same,支払いと受取は別の通貨にしてください。
diff --git a/src/Decentralized/ModFiles/Localization/pl.csv b/src/Decentralized/ModFiles/Localization/pl.csv
index 076e9bd..aa1b017 100644
--- a/src/Decentralized/ModFiles/Localization/pl.csv
+++ b/src/Decentralized/ModFiles/Localization/pl.csv
@@ -149,8 +149,6 @@ bank.ops,Wpłata / wypłata
poin.account.h,Konto Poinbase
trade.h,Handel
web3.wallet.h,Portfel on-chain
-col.peg,Cena
-col.status,Status
stable.ok,W parytecie
stable.depeg,Utrata parytetu
stable.warn,Ostrzeżenie: niska pula dotacji stakingu
@@ -902,3 +900,9 @@ rig.st.power,Pobór
rig.st.hash,Moc obl.
rig.st.kj,{0} kJ/dzień
lbl.point.model,Wskaż model
+col.crcprice,Cena w CRC
+swap.pay,Płacę
+swap.get,Otrzymuję
+swap.preview,"Około {0} {1} pule: {2}, strata {3}%"
+swap.go,Wymień
+swap.same,Waluty płatności i odbioru muszą się różnić.
diff --git a/src/Decentralized/ModFiles/Localization/ru.csv b/src/Decentralized/ModFiles/Localization/ru.csv
index edaa9e8..8b3cce8 100644
--- a/src/Decentralized/ModFiles/Localization/ru.csv
+++ b/src/Decentralized/ModFiles/Localization/ru.csv
@@ -149,8 +149,6 @@ bank.ops,Депозит / вывод
poin.account.h,Счёт Poinbase
trade.h,Торговля
web3.wallet.h,Ончейн-кошелёк
-col.peg,Цена
-col.status,Статус
stable.ok,Привязан
stable.depeg,Отвязан
stable.warn,Внимание: пул субсидий стейкинга на исходе
@@ -902,3 +900,9 @@ rig.st.power,Расход
rig.st.hash,Хешрейт
rig.st.kj,{0} кДж/день
lbl.point.model,Указать модель
+col.crcprice,Цена в CRC
+swap.pay,Отдаю
+swap.get,Получаю
+swap.preview,"Около {0} {1} пулов: {2}, потери {3}%"
+swap.go,Обменять
+swap.same,Монеты оплаты и получения должны различаться.
diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv
index 97bb46c..6965684 100644
--- a/src/Decentralized/ModFiles/Localization/zh.csv
+++ b/src/Decentralized/ModFiles/Localization/zh.csv
@@ -148,8 +148,6 @@ bank.ops,"存取"
poin.account.h,"Poinbase 账户"
trade.h,"交易"
web3.wallet.h,"链上钱包"
-col.peg,"价格"
-col.status,"状态"
stable.ok,"锚定正常"
stable.depeg,"脱锚"
stable.warn,"预兆:质押补贴池告急"
@@ -901,3 +899,9 @@ rig.st.power,耗电
rig.st.hash,算力
rig.st.kj,{0} kJ/天
lbl.point.model,指出型号
+col.crcprice,CRC 价格
+swap.pay,支付
+swap.get,获得
+swap.preview,约得 {0} {1} 经 {2} 个池,损耗 {3}%
+swap.go,兑换
+swap.same,支付和获得的币种不能相同。
diff --git a/src/Decentralized/Session.cs b/src/Decentralized/Session.cs
index 213f13e..f20ec0c 100644
--- a/src/Decentralized/Session.cs
+++ b/src/Decentralized/Session.cs
@@ -136,7 +136,8 @@ namespace Decentralized
}
catch (Exception ex) { Log.Error($"读取存档数据失败,重新开始:{ex.Message}"); }
if (Data == null) Data = new SaveData();
- if (Data.Market == null) Data.Market = Market.CreateDefault(StoreStation.GetDayCounter());
+ // 池子表在第十阶段改过(全部以 CRC 计价);旧测试档里的表缺池子,重建成默认值(价格从起始价重来)
+ if (Data.Market == null || Data.Market.DexPool(Assets.SPARK) == null) Data.Market = Market.CreateDefault(StoreStation.GetDayCounter());
// 旧存档里的商家地址没有校验位:换成合法地址
foreach (var s in Data.Black.Sellers) if (!ChainAddress.Valid(s.Name)) s.Name = ChainAddress.Make(Data.Rng.NextUInt());
if (Data.Story == null) Data.Story = new StoryMods();
diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs
index 5303bd9..29510da 100644
--- a/src/Decentralized/TerminalWindow.cs
+++ b/src/Decentralized/TerminalWindow.cs
@@ -231,9 +231,6 @@ namespace Decentralized
b.AddLabel(L.T("aml.taint", (int)Math.Round(taintRatio * 100)), taintRatio > 0.05 ? "aml_note" : "aml_dim");
}
- private static readonly float[] StableWBase = new[] { 80f, 100f, 220f };
- private static float[] StableW => Sc(StableWBase);
-
private static string StableStatus(Stablecoins st, string asset, int day)
{
double p = st.Price(asset);
@@ -242,15 +239,78 @@ namespace Decentralized
return p < 0.99 ? L.T("stable.depeg") : L.T("stable.ok");
}
- /// 稳定币行情:价格与状态(脱锚、预兆)。
- private static void StableTable(CustomUIBuilder b)
+ private static readonly float[] PriceWBase = new[] { 100f, 160f };
+ private static float[] PriceW => Sc(PriceWBase);
+ private static readonly string[] SwapCoins = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA, Assets.SPARK };
+ private static string _swapFrom = Assets.CRC, _swapTo = Assets.PSC;
+
+ /// 币价表:各币在 DEX 池里以 CRC 计的价格。
+ private static void PriceTable(CustomUIBuilder b)
{
- var st = Session.Data.Stable;
- int day = StoreStation.GetDayCounter();
+ var m = Session.Data.Market;
b.AddSeparator();
- TableHeader(b, StableW, L.T("col.asset"), L.T("col.peg"), L.T("col.status"));
- foreach (var a in new[] { Assets.CRC, Assets.CRT, Assets.CRA })
- TableRow(b, StableW, a, Fmt.Num(st.Price(a)), StableStatus(st, a, day));
+ TableHeader(b, PriceW, L.T("col.asset"), L.T("col.crcprice"));
+ foreach (var a in SwapCoins.Where(x => x != Assets.CRC))
+ TableRow(b, PriceW, a, Fmt.Num(m.DexPool(a).Price));
+ }
+
+ /// 互换:任意币对任意币,实际经 CRC 路由;预览显示得到数量、经过几个池、损耗。
+ private static void SwapBlock(CustomUIBuilder b)
+ {
+ var m = Session.Data.Market;
+ Action picker = (label, cur, from) =>
+ {
+ b.AddLabel(label);
+ b.BeginRow(4f);
+ foreach (var c in SwapCoins)
+ {
+ var coin = c;
+ PledgerSkin.Pick(b, coin, coin == cur, () =>
+ {
+ if (from) { _swapFrom = coin; if (_swapTo == coin) _swapTo = SwapCoins.First(x => x != coin); }
+ else { _swapTo = coin; if (_swapFrom == coin) _swapFrom = SwapCoins.First(x => x != coin); }
+ Open();
+ }, _picked);
+ }
+ b.End();
+ };
+ AmountRow(b);
+ picker(L.T("swap.pay"), _swapFrom, true);
+ picker(L.T("swap.get"), _swapTo, false);
+ double v;
+ bool ok = double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) && v > 0;
+ var q = ok ? m.Quote(_swapFrom, _swapTo, v) : null;
+ if (q != null) b.AddLabel(L.T("swap.preview", Fmt.Num(q.Out), _swapTo, q.Hops, (q.Impact * 100).ToString("0.0", CultureInfo.InvariantCulture)), "swap_preview_dim");
+ b.AddButton(L.T("swap.go"), () => Swap());
+ }
+
+ /// 互换:DEX 是合约调用,要在硬件钱包上签名。
+ private static void Swap()
+ {
+ if (!ReadAmount(out double v)) return;
+ string from = _swapFrom, to = _swapTo;
+ var q = Session.Data.Market.Quote(from, to, v);
+ if (q == null) { _message = L.T("swap.same"); Open(); return; }
+ var w = Session.Data.Hot;
+ if (w.Avail(from) + 1e-9 < v) { _message = L.T(frozenPay(w, from) ? "msg.frozen" : "msg.balance"); Open(); return; }
+ _message = L.T("hw.wait");
+ Open();
+ WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Asset = from, Amount = v, Summary = L.T("hw.swap", Fmt.Num(v), from, Fmt.Num(q.Out), to), Execute = pw => DoSwap(from, to, v, pw) });
+ }
+
+ private static void DoSwap(string from, string to, double v, Wallet w)
+ {
+ if (w.Avail(from) + 1e-9 < v) { _message = L.T(frozenPay(w, from) ? "msg.frozen" : "msg.balance"); Open(); return; }
+ double got = Session.Data.Market.Swap(from, to, v);
+ if (got <= 0) { _message = L.T("swap.same"); Open(); return; }
+ double spent = v * Session.Price(from);
+ CandleChart.Trade(from, spent); CandleChart.Trade(to, spent);
+ w.Add(from, -v);
+ w.Add(to, got);
+ w.RecordBuy(to, got, spent);
+ _message = L.T("msg.trade", Fmt.Num(v), from, Fmt.Num(got), to);
+ Session.Save();
+ Open();
}
private static void TradeRows(CustomUIBuilder b, Venue venue)
@@ -1193,14 +1253,13 @@ namespace Decentralized
l.AddSeparator();
l.AddLabel(L.T("wallet.total", Fmt.Credits(w.Value(Session.Price))));
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)), "taint_note");
- StableTable(l);
+ PriceTable(l);
l.AddSeparator();
CandleChart.AddPicker(l, "web3", Assets.PSC, Open, _picked);
}, r =>
{
r.AddHeader(L.T("trade.h"));
- AmountRow(r);
- TradeRows(r, Venue.Dex);
+ SwapBlock(r);
if (Unlock.BlackUnlocked && BlackMarket.DeskOpen(StoreReputation.GetBMReputation())) PoneroBlock(r);
});
}
@@ -2311,18 +2370,14 @@ namespace Decentralized
Open();
}
- /// 交易:Poinbase 是托管账户,直接成交;DEX 是合约调用,要在硬件钱包上签名。
+ /// 交易:Poinbase 是托管账户,直接成交。
private static void Trade(Pool p, bool buy)
{
if (!ReadAmount(out double v)) return;
- var w = p.Venue == Venue.Dex ? Session.Data.Hot : Session.Data.Poinbase;
- string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
+ var w = Session.Data.Poinbase;
+ string pay = buy ? p.Quote : p.Base;
if (w.Avail(pay) + 1e-9 < v) { _message = L.T(frozenPay(w, pay) ? "msg.frozen" : "msg.balance"); Open(); return; }
- if (p.Venue != Venue.Dex) { DoTrade(p, buy, v, w); return; }
- double quote = buy ? v / p.Price : v * p.Price;
- _message = L.T("hw.wait");
- Open();
- WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Asset = pay, Amount = v, Summary = L.T("hw.swap", Fmt.Num(v), pay, Fmt.Num(quote), get), Execute = pw => DoTrade(p, buy, v, pw) });
+ DoTrade(p, buy, v, w);
}
private static void DoTrade(Pool p, bool buy, double v, Wallet w)
diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs
index ef1a175..f9371cf 100644
--- a/tests/Decentralized.Domain.Tests/Program.cs
+++ b/tests/Decentralized.Domain.Tests/Program.cs
@@ -318,17 +318,38 @@ internal static class Program
Check(n.Anchor(new MarketEvent[0]) > Tuning.PscStartPrice * 1.05, "锚随之上移");
});
- Test("市场:其他交易对每晚被套利拉向参考价,三角价差收敛", () =>
+ Test("市场:其他交易对每晚被套利拉向参考价,价差收敛", () =>
{
var m = Market.CreateDefault(1);
- var crt = m.Find(Venue.Dex, Assets.PSC, Assets.CRT);
- crt.SetPrice(130, crt.QuoteReserve);
+ var dexPsc = m.DexPool(Assets.PSC);
+ dexPsc.SetPrice(130, dexPsc.QuoteReserve);
for (int d = 1; d <= 5; d++) m.Night(new MarketEvent[0], new Rng((ulong)d), 1, a => 1);
- double gap = Math.Abs(crt.Price / m.Price - 1);
- Check(gap < 0.03, $"五晚后 PSC/CRT 与参考价相差 {gap:P1}");
- var crtCrc = m.Find(Venue.Dex, Assets.CRT, Assets.CRC);
+ double gap = Math.Abs(dexPsc.Price / m.Price - 1);
+ Check(gap < 0.03, $"五晚后 DEX 的 PSC/CRC 与参考价相差 {gap:P1}");
+ var crtCrc = m.DexPool(Assets.CRT);
m.Night(new MarketEvent[0], new Rng(9), 1, a => a == Assets.CRT ? 0.9 : 1);
Check(crtCrc.Price < 0.98, $"CRT 脱锚时 CRT/CRC 向 0.9 靠拢:{crtCrc.Price:0.000}");
+ var m2 = Market.CreateDefault(1);
+ var spark = m2.DexPool(Assets.SPARK);
+ m2.Night(new MarketEvent[0], new Rng(11), 1, a => a == Assets.SPARK ? 20 : 1);
+ Check(spark.Price > Tuning.SparkStart * 1.3, $"SPARK/CRC 向 SPARK 价格靠拢:{spark.Price:0.00}");
+ });
+
+ Test("市场:任意两币互换经 CRC 路由,两跳付两次手续费", () =>
+ {
+ var m = Market.CreateDefault(1);
+ var oneHop = m.Quote(Assets.CRC, Assets.PSC, 100);
+ var twoHop = m.Quote(Assets.CRA, Assets.PSC, 100);
+ Check(oneHop != null && oneHop.Hops == 1 && twoHop != null && twoHop.Hops == 2, "CRC 在一端走一个池,否则两个池");
+ Check(m.Quote(Assets.PSC, Assets.PSC, 1) == null && m.Quote(Assets.CRC, Assets.CRC, 1) == null && m.Quote(Assets.CRC, Assets.Ponero, 1) == null, "同币、没有池没有路由");
+ Check(twoHop.Impact > oneHop.Impact && twoHop.Impact > Tuning.DexFee * 1.9, $"两跳损失更大:{oneHop.Impact:P2} 对 {twoHop.Impact:P2}");
+ double expect = m.DexPool(Assets.PSC).BaseOut(m.DexPool(Assets.CRA).QuoteOut(100));
+ Check(Near(twoHop.Out, expect, 1e-9), "两跳的数量 = 先卖成 CRC 再买");
+ double got = m.Swap(Assets.CRA, Assets.PSC, 100);
+ Check(Near(got, twoHop.Out, 1e-9), "成交数量与报价一致");
+ Check(m.DexPool(Assets.CRA).Price < 1 && m.DexPool(Assets.PSC).Price > Tuning.PscStartPrice, "成交推动两个池的价格");
+ var big = Market.CreateDefault(1).Quote(Assets.CRC, Assets.SPARK, 20000);
+ Check(big.Impact > 0.2, $"SPARK 池比较薄,大单滑点大:{big.Impact:P0}");
});
Test("稳定币:CRC 脱锚与减记,CRT 脱锚与冻结概率", () =>