Chain drama: fee tiers, congestion, mempool drops, address poisoning, clipboard hijack, phishing; selected-button style
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2b781acd82
commit
dbba8b4376
15 files changed
+357
-20
No files matched your search
@@ -20,7 +20,7 @@ namespace Decentralized
|
||||
public static class WalletWindow
|
||||
{
|
||||
private const string IdPrefix = "pd_wallet_";
|
||||
private const float WinW = 452f, WinH = 624f, ScreenW = 384f, ScreenH = 470f, Bezel = 6f;
|
||||
private const float WinW = 452f, WinH = 694f, ScreenW = 384f, ScreenH = 540f, Bezel = 6f;
|
||||
private enum Screen { Home, Send, Review, Broadcast, Done }
|
||||
|
||||
/// <summary>终端开出的账单:转账(有收款地址)或合约调用(DEX 兑换,没有地址)。付清后用付款的钱包执行。</summary>
|
||||
@@ -43,6 +43,13 @@ namespace Decentralized
|
||||
public long Block;
|
||||
public float Start;
|
||||
public bool Finished;
|
||||
/// <summary>手续费(币的数量)与扣费的币。</summary>
|
||||
public double Fee;
|
||||
public string FeeAsset;
|
||||
/// <summary>各阶段结束的时刻(秒):签名、内存池、打包、每次确认;被挤出内存池时只有前两段再加一段等待。</summary>
|
||||
public float[] Times;
|
||||
public int Confs;
|
||||
public bool Dropped;
|
||||
}
|
||||
|
||||
/// <summary>一台硬件钱包的屏幕状态。</summary>
|
||||
@@ -51,6 +58,7 @@ namespace Decentralized
|
||||
public string Address;
|
||||
public Screen Screen = Screen.Home;
|
||||
public string To = "", Amount = "", Asset = Assets.PSC, Note;
|
||||
public FeeTier Tier = FeeTier.Normal;
|
||||
public Tx Tx;
|
||||
public string Id => IdPrefix + Address;
|
||||
public Wallet Wallet => Session.Data.Own(Address);
|
||||
@@ -59,8 +67,24 @@ namespace Decentralized
|
||||
private static readonly Dictionary<string, Device> Devices = new Dictionary<string, Device>();
|
||||
private static readonly string[] SendAssets = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA, Assets.Ponero };
|
||||
|
||||
// 出块动画各阶段的结束时刻(秒):签名、内存池、打包、三次确认
|
||||
private static readonly float[] Phases = { 0.8f, 2.0f, 3.0f, 3.7f, 4.4f, 5.1f };
|
||||
/// <summary>网络拥堵:夜间结算随机开始,持续一两天,期间手续费变贵、慢档和普通档确认变慢。</summary>
|
||||
public static bool Congested
|
||||
{
|
||||
get { var d = Session.Data; int day = StoreStation.GetDayCounter(); return d != null && day >= d.CongestedFrom && day <= d.CongestedUntil; }
|
||||
}
|
||||
|
||||
/// <summary>手续费折成币的数量。</summary>
|
||||
private static double FeeIn(string asset, FeeTier tier) { double p = Session.Price(asset); return p > 0 ? TxRules.FeeCredits(tier, Congested) / p : 0; }
|
||||
|
||||
/// <summary>出块动画的时间表:签名 0.8 秒,内存池 1.2 秒,打包 1 秒,每次确认 0.6 秒;拥堵时按档位放慢。</summary>
|
||||
private static float[] Timeline(int confs, double slow, bool dropped)
|
||||
{
|
||||
var t = new List<float> { 0.8f, 0.8f + 1.2f * (float)slow };
|
||||
if (dropped) { t.Add(t[1] + 1.5f * (float)slow); return t.ToArray(); }
|
||||
t.Add(t[1] + 1.0f);
|
||||
for (int i = 0; i < confs; i++) t.Add(t[t.Count - 1] + 0.6f * (float)slow);
|
||||
return t.ToArray();
|
||||
}
|
||||
|
||||
private static readonly Color Ink = new Color(0.12f, 0.12f, 0.14f),
|
||||
Key = new Color(0.16f, 0.16f, 0.19f), KeyText = new Color(0.93f, 0.93f, 0.90f),
|
||||
@@ -124,7 +148,7 @@ namespace Decentralized
|
||||
WalletDriver.Ensure();
|
||||
var b = CustomUIManager.Window(dev.Id, "PLEDGER");
|
||||
if (b == null) return;
|
||||
_rows.Clear(); _clear.Clear();
|
||||
_rows.Clear(); _clear.Clear(); _picked.Clear();
|
||||
int idx = Session.Data.Wallets.IndexOf(dev.Wallet);
|
||||
b.SetSize(WinW, WinH).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
|
||||
b.SetPosition(new Vector2(600f + 28f * idx, -24f * idx));
|
||||
@@ -188,8 +212,9 @@ namespace Decentralized
|
||||
foreach (var btn in screen.GetComponentsInChildren<Button>(true))
|
||||
{
|
||||
var img = btn.GetComponent<Image>();
|
||||
if (img != null) img.color = Key;
|
||||
foreach (var t in btn.GetComponentsInChildren<TextMeshProUGUI>(true)) t.color = KeyText;
|
||||
bool on = _picked.Contains(btn.gameObject);
|
||||
if (img != null) { if (on) Slice(img, "ui_hw_sel", 5); else img.color = Key; }
|
||||
foreach (var t in btn.GetComponentsInChildren<TextMeshProUGUI>(true)) { t.color = on ? Ink : KeyText; if (on) t.fontStyle |= FontStyles.Bold; }
|
||||
}
|
||||
foreach (var img in _clear) if (img != null) img.color = new Color(0, 0, 0, 0);
|
||||
// 余额行:整行可点,选中的一行反色高亮
|
||||
@@ -231,6 +256,14 @@ namespace Decentralized
|
||||
private const float RowW = ScreenW - 28f;
|
||||
private static readonly List<(GameObject root, Image image, string asset)> _rows = new List<(GameObject, Image, string)>();
|
||||
private static readonly List<Image> _clear = new List<Image>();
|
||||
private static readonly List<GameObject> _picked = new List<GameObject>();
|
||||
|
||||
/// <summary>屏上的选择按钮(币种、手续费档位):选中的一个画成白底黑框黑字、四角带准星角标。</summary>
|
||||
private static void Pick(CustomUIBuilder b, string text, bool selected, Action onClick)
|
||||
{
|
||||
b.AddButton(text, onClick);
|
||||
if (selected) _picked.Add(b.Last.root);
|
||||
}
|
||||
|
||||
public static string Amt(double v) => v.ToString(v >= 100 ? "0.##" : "0.####", CultureInfo.InvariantCulture);
|
||||
|
||||
@@ -274,6 +307,7 @@ namespace Decentralized
|
||||
b.AddLabel(L.T("hw.total", Fmt.Credits(w.Value(Session.Price))));
|
||||
if (w.Taint > 0.5) b.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
|
||||
if (Pending != null) b.AddLabel(Pending.Contract ? L.T("hw.pending.sign", Pending.Summary) : L.T("hw.pending.bill", Pending.Summary));
|
||||
if (Congested) b.AddLabel(L.T("hw.congested"), "congest_note");
|
||||
b.AddSpace(6f);
|
||||
b.BeginRow();
|
||||
b.AddButton(L.T("hw.send"), () => Go(dev, Screen.Send));
|
||||
@@ -291,13 +325,19 @@ namespace Decentralized
|
||||
_clear.Add(b.Last.image);
|
||||
b.AddInput("0x", dev.To, s => dev.To = s, "to", s => dev.To = s);
|
||||
b.End();
|
||||
b.AddButton(L.T("hw.paste"), () => { dev.To = Clipboard.Paste(); dev.Note = null; Open(dev); });
|
||||
b.AddButton(L.T("hw.paste"), () =>
|
||||
{
|
||||
string clip = Clipboard.Paste();
|
||||
// 被投毒的钱包:固件里的木马有时把粘贴的地址换成长得很像的骗子地址
|
||||
if (w.Poisoned && ChainAddress.Valid(clip) && Session.Data.Rng.Chance(Tuning.TxHijackChance)) { clip = ChainAddress.Lookalike(clip, Session.Data.Rng); Log.Info($"剪贴板木马:{Clipboard.Paste()} → {clip}"); }
|
||||
dev.To = clip; dev.Note = null; Open(dev);
|
||||
});
|
||||
b.End();
|
||||
b.BeginRow();
|
||||
foreach (var a in SendAssets)
|
||||
{
|
||||
var asset = a;
|
||||
b.AddButton(asset == dev.Asset ? $"[{asset}]" : asset, () => { dev.Asset = asset; Open(dev); });
|
||||
Pick(b, asset, asset == dev.Asset, () => { dev.Asset = asset; Open(dev); });
|
||||
}
|
||||
b.End();
|
||||
b.AddLabel(L.T("hw.avail", dev.Asset, Amt(w.Avail(dev.Asset))), "avail_dim");
|
||||
@@ -308,6 +348,17 @@ namespace Decentralized
|
||||
b.AddLabel(L.T("hw.bill", Pending.Summary, Amt(Pending.Amount), Pending.Asset), "bill_dim");
|
||||
b.AddButton(L.T("hw.fill"), () => { dev.Asset = Pending.Asset; dev.Amount = Amt(Pending.Amount); Open(dev); });
|
||||
}
|
||||
// 最近转账:点一下填入地址(地址投毒的 0 币转入也混在这里,缩写看起来一样)
|
||||
var recent = w.History.GroupBy(r => r.Addr + (r.In ? "<" : ">")).Select(g => g.First()).Take(4).ToList();
|
||||
if (recent.Count > 0)
|
||||
{
|
||||
b.AddLabel(L.T("hw.recent"), "recent_dim");
|
||||
foreach (var r in recent)
|
||||
{
|
||||
var rec = r;
|
||||
b.AddButton(L.T(rec.In ? "hw.recent.in" : "hw.recent.out", ChainAddress.Short(rec.Addr), Amt(rec.Amount), rec.Asset), () => { dev.To = rec.Addr; dev.Note = null; Open(dev); });
|
||||
}
|
||||
}
|
||||
b.AddLabel(L.T("hw.next"), "hint_dim");
|
||||
}
|
||||
|
||||
@@ -326,6 +377,7 @@ namespace Decentralized
|
||||
if (ChainAddress.Same(to, w.Address)) return L.T("hw.err.self");
|
||||
if (!ReadAmount(dev, out double v)) return L.T("msg.amount");
|
||||
if (w.Avail(dev.Asset) + 1e-9 < v) return L.T(w.Frozen.ContainsKey(dev.Asset) ? "msg.frozen" : "msg.balance");
|
||||
if (w.Avail(dev.Asset) + 1e-9 < v + FeeIn(dev.Asset, dev.Tier)) return L.T("hw.err.fee");
|
||||
var bill = Match(to);
|
||||
if (bill != null)
|
||||
{
|
||||
@@ -366,9 +418,25 @@ namespace Decentralized
|
||||
if (bill != null) b.AddLabel(L.T("hw.review.bill", bill.Summary));
|
||||
else if (own != null) b.AddLabel(L.T("hw.review.own", NameOf(own)));
|
||||
else if (IsPoinbase(to)) b.AddLabel(L.T("hw.review.poinbase"));
|
||||
else if (dev.Wallet.History.Any(r => ChainAddress.Same(r.Addr, to))) b.AddLabel(L.T("hw.review.history"));
|
||||
else b.AddLabel(L.T("hw.review.unknown"), "warn_note");
|
||||
if (bill != null && v > bill.Amount + 1e-6) b.AddLabel(L.T("hw.review.over", Amt(v - bill.Amount), dev.Asset), "over_note");
|
||||
}
|
||||
// 手续费档位、确认数、拥堵
|
||||
string feeAsset = FeeAssetOf(dev);
|
||||
b.AddSeparator();
|
||||
b.BeginRow();
|
||||
foreach (FeeTier t in Enum.GetValues(typeof(FeeTier)))
|
||||
{
|
||||
var tier = t;
|
||||
string n = L.T("hw.fee." + t.ToString().ToLowerInvariant());
|
||||
Pick(b, n, tier == dev.Tier, () => { dev.Tier = tier; dev.Note = null; Open(dev); });
|
||||
}
|
||||
b.End();
|
||||
double fee = FeeIn(feeAsset, dev.Tier);
|
||||
b.AddLabel(L.T("hw.fee", Amt(fee), feeAsset, Fmt.Num(TxRules.FeeCredits(dev.Tier, Congested))));
|
||||
b.AddLabel(L.T("hw.confs", TxRules.Confirmations(ValueOf(dev))));
|
||||
if (Congested) b.AddLabel(dev.Tier == FeeTier.Slow ? L.T("hw.congested.slow") : L.T("hw.congested"), "congest_note");
|
||||
b.AddSpace(8f);
|
||||
b.AddLabel(L.T("hw.hold"), "hold_dim");
|
||||
}
|
||||
@@ -385,6 +453,15 @@ namespace Decentralized
|
||||
private static void DoneScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
var tx = dev.Tx;
|
||||
if (tx != null && tx.Dropped)
|
||||
{
|
||||
b.AddHeader(L.T("hw.dropped.h"));
|
||||
b.AddLabel(L.T("hw.tx.dropped"));
|
||||
b.AddLabel(L.T("hw.fee", Amt(tx.Fee), tx.FeeAsset, Fmt.Num(tx.Fee * Session.Price(tx.FeeAsset))));
|
||||
b.AddSpace(8f);
|
||||
b.AddButton(L.T("hw.back"), () => { dev.Tx = null; Go(dev, Screen.Home); });
|
||||
return;
|
||||
}
|
||||
b.AddHeader(L.T("hw.done.h"));
|
||||
if (tx != null)
|
||||
{
|
||||
@@ -401,17 +478,32 @@ namespace Decentralized
|
||||
}
|
||||
|
||||
private static float Elapsed(Device dev) => dev.Tx == null ? 0 : Time.unscaledTime - dev.Tx.Start;
|
||||
private static float Progress(Device dev) => Mathf.Clamp01(Elapsed(dev) / Phases[Phases.Length - 1]);
|
||||
private static float Total(Device dev) => dev.Tx?.Times == null ? 0 : dev.Tx.Times[dev.Tx.Times.Length - 1];
|
||||
private static float Progress(Device dev) => Total(dev) <= 0 ? 0 : Mathf.Clamp01(Elapsed(dev) / Total(dev));
|
||||
|
||||
/// <summary>扣手续费的币:转账用转出的币,合约调用用账单上的币。</summary>
|
||||
private static string FeeAssetOf(Device dev) => Pending != null && Pending.Contract ? (Pending.Asset ?? Assets.PSC) : dev.Asset;
|
||||
|
||||
/// <summary>这笔交易的价值(信用点),决定确认数。</summary>
|
||||
private static double ValueOf(Device dev)
|
||||
{
|
||||
if (Pending != null && Pending.Contract) return Pending.Amount * Session.Price(Pending.Asset ?? Assets.PSC);
|
||||
ReadAmount(dev, out double v);
|
||||
return v * Session.Price(dev.Asset);
|
||||
}
|
||||
|
||||
private static string StatusText(Device dev)
|
||||
{
|
||||
float t = Elapsed(dev);
|
||||
if (dev.Tx == null) return "";
|
||||
if (t < Phases[0]) return L.T("hw.tx.signing");
|
||||
if (t < Phases[1]) return L.T("hw.tx.mempool");
|
||||
if (t < Phases[2]) return L.T("hw.tx.packed", dev.Tx.Block);
|
||||
int conf = t < Phases[3] ? 1 : t < Phases[4] ? 2 : 3;
|
||||
return L.T("hw.tx.confirm", dev.Tx.Block, conf, 3);
|
||||
var tx = dev.Tx;
|
||||
if (tx?.Times == null) return "";
|
||||
if (t < tx.Times[0]) return L.T("hw.tx.signing");
|
||||
if (t < tx.Times[1]) return L.T("hw.tx.mempool");
|
||||
if (tx.Dropped) return L.T("hw.tx.stuck");
|
||||
if (t < tx.Times[2]) return L.T("hw.tx.packed", tx.Block);
|
||||
int conf = 1;
|
||||
while (conf < tx.Confs && t >= tx.Times[2 + conf]) conf++;
|
||||
return L.T("hw.tx.confirm", tx.Block, conf, tx.Confs);
|
||||
}
|
||||
|
||||
/// <summary>左键 ×:拒绝签名 / 返回上一页。</summary>
|
||||
@@ -455,7 +547,11 @@ namespace Decentralized
|
||||
var d = Session.Data;
|
||||
d.Block += 1 + d.Rng.Range(0, 3);
|
||||
if (Pending != null && Pending.Contract)
|
||||
{
|
||||
string a = Pending.Asset ?? Assets.PSC;
|
||||
if (dev.Wallet.Avail(a) + 1e-9 < Pending.Amount + FeeIn(a, dev.Tier)) { dev.Note = L.T("hw.err.fee"); Open(dev); return; }
|
||||
dev.Tx = new Tx { Bill = Pending, Block = d.Block, Start = Time.unscaledTime };
|
||||
}
|
||||
else
|
||||
{
|
||||
string err = CheckSend(dev);
|
||||
@@ -464,6 +560,12 @@ namespace Decentralized
|
||||
string to = ChainAddress.Normalize(dev.To);
|
||||
dev.Tx = new Tx { To = to, Asset = dev.Asset, Amount = v, Bill = Match(to), Block = d.Block, Start = Time.unscaledTime };
|
||||
}
|
||||
bool cong = Congested;
|
||||
dev.Tx.FeeAsset = FeeAssetOf(dev);
|
||||
dev.Tx.Fee = FeeIn(dev.Tx.FeeAsset, dev.Tier);
|
||||
dev.Tx.Confs = TxRules.Confirmations(ValueOf(dev));
|
||||
dev.Tx.Dropped = d.Rng.Chance(TxRules.DropChance(dev.Tier, cong));
|
||||
dev.Tx.Times = Timeline(dev.Tx.Confs, TxRules.Slowdown(dev.Tier, cong), dev.Tx.Dropped);
|
||||
if (dev.Tx.Bill != null) Pending = null;
|
||||
dev.Screen = Screen.Broadcast;
|
||||
dev.Note = null;
|
||||
@@ -481,7 +583,25 @@ namespace Decentralized
|
||||
Log.Guard("钱包交易落地", () =>
|
||||
{
|
||||
if (w == null) return;
|
||||
// 手续费先扣(被挤出内存池也照扣)
|
||||
if (tx.Fee > 0 && tx.FeeAsset != null) Wallet.Transfer(w, new Wallet(), tx.FeeAsset, Math.Min(tx.Fee, w.Avail(tx.FeeAsset)), Session.Price);
|
||||
if (tx.Dropped)
|
||||
{
|
||||
// 没成交:账单还在,可以重新付
|
||||
if (tx.Bill != null && Pending == null) Pending = tx.Bill;
|
||||
TerminalWindow.Refresh(L.T("hw.tx.dropped"));
|
||||
return;
|
||||
}
|
||||
int day = StoreStation.GetDayCounter();
|
||||
double amt = tx.To != null ? Math.Min(tx.Amount, w.Avail(tx.Asset)) : 0;
|
||||
if (tx.To != null)
|
||||
{
|
||||
TxRules.Record(w.History, new TxRecord { Addr = tx.To, Asset = tx.Asset, Amount = amt, Day = day });
|
||||
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 币转入
|
||||
else if (d.Rng.Chance(Tuning.TxPoisonChance)) d.PoisonPlans.Add(new PoisonPlan { Wallet = w.Address, Target = tx.To, Day = day + d.Rng.Range(1, Tuning.TxPoisonDelayMax) });
|
||||
}
|
||||
if (tx.Bill != null && !tx.Bill.Contract)
|
||||
{
|
||||
// 多付的部分对方不退
|
||||
@@ -516,7 +636,7 @@ namespace Decentralized
|
||||
foreach (var dev in Devices.Values.ToList())
|
||||
{
|
||||
if (dev.Tx == null || dev.Tx.Finished || dev.Screen != Screen.Broadcast) continue;
|
||||
if (Elapsed(dev) >= Phases[Phases.Length - 1]) { Finish(dev); continue; }
|
||||
if (Elapsed(dev) >= Total(dev)) { Finish(dev); continue; }
|
||||
if (!IsOpen(dev)) continue;
|
||||
var m = CustomUIManager.Instance;
|
||||
m.GetElement(dev.Id, "status")?.SetText(StatusText(dev));
|
||||
|
||||
Reference in new issue
Block a user