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