From a3e955ca59a079ce753454d170d68f4d631558fb Mon Sep 17 00:00:00 2001 From: ZHU MINCHAO <48154958+MinchaoZhu@users.noreply.github.com> Date: Thu, 5 Nov 2026 07:22:01 +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=E5=9B=9B=EF=BC=89=EF=BC=9A=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E9=A1=B5=EF=BC=88=E5=85=A8=E5=B1=80=E8=B4=A6=E6=9C=AC?= =?UTF-8?q?=EF=BC=89=E3=80=81Web3=20=E9=A1=B5=E7=AD=BE=E6=8A=98=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Decentralized.Domain/Ledger.cs | 37 ++++++++++ src/Decentralized.Domain/Tuning.cs | 2 + src/Decentralized.Domain/TuningTable.cs | 2 + src/Decentralized/Funds.cs | 2 + .../ModFiles/Localization/de.csv | 9 +++ .../ModFiles/Localization/en.csv | 9 +++ .../ModFiles/Localization/es.csv | 9 +++ .../ModFiles/Localization/fr.csv | 9 +++ .../ModFiles/Localization/ja.csv | 9 +++ .../ModFiles/Localization/pl.csv | 9 +++ .../ModFiles/Localization/ru.csv | 9 +++ .../ModFiles/Localization/zh.csv | 9 +++ src/Decentralized/Night.cs | 1 + src/Decentralized/Session.cs | 2 + src/Decentralized/TerminalWindow.cs | 71 +++++++++++++++++-- src/Decentralized/WalletWindow.cs | 1 + tests/Decentralized.Domain.Tests/Program.cs | 15 ++++ 17 files changed, 199 insertions(+), 6 deletions(-) create mode 100644 src/Decentralized.Domain/Ledger.cs diff --git a/src/Decentralized.Domain/Ledger.cs b/src/Decentralized.Domain/Ledger.cs new file mode 100644 index 0000000..fe4d3c1 --- /dev/null +++ b/src/Decentralized.Domain/Ledger.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Decentralized.Domain +{ + /// 浏览器里的一条链上转账:区块、天、转出方、转入方、币种、数额、手续费。 + public sealed class LedgerEntry + { + public long Block; + public int Day; + public string From, To, Asset; + public double Amount, Fee; + } + + /// + /// 全局账本:只记真实发生的转账(玩家的转出和转入、矿机收益、出金),不生成 NPC 流水;最新的在前,超过 条丢最旧的。 + /// + public sealed class Ledger + { + public List Entries = new List(); + + public void Add(LedgerEntry e) + { + if (e == null || e.Amount <= 0) return; + Entries.Insert(0, e); + if (Entries.Count > Tuning.LedgerMax) Entries.RemoveRange(Tuning.LedgerMax, Entries.Count - Tuning.LedgerMax); + } + + /// 某个地址相关(转出或转入)的记录,最新的在前,最多 limit 条。address 为空时返回全部最近的。 + public List Of(string address, int limit) + { + if (string.IsNullOrWhiteSpace(address)) return Entries.Take(limit).ToList(); + return Entries.Where(e => ChainAddress.Same(e.From, address) || ChainAddress.Same(e.To, address)).Take(limit).ToList(); + } + } +} diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index a38a5bf..5f90c4a 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -61,6 +61,8 @@ namespace Decentralized.Domain public const double CraRugPerDepeg = 1; /// Poinbase 出金到钱包的确认动画:基础秒数,加每个确认的秒数(确认数同 PLEDGER 的 TxConfirm*)。 public const double FundsBaseSecs = 1.2, FundsConfSecs = 0.6; + /// 浏览器账本最多保留的转账条数,一次最多显示的条数。 + public const int LedgerMax = 200, LedgerShow = 12; public const double CraSpiralWeekly = 0.03; public const int CraWarnLeadMin = 4, CraWarnLeadMax = 7; public static readonly double[] CraSpiralPath = { 0.85, 0.30, 0.02 }; diff --git a/src/Decentralized.Domain/TuningTable.cs b/src/Decentralized.Domain/TuningTable.cs index baf6abd..e8d52e8 100644 --- a/src/Decentralized.Domain/TuningTable.cs +++ b/src/Decentralized.Domain/TuningTable.cs @@ -244,6 +244,8 @@ namespace Decentralized.Domain ["IntelBriefPrice"] = Tuning.IntelBriefPrice, ["IntelFake"] = Tuning.IntelFake, ["IntelRep"] = Tuning.IntelRep, + ["LedgerMax"] = Tuning.LedgerMax, + ["LedgerShow"] = Tuning.LedgerShow, ["ManipBase"] = Tuning.ManipBase, ["ManipFree"] = Tuning.ManipFree, ["ManipFreezeDays"] = Tuning.ManipFreezeDays, diff --git a/src/Decentralized/Funds.cs b/src/Decentralized/Funds.cs index bc22aa3..963a4ef 100644 --- a/src/Decentralized/Funds.cs +++ b/src/Decentralized/Funds.cs @@ -97,6 +97,8 @@ namespace Decentralized double net = amt - fee; var own = d.Own(t.To); Wallet.Transfer(pb, own ?? new Wallet(), t.Asset, net, Session.Price); + d.Block += 1; + d.Ledger.Add(new LedgerEntry { Block = d.Block, Day = StoreStation.GetDayCounter(), From = d.Address("poinbase"), To = t.To, Asset = t.Asset, Amount = net, Fee = fee }); if (own != null) { TxRules.Record(own.History, new TxRecord { Addr = d.Address("poinbase"), Asset = t.Asset, Amount = net, Day = StoreStation.GetDayCounter(), In = true }); diff --git a/src/Decentralized/ModFiles/Localization/de.csv b/src/Decentralized/ModFiles/Localization/de.csv index 30e343c..4d8c21f 100644 --- a/src/Decentralized/ModFiles/Localization/de.csv +++ b/src/Decentralized/ModFiles/Localization/de.csv @@ -930,3 +930,12 @@ funds.err.addr,Ungültige Adresse. funds.err.self,Das ist die Einzahlungsadresse von Poinbase. funds.err.known,Diese Adresse akzeptiert nur Zahlungen gegen Rechnung. funds.err.fee,Der Betrag deckt die Gebühr nicht. +web3.sub.explorer,Explorer +explorer.search,Adresse +explorer.all,Neueste +explorer.none,Keine Überweisungen. +explorer.col.block,Block +explorer.col.flow,Von → An +explorer.col.amount,Betrag +explorer.net,Netzwerk +explorer.mixer,Mixer diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index abb4281..9855b74 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -930,3 +930,12 @@ funds.err.addr,Invalid address. funds.err.self,That is the Poinbase deposit address. funds.err.known,That address only accepts payments against a bill. funds.err.fee,The amount does not cover the fee. +web3.sub.explorer,Explorer +explorer.search,Address +explorer.all,Latest +explorer.none,No transfers. +explorer.col.block,Block +explorer.col.flow,From → To +explorer.col.amount,Amount +explorer.net,Network +explorer.mixer,Mixer diff --git a/src/Decentralized/ModFiles/Localization/es.csv b/src/Decentralized/ModFiles/Localization/es.csv index c27169b..1a5ffaf 100644 --- a/src/Decentralized/ModFiles/Localization/es.csv +++ b/src/Decentralized/ModFiles/Localization/es.csv @@ -930,3 +930,12 @@ funds.err.addr,Dirección no válida. funds.err.self,Esa es la dirección de depósito de Poinbase. funds.err.known,Esa dirección solo acepta pagos contra una factura. funds.err.fee,La cantidad no cubre la comisión. +web3.sub.explorer,Explorador +explorer.search,Dirección +explorer.all,Recientes +explorer.none,Sin transferencias. +explorer.col.block,Bloque +explorer.col.flow,De → A +explorer.col.amount,Cantidad +explorer.net,Red +explorer.mixer,Mezclador diff --git a/src/Decentralized/ModFiles/Localization/fr.csv b/src/Decentralized/ModFiles/Localization/fr.csv index 06365c9..35c1a16 100644 --- a/src/Decentralized/ModFiles/Localization/fr.csv +++ b/src/Decentralized/ModFiles/Localization/fr.csv @@ -930,3 +930,12 @@ funds.err.addr,Adresse invalide. funds.err.self,C'est l'adresse de dépôt de Poinbase. funds.err.known,Cette adresse n'accepte que les paiements d'une facture. funds.err.fee,Le montant ne couvre pas les frais. +web3.sub.explorer,Explorateur +explorer.search,Adresse +explorer.all,Récent +explorer.none,Aucun transfert. +explorer.col.block,Bloc +explorer.col.flow,De → Vers +explorer.col.amount,Montant +explorer.net,Réseau +explorer.mixer,Mixeur diff --git a/src/Decentralized/ModFiles/Localization/ja.csv b/src/Decentralized/ModFiles/Localization/ja.csv index eef7d69..e05dcad 100644 --- a/src/Decentralized/ModFiles/Localization/ja.csv +++ b/src/Decentralized/ModFiles/Localization/ja.csv @@ -930,3 +930,12 @@ funds.err.addr,アドレスが正しくありません。 funds.err.self,Poinbase 自身の入金アドレスです。 funds.err.known,そのアドレスは請求書に対する支払いのみ受け付けます。 funds.err.fee,金額が手数料に満たません。 +web3.sub.explorer,エクスプローラー +explorer.search,アドレス +explorer.all,最新 +explorer.none,送金履歴はありません。 +explorer.col.block,ブロック +explorer.col.flow,送信元 → 送信先 +explorer.col.amount,数量 +explorer.net,ネットワーク +explorer.mixer,ミキサー diff --git a/src/Decentralized/ModFiles/Localization/pl.csv b/src/Decentralized/ModFiles/Localization/pl.csv index e0a550c..9275eb6 100644 --- a/src/Decentralized/ModFiles/Localization/pl.csv +++ b/src/Decentralized/ModFiles/Localization/pl.csv @@ -930,3 +930,12 @@ funds.err.addr,Nieprawidłowy adres. funds.err.self,To adres wpłat Poinbase. funds.err.known,Ten adres przyjmuje tylko płatności za rachunek. funds.err.fee,Kwota nie pokrywa opłaty. +web3.sub.explorer,Eksplorator +explorer.search,Adres +explorer.all,Najnowsze +explorer.none,Brak transferów. +explorer.col.block,Blok +explorer.col.flow,Od → Do +explorer.col.amount,Kwota +explorer.net,Sieć +explorer.mixer,Mikser diff --git a/src/Decentralized/ModFiles/Localization/ru.csv b/src/Decentralized/ModFiles/Localization/ru.csv index 0c472ee..317d185 100644 --- a/src/Decentralized/ModFiles/Localization/ru.csv +++ b/src/Decentralized/ModFiles/Localization/ru.csv @@ -930,3 +930,12 @@ funds.err.addr,Неверный адрес. funds.err.self,Это адрес пополнения Poinbase. funds.err.known,Этот адрес принимает только оплату по счёту. funds.err.fee,Сумма не покрывает комиссию. +web3.sub.explorer,Обозреватель +explorer.search,Адрес +explorer.all,Последние +explorer.none,Переводов нет. +explorer.col.block,Блок +explorer.col.flow,От → Кому +explorer.col.amount,Сумма +explorer.net,Сеть +explorer.mixer,Миксер diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv index f83b0df..938fa05 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -929,3 +929,12 @@ funds.err.addr,地址格式或校验位不对。 funds.err.self,这是 Poinbase 自己的充值地址。 funds.err.known,该地址只接受对应账单的付款。 funds.err.fee,数额不够付手续费。 +web3.sub.explorer,浏览器 +explorer.search,地址 +explorer.all,最近 +explorer.none,没有转账记录。 +explorer.col.block,区块 +explorer.col.flow,转出 → 转入 +explorer.col.amount,数额 +explorer.net,全网 +explorer.mixer,混币器 diff --git a/src/Decentralized/Night.cs b/src/Decentralized/Night.cs index 4590fba..3433f18 100644 --- a/src/Decentralized/Night.cs +++ b/src/Decentralized/Night.cs @@ -87,6 +87,7 @@ namespace Decentralized var w = string.IsNullOrEmpty(addr) ? null : d.Own(addr); if (w == null) { lost += share; continue; } w.Add(Assets.PSC, share); + d.Ledger.Add(new LedgerEntry { Block = d.Block, Day = StoreStation.GetDayCounter(), From = "network", To = w.Address, Asset = Assets.PSC, Amount = share }); } if (lost > 1e-9) { diff --git a/src/Decentralized/Session.cs b/src/Decentralized/Session.cs index f20ec0c..824ef15 100644 --- a/src/Decentralized/Session.cs +++ b/src/Decentralized/Session.cs @@ -31,6 +31,8 @@ namespace Decentralized public Stablecoins Stable = new Stablecoins(); /// 革命军 CRA 质押池里玩家各钱包的仓位。 public Staking Staking = new Staking(); + /// 浏览器页的全局转账账本。 + public Ledger Ledger = new Ledger(); /// Poinbase 永续合约账本(保证金用 Poinbase 账户的 CRC)。 public PerpBook Perps = new PerpBook(); /// 预测市场:盘口、持仓、观测记录(下注资金用 Poinbase 账户的 CRC)。 diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs index 17571cb..097b941 100644 --- a/src/Decentralized/TerminalWindow.cs +++ b/src/Decentralized/TerminalWindow.cs @@ -1279,19 +1279,31 @@ namespace Decentralized if (_web3Tab == Web3Tab.Mixer && !Unlock.MixerUnlocked) _web3Tab = Web3Tab.Trade; if (_web3Tab == Web3Tab.Black && !Unlock.BlackUnlocked) _web3Tab = Web3Tab.Trade; if (_web3Tab == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter())) _web3Tab = Web3Tab.Trade; - b.BeginRow(4f); - foreach (Web3Tab t in Enum.GetValues(typeof(Web3Tab))) + var tabs = Enum.GetValues(typeof(Web3Tab)).Cast() + .Where(t => !((t == Web3Tab.Mixer && !Unlock.MixerUnlocked) || (t == Web3Tab.Black && !Unlock.BlackUnlocked) || (t == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter())))) + .ToList(); + // 页签按字数估宽,超出一行就折到下一行;连接钱包的控件放在最后一行右端,放不下就单独一行 + float avail = ScreenW - 40f, used = 0f; + Func px = txt => txt.Sum(c => c > 0x2E80 ? 18f : 9f) + 28f; + bool open = false; + foreach (var t in tabs) { - if ((t == Web3Tab.Mixer && !Unlock.MixerUnlocked) || (t == Web3Tab.Black && !Unlock.BlackUnlocked) || (t == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter()))) continue; - var tab = t; string name = L.T("web3.sub." + t.ToString().ToLowerInvariant()); + float wpx = px(name); + if (open && used + wpx > avail) { b.End(); open = false; used = 0f; } + if (!open) { b.BeginRow(4f); open = true; } + used += wpx + 4f; + var tab = t; PledgerSkin.Pick(b, name, tab == _web3Tab, () => { _web3Tab = tab; _message = null; _edit = null; Open(); }, _picked); } + float widget = 120f + (Session.Data.Connected ? px(L.T("web3.conn", WalletWindow.NameOf(Session.Data.Hot), ChainAddress.Short(Session.Data.Hot.Address))) : px(L.T("web3.connect"))); + if (open && used + widget > avail) { b.End(); open = false; } + if (!open) b.BeginRow(4f); b.AddFlexibleSpace(); ConnectWidget(b); b.End(); ConnectPicker(b); - if (!Session.Data.Connected) b.AddLabel(L.T("web3.err.noconn"), "term_note"); + if (!Session.Data.Connected && _web3Tab != Web3Tab.Explorer) b.AddLabel(L.T("web3.err.noconn"), "term_note"); if (_web3Tab == Web3Tab.Stake) { StakePage(b); return; } if (_web3Tab == Web3Tab.Predict) { PredictPage(b); return; } if (_web3Tab == Web3Tab.Ponzi) { YieldPage(b); return; } @@ -1299,6 +1311,7 @@ namespace Decentralized if (_web3Tab == Web3Tab.Mining) { ReportPage(b); return; } if (_web3Tab == Web3Tab.Mixer) { MixerPage(b); return; } if (_web3Tab == Web3Tab.Black) { BlackPage(b); return; } + if (_web3Tab == Web3Tab.Explorer) { ExplorerPage(b); return; } } Columns(b, l => { @@ -1352,6 +1365,52 @@ namespace Decentralized b.End(); } + private static string _explorerQuery = ""; + private static readonly float[] ExplWBase = new[] { 80f, 300f, 160f }; + private static float[] ExplW => Sc(ExplWBase); + private static readonly TextAnchor[] ExplAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleLeft, TextAnchor.MiddleRight }; + + /// 地址的显示名:自己的钱包、Poinbase、混币器、全网矿池、黑市卖家,其余是缩写。 + private static string AddressLabel(string a) + { + var d = Session.Data; + if (string.IsNullOrEmpty(a)) return "—"; + if (a == "network") return L.T("explorer.net"); + var own = d.Own(a); + if (own != null) return WalletWindow.NameOf(own); + if (ChainAddress.Same(a, d.Address("poinbase"))) return "Poinbase"; + if (d.Addresses.TryGetValue("mixer", out var mx) && ChainAddress.Same(a, mx)) return L.T("explorer.mixer"); + return ChainAddress.Short(a); + } + + /// 浏览器:公共数据,不需要连接钱包。查任意地址的转账,只显示真实发生的。 + private static void ExplorerPage(CustomUIBuilder b) + { + var d = Session.Data; + b.BeginPanel(RightW - 30f, -1f, TextAnchor.MiddleCenter, 0f, 0f); + _clear.Add(b.Last.image); + b.AddInput(L.T("explorer.search"), _explorerQuery, s => _explorerQuery = s, "explorer", s => { _explorerQuery = s; Open(); }); + b.End(); + if (d.Wallets.Count > 0) + { + b.BeginRow(4f); + PledgerSkin.Pick(b, L.T("explorer.all"), string.IsNullOrWhiteSpace(_explorerQuery), () => { _explorerQuery = ""; Open(); }, _picked); + foreach (var ow in d.Wallets) + { + var own = ow; + PledgerSkin.Pick(b, WalletWindow.NameOf(own), ChainAddress.Same(_explorerQuery, own.Address), () => { _explorerQuery = own.Address; Open(); }, _picked); + } + b.End(); + } + b.AddSeparator(); + var rows = d.Ledger.Of(_explorerQuery, Tuning.LedgerShow); + if (rows.Count == 0) { b.AddLabel(L.T("explorer.none"), "explorer_none_dim"); return; } + TableHeader(b, ExplW.Sum() + 8f, ExplW, ExplAligns, L.T("explorer.col.block"), L.T("explorer.col.flow"), L.T("explorer.col.amount")); + foreach (var e in rows) + TableRow(b, ExplW.Sum() + 8f, ExplW, ExplAligns, + new[] { "#" + e.Block, AddressLabel(e.From) + " → " + AddressLabel(e.To), Fmt.Num(e.Amount) + " " + e.Asset }); + } + private enum BlackTab { Buy, Sell, Orders } private static BlackTab _blackTab = BlackTab.Buy; private static PayMode _mode = PayMode.Escrow; @@ -2184,7 +2243,7 @@ namespace Decentralized Open(); } - private enum Web3Tab { Trade, Stake, Airdrop, Predict, Ponzi, Mining, Mixer, Black } + private enum Web3Tab { Trade, Stake, Airdrop, Predict, Ponzi, Mining, Mixer, Black, Explorer } private static Web3Tab _web3Tab = Web3Tab.Trade; private static string _mixAsset = Assets.PSC; private static int _mixRounds = 2; diff --git a/src/Decentralized/WalletWindow.cs b/src/Decentralized/WalletWindow.cs index 7aad9a6..c6fb5b3 100644 --- a/src/Decentralized/WalletWindow.cs +++ b/src/Decentralized/WalletWindow.cs @@ -544,6 +544,7 @@ namespace Decentralized if (tx.To != null) { TxRules.Record(w.History, new TxRecord { Addr = tx.To, Asset = tx.Asset, Amount = amt, Day = day }); + d.Ledger.Add(new LedgerEntry { Block = tx.Block, Day = day, From = w.Address, To = tx.To, Asset = tx.Asset, Amount = amt, Fee = tx.FeeAsset == tx.Asset ? tx.Fee : 0 }); var mine = d.Own(tx.To); if (mine != null) TxRules.Record(mine.History, new TxRecord { Addr = w.Address, Asset = tx.Asset, Amount = amt, Day = day, In = true }); // 地址投毒:链上公开的转账被人盯上,过一两天会收到一笔来自相似地址的 0 币转入 diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index 68c3c61..a2d8033 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -410,6 +410,21 @@ internal static class Program Check(Stablecoins.PriceOfRatio(Tuning.CraSafeRatio) == 1 && Near(Stablecoins.PriceOfRatio(Tuning.CraCrashRatio), 1 - Tuning.CraDiscountMax, 1e-9), "价格曲线:安全线 1,崩盘线折价 15%"); }); + Test("浏览器账本:最新在前、按地址过滤、超过上限丢最旧", () => + { + var l = new Ledger(); + string a = ChainAddress.Make(1), b = ChainAddress.Make(2), c = ChainAddress.Make(3); + l.Add(new LedgerEntry { Block = 1, From = a, To = b, Asset = "PSC", Amount = 1 }); + l.Add(new LedgerEntry { Block = 2, From = b, To = c, Asset = "CRC", Amount = 5, Fee = 1 }); + l.Add(new LedgerEntry { Block = 3, From = c, To = a, Asset = "PSC", Amount = 0 }); + Check(l.Entries.Count == 2 && l.Entries[0].Block == 2, "数额为 0 不记;最新的在前"); + Check(l.Of(b, 10).Count == 2 && l.Of(a, 10).Count == 1 && l.Of(a.ToUpperInvariant(), 10).Count == 1 && l.Of("", 10).Count == 2, "按地址过滤,大小写不敏感,空地址是最近的全部"); + for (int i = 0; i < Tuning.LedgerMax + 30; i++) l.Add(new LedgerEntry { Block = 100 + i, From = a, To = b, Asset = "PSC", Amount = 1 }); + Check(l.Entries.Count == Tuning.LedgerMax && l.Entries[0].Block == 100 + Tuning.LedgerMax + 29 && l.Of("", 5).Count == 5, "上限 200 条"); + var back = Newtonsoft.Json.JsonConvert.DeserializeObject(Newtonsoft.Json.JsonConvert.SerializeObject(l)); + Check(back.Entries.Count == l.Entries.Count && back.Entries[0].Block == l.Entries[0].Block, "存档往返"); + }); + Test("CRA 长期行为:200 天里有的局崩盘有的不崩,质押越多崩得越多,崩盘前有预兆", () => { Func run = staked =>