第十阶段 stage 2(四):浏览器页(全局账本)、Web3 页签折行

This commit is contained in:
ZHU MINCHAO committed 2026-11-05 07:22:01 +08:00
1 parent e3e5ec2d6e
commit a3e955ca59
17 files changed
+199 -6

No files matched your search

+37
View File
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Decentralized.Domain
{
/// <summary>浏览器里的一条链上转账:区块、天、转出方、转入方、币种、数额、手续费。</summary>
public sealed class LedgerEntry
{
public long Block;
public int Day;
public string From, To, Asset;
public double Amount, Fee;
}
/// <summary>
/// 全局账本:只记真实发生的转账(玩家的转出和转入、矿机收益、出金),不生成 NPC 流水;最新的在前,超过 <see cref="Tuning.LedgerMax"/> 条丢最旧的。
/// </summary>
public sealed class Ledger
{
public List<LedgerEntry> Entries = new List<LedgerEntry>();
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);
}
/// <summary>某个地址相关(转出或转入)的记录,最新的在前,最多 limit 条。address 为空时返回全部最近的。</summary>
public List<LedgerEntry> 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();
}
}
}
+2
View File
@@ -61,6 +61,8 @@ namespace Decentralized.Domain
public const double CraRugPerDepeg = 1;
/// <summary>Poinbase 出金到钱包的确认动画:基础秒数,加每个确认的秒数(确认数同 PLEDGER 的 TxConfirm*)。</summary>
public const double FundsBaseSecs = 1.2, FundsConfSecs = 0.6;
/// <summary>浏览器账本最多保留的转账条数,一次最多显示的条数。</summary>
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 };
+2
View File
@@ -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,
+2
View File
@@ -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 });
@@ -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
1 # Probably Decentralized localization table: German (de)
930 funds.err.self
931 funds.err.known
932 funds.err.fee
933 web3.sub.explorer
934 explorer.search
935 explorer.all
936 explorer.none
937 explorer.col.block
938 explorer.col.flow
939 explorer.col.amount
940 explorer.net
941 explorer.mixer
@@ -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
1 # Probably Decentralized localization table: English (en)
930 funds.err.self
931 funds.err.known
932 funds.err.fee
933 web3.sub.explorer
934 explorer.search
935 explorer.all
936 explorer.none
937 explorer.col.block
938 explorer.col.flow
939 explorer.col.amount
940 explorer.net
941 explorer.mixer
@@ -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
1 # Probably Decentralized localization table: Spanish (es)
930 funds.err.self
931 funds.err.known
932 funds.err.fee
933 web3.sub.explorer
934 explorer.search
935 explorer.all
936 explorer.none
937 explorer.col.block
938 explorer.col.flow
939 explorer.col.amount
940 explorer.net
941 explorer.mixer
@@ -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
1 # Probably Decentralized localization table: French (fr)
930 funds.err.self
931 funds.err.known
932 funds.err.fee
933 web3.sub.explorer
934 explorer.search
935 explorer.all
936 explorer.none
937 explorer.col.block
938 explorer.col.flow
939 explorer.col.amount
940 explorer.net
941 explorer.mixer
@@ -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,ミキサー
Can't render this file because it contains an unexpected character in line 507 and column 24.
@@ -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
1 # Probably Decentralized localization table: Polish (pl)
930 funds.err.self
931 funds.err.known
932 funds.err.fee
933 web3.sub.explorer
934 explorer.search
935 explorer.all
936 explorer.none
937 explorer.col.block
938 explorer.col.flow
939 explorer.col.amount
940 explorer.net
941 explorer.mixer
@@ -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,Миксер
1 # Probably Decentralized localization table: Russian (ru)
930 funds.err.self
931 funds.err.known
932 funds.err.fee
933 web3.sub.explorer
934 explorer.search
935 explorer.all
936 explorer.none
937 explorer.col.block
938 explorer.col.flow
939 explorer.col.amount
940 explorer.net
941 explorer.mixer
@@ -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,混币器
1 # Probably Decentralized 本地化表:简体中文(zh)
929 funds.err.self
930 funds.err.known
931 funds.err.fee
932 web3.sub.explorer
933 explorer.search
934 explorer.all
935 explorer.none
936 explorer.col.block
937 explorer.col.flow
938 explorer.col.amount
939 explorer.net
940 explorer.mixer
+1
View File
@@ -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)
{
+2
View File
@@ -31,6 +31,8 @@ namespace Decentralized
public Stablecoins Stable = new Stablecoins();
/// <summary>革命军 CRA 质押池里玩家各钱包的仓位。</summary>
public Staking Staking = new Staking();
/// <summary>浏览器页的全局转账账本。</summary>
public Ledger Ledger = new Ledger();
/// <summary>Poinbase 永续合约账本(保证金用 Poinbase 账户的 CRC)。</summary>
public PerpBook Perps = new PerpBook();
/// <summary>预测市场:盘口、持仓、观测记录(下注资金用 Poinbase 账户的 CRC)。</summary>
+65 -6
View File
@@ -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<Web3Tab>()
.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<string, float> 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 };
/// <summary>地址的显示名:自己的钱包、Poinbase、混币器、全网矿池、黑市卖家,其余是缩写。</summary>
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);
}
/// <summary>浏览器:公共数据,不需要连接钱包。查任意地址的转账,只显示真实发生的。</summary>
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;
+1
View File
@@ -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 币转入
@@ -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<Ledger>(Newtonsoft.Json.JsonConvert.SerializeObject(l));
Check(back.Entries.Count == l.Entries.Count && back.Entries[0].Block == l.Entries[0].Block, "存档往返");
});
Test("CRA 长期行为:200 天里有的局崩盘有的不崩,质押越多崩得越多,崩盘前有预兆", () =>
{
Func<double, (double crash, double lead)> run = staked =>