using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using Decentralized.Domain; using TMPro; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; namespace Decentralized { /// /// 硬件钱包 PLEDGER:每个钱包一台,从终端的钱包页打开,各是一个独立窗口。石墨机身、墨水屏、下面两个物理按键 /// (左 × 拒绝 / 返回,右 ✓ 下一步,签名要按住)。链上的转账都从这里发:终端只开账单(收款地址、币种、金额), /// 玩家在钱包里手输地址、核对、按住 ✓ 签名,然后看着交易进内存池、打包、确认。地址带校验位,输错一位几乎都会被拦下; /// 碰巧合法的错地址没人用,币就没了。DEX 兑换是合约调用,没有收款地址,只要签名。 /// 收款方:有账单的地址(执行账单)、自己的另一个钱包、Poinbase 充值地址(只收 CRC / PSC);其余地址币转进去就回不来。 /// public static class WalletWindow { private const string IdPrefix = "pd_wallet_"; private const float WinW = 452f, WinH = 624f, ScreenW = 384f, ScreenH = 470f, Bezel = 6f; private enum Screen { Home, Send, Review, Broadcast, Done } /// 终端开出的账单:转账(有收款地址)或合约调用(DEX 兑换,没有地址)。付清后用付款的钱包执行。 public sealed class Bill { public string Address, Asset, Summary; public double Amount; public bool Contract; public Action Execute; } /// 当前待付的账单(同一时间一张,新的覆盖旧的)。任何一台钱包都可以付。 public static Bill Pending; private sealed class Tx { public string To, Asset; public double Amount; public Bill Bill; public long Block; public float Start; public bool Finished; } /// 一台硬件钱包的屏幕状态。 private sealed class Device { public string Address; public Screen Screen = Screen.Home; public string To = "", Amount = "", Asset = Assets.PSC, Note; public Tx Tx; public string Id => IdPrefix + Address; public Wallet Wallet => Session.Data.Own(Address); } private static readonly Dictionary Devices = new Dictionary(); 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 }; 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), Dim = new Color(0.38f, 0.39f, 0.40f), Alert = new Color(0.62f, 0.16f, 0.12f); public static string NameOf(Wallet w) { int i = Session.Data.Wallets.IndexOf(w); return i == 0 ? L.T("hw.wallet.main") : L.T("hw.wallet.n", i + 1); } private static Device Dev(Wallet w) { if (!Devices.TryGetValue(w.Address, out var dev)) Devices[w.Address] = dev = new Device { Address = w.Address }; return dev; } private static bool IsOpen(Device dev) => CustomUIManager.Instance != null && CustomUIManager.Instance.IsOpen(dev.Id); /// 读档 / 新游戏后清掉上一局的设备状态并关窗。 public static void Reset() { foreach (var dev in Devices.Values) if (IsOpen(dev)) CustomUIManager.Instance.CloseWindow(dev.Id); Devices.Clear(); Pending = null; } /// 终端"打开钱包转账" / DEX 签名请求:在当前连接的钱包上打开。转账填好币种和金额,地址留给玩家手输。 public static void OpenForBill(Bill bill) { Pending = bill; var dev = Dev(Session.Data.Hot); if (dev.Tx != null && !dev.Tx.Finished) { Open(dev); return; } dev.Note = null; if (bill.Contract) dev.Screen = Screen.Review; else { dev.Screen = Screen.Send; dev.To = ""; dev.Asset = bill.Asset; dev.Amount = Amt(bill.Amount); } Open(dev); } public static void Open(Wallet w) { if (w != null) Open(Dev(w)); } private static void Open(Device dev) { if (Session.Data == null || dev.Wallet == null) return; WalletDriver.Ensure(); var b = CustomUIManager.Window(dev.Id, "PLEDGER"); if (b == null) return; 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)); // 屏幕外面一圈黑色边框,里面是墨水屏 b.BeginPanel(ScreenW + Bezel * 2, ScreenH + Bezel * 2, TextAnchor.MiddleCenter, Bezel + 3f, 0f); var bezel = b.Last.image; b.BeginPanel(ScreenW, ScreenH, TextAnchor.UpperLeft, 14f, 6f); var screen = b.CurrentContainer; var paper = b.Last.image; switch (dev.Screen) { case Screen.Send: SendScreen(b, dev); break; case Screen.Review: ReviewScreen(b, dev); break; case Screen.Broadcast: BroadcastScreen(b, dev); break; case Screen.Done: DoneScreen(b, dev); break; default: HomeScreen(b, dev); break; } b.AddFlexibleSpace(); if (!string.IsNullOrEmpty(dev.Note)) b.AddLabel(dev.Note, "note"); b.End(); b.End(); // 机身下沿:左 ×,右 ✓,左右对称 b.BeginRow(0f, TextAnchor.MiddleCenter); b.AddIconButton(Items.ModSprite("ui_hw_no"), 60f, () => Reject(dev)); b.BeginPanel(ScreenW + Bezel * 2 - 120f, 60f, TextAnchor.MiddleCenter, 0f, 0f); var plate = b.Last.image; b.End(); b.AddIconButton(Items.ModSprite("ui_hw_ok"), 60f, () => Confirm(dev)); var ok = b.Last.root; b.End(); var win = b.Show(); Log.Guard("硬件钱包外观", () => Skin(win, screen, bezel, paper, plate, ok, dev)); } /// 换上硬件的样子:机身、屏幕边框、墨水屏是自己画的像素九宫格,屏上按钮做成黑底白字的触控键。 private static void Skin(CustomUIWindow win, RectTransform screen, Image bezel, Image paper, Image plate, GameObject ok, Device dev) { if (win?.root == null) return; Slice(win.root.GetComponent(), "ui_hw_shell", 10); Slice(bezel, "ui_hw_bezel", 5); Slice(paper, "ui_hw_paper", 3); // 标题栏和分隔线透明,标题当作机身上的刻字 foreach (var name in new[] { "header", "rule" }) { var img = win.root.transform.Find(name)?.GetComponent(); if (img != null) img.color = new Color(0, 0, 0, 0); } if (win.frame?.titleText != null) { var t = win.frame.titleText; t.color = new Color(0.62f, 0.64f, 0.70f); t.fontStyle = FontStyles.Bold; t.characterSpacing = 18f; t.fontSize = 16f; t.horizontalAlignment = HorizontalAlignmentOptions.Center; } var content = win.frame?.contentRoot?.GetComponent(); if (content != null) content.childAlignment = TextAnchor.UpperCenter; if (plate != null) plate.color = new Color(0, 0, 0, 0); foreach (var t in screen.GetComponentsInChildren(true)) { if (t.GetComponentInParent