Multiple hardware wallets: per-wallet devices, wallets page, Poinbase custodial account, PLEDGER item with supply-chain poisoning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
add61a6b5e
commit
783f73989d
19 files changed
+557
-199
No files matched your search
+197
-136
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Decentralized.Domain;
|
||||
@@ -10,27 +11,28 @@ using UnityEngine.UI;
|
||||
namespace Decentralized
|
||||
{
|
||||
/// <summary>
|
||||
/// 硬件钱包:从终端打开的独立窗口,石墨机身、墨水屏、下面两个物理按键(左 ✕ 拒绝,右 ✓ 确认,签名要按住)。
|
||||
/// 链上的转账都从这里发:终端只开账单(收款地址、币种、金额),玩家在钱包里手输地址、核对、按住 ✓ 签名,
|
||||
/// 然后看着交易进内存池、打包、确认。地址带校验位,输错一位几乎都会被拦下;碰巧合法的错地址没人用,币就没了。
|
||||
/// DEX 兑换是合约调用,没有收款地址,只要签名。
|
||||
/// 硬件钱包 PLEDGER:每个钱包一台,从终端的钱包页打开,各是一个独立窗口。石墨机身、墨水屏、下面两个物理按键
|
||||
/// (左 × 拒绝 / 返回,右 ✓ 下一步,签名要按住)。链上的转账都从这里发:终端只开账单(收款地址、币种、金额),
|
||||
/// 玩家在钱包里手输地址、核对、按住 ✓ 签名,然后看着交易进内存池、打包、确认。地址带校验位,输错一位几乎都会被拦下;
|
||||
/// 碰巧合法的错地址没人用,币就没了。DEX 兑换是合约调用,没有收款地址,只要签名。
|
||||
/// 收款方:有账单的地址(执行账单)、自己的另一个钱包、Poinbase 充值地址(只收 CRC / PSC);其余地址币转进去就回不来。
|
||||
/// </summary>
|
||||
public static class WalletWindow
|
||||
{
|
||||
public const string Id = "pd_wallet";
|
||||
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 }
|
||||
|
||||
/// <summary>终端开出的账单:转账(有收款地址)或合约调用(DEX 兑换,没有地址)。付清后执行。</summary>
|
||||
/// <summary>终端开出的账单:转账(有收款地址)或合约调用(DEX 兑换,没有地址)。付清后用付款的钱包执行。</summary>
|
||||
public sealed class Bill
|
||||
{
|
||||
public string Address, Asset, Summary;
|
||||
public double Amount;
|
||||
public bool Contract;
|
||||
public Action Execute;
|
||||
public Action<Wallet> Execute;
|
||||
}
|
||||
|
||||
/// <summary>当前待付的账单(同一时间一张,新的覆盖旧的)。</summary>
|
||||
/// <summary>当前待付的账单(同一时间一张,新的覆盖旧的)。任何一台钱包都可以付。</summary>
|
||||
public static Bill Pending;
|
||||
|
||||
private sealed class Tx
|
||||
@@ -43,84 +45,118 @@ namespace Decentralized
|
||||
public bool Finished;
|
||||
}
|
||||
|
||||
private static Screen _screen = Screen.Home;
|
||||
private static string _to = "", _amount = "", _asset = Assets.PSC, _note;
|
||||
private static Tx _tx;
|
||||
/// <summary>一台硬件钱包的屏幕状态。</summary>
|
||||
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<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 };
|
||||
|
||||
private static readonly Color Ink = new Color(0.12f, 0.12f, 0.14f),
|
||||
Paper = new Color(0.86f, 0.87f, 0.83f), Key = new Color(0.16f, 0.16f, 0.19f), KeyText = new Color(0.93f, 0.93f, 0.90f),
|
||||
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 bool IsOpen => CustomUIManager.Instance != null && CustomUIManager.Instance.IsOpen(Id);
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>终端"打开钱包转账":账单的币种和金额填好,地址留给玩家手输。</summary>
|
||||
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);
|
||||
|
||||
/// <summary>读档 / 新游戏后清掉上一局的设备状态并关窗。</summary>
|
||||
public static void Reset()
|
||||
{
|
||||
foreach (var dev in Devices.Values)
|
||||
if (IsOpen(dev)) CustomUIManager.Instance.CloseWindow(dev.Id);
|
||||
Devices.Clear();
|
||||
Pending = null;
|
||||
}
|
||||
|
||||
/// <summary>终端"打开钱包转账" / DEX 签名请求:在当前连接的钱包上打开。转账填好币种和金额,地址留给玩家手输。</summary>
|
||||
public static void OpenForBill(Bill bill)
|
||||
{
|
||||
Pending = bill;
|
||||
if (_tx != null && !_tx.Finished) { Open(); return; }
|
||||
_note = null;
|
||||
if (bill.Contract) _screen = Screen.Review;
|
||||
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
|
||||
{
|
||||
_screen = Screen.Send;
|
||||
_to = "";
|
||||
_asset = bill.Asset;
|
||||
_amount = Amt(bill.Amount);
|
||||
dev.Screen = Screen.Send;
|
||||
dev.To = "";
|
||||
dev.Asset = bill.Asset;
|
||||
dev.Amount = Amt(bill.Amount);
|
||||
}
|
||||
Open();
|
||||
Open(dev);
|
||||
}
|
||||
|
||||
public static void Open()
|
||||
public static void Open(Wallet w) { if (w != null) Open(Dev(w)); }
|
||||
|
||||
private static void Open(Device dev)
|
||||
{
|
||||
if (Session.Data == null) return;
|
||||
if (Session.Data == null || dev.Wallet == null) return;
|
||||
WalletDriver.Ensure();
|
||||
var b = CustomUIManager.Window(Id, "PLEDGER");
|
||||
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, 0f));
|
||||
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 (_screen)
|
||||
switch (dev.Screen)
|
||||
{
|
||||
case Screen.Send: SendScreen(b); break;
|
||||
case Screen.Review: ReviewScreen(b); break;
|
||||
case Screen.Broadcast: BroadcastScreen(b); break;
|
||||
case Screen.Done: DoneScreen(b); break;
|
||||
default: HomeScreen(b); break;
|
||||
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(_note)) { b.AddLabel(_note, "note"); }
|
||||
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);
|
||||
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);
|
||||
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));
|
||||
Log.Guard("硬件钱包外观", () => Skin(win, screen, bezel, paper, plate, ok, dev));
|
||||
}
|
||||
|
||||
/// <summary>换上硬件的样子:机身石墨色、屏幕是浅色墨水屏、屏上按钮做成黑底白字的触控键。</summary>
|
||||
private static void Skin(CustomUIWindow win, RectTransform screen, Image bezel, Image paper, Image plate, GameObject ok)
|
||||
/// <summary>换上硬件的样子:机身、屏幕边框、墨水屏是自己画的像素九宫格,屏上按钮做成黑底白字的触控键。</summary>
|
||||
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<Image>(), "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<Image>();
|
||||
@@ -146,7 +182,7 @@ namespace Decentralized
|
||||
foreach (var t in btn.GetComponentsInChildren<TextMeshProUGUI>(true)) t.color = KeyText;
|
||||
}
|
||||
// 签名要按住 ✓:在按键上挂一个按住计时
|
||||
if (ok != null && _screen == Screen.Review && ok.GetComponent<HoldToSign>() == null) ok.AddComponent<HoldToSign>();
|
||||
if (ok != null && dev.Screen == Screen.Review && ok.GetComponent<HoldToSign>() == null) ok.AddComponent<HoldToSign>().Device = dev.Address;
|
||||
}
|
||||
|
||||
/// <summary>把本 mod 的像素图做成九宫格贴到 Image 上:边角 border 像素不拉伸,一个像素放大成 3 个界面单位。</summary>
|
||||
@@ -166,21 +202,20 @@ namespace Decentralized
|
||||
img.color = Color.white;
|
||||
}
|
||||
|
||||
private static readonly System.Collections.Generic.Dictionary<string, Sprite> Sliced = new System.Collections.Generic.Dictionary<string, Sprite>();
|
||||
private static readonly Dictionary<string, Sprite> Sliced = new Dictionary<string, Sprite>();
|
||||
|
||||
public static string Amt(double v) => v.ToString(v >= 100 ? "0.##" : "0.####", CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>账单金额按钱包显示的精度往上取整,照着填就够付。</summary>
|
||||
public static double RoundUp(double v) => v >= 100 ? Math.Ceiling(v * 100 - 1e-6) / 100 : Math.Ceiling(v * 10000 - 1e-6) / 10000;
|
||||
|
||||
private static Wallet W => Session.Data.Hot;
|
||||
private static string Me => Session.Data.Address("me");
|
||||
private static void Go(Device dev, Screen s) { dev.Screen = s; dev.Note = null; Open(dev); }
|
||||
|
||||
private static void HomeScreen(CustomUIBuilder b)
|
||||
private static void HomeScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
var w = W;
|
||||
b.AddHeader(L.T("hw.wallet.main"));
|
||||
b.AddLabel(Me, "addr_dim");
|
||||
var w = dev.Wallet;
|
||||
b.AddHeader(NameOf(w));
|
||||
b.AddLabel(w.Address, "addr_dim");
|
||||
b.AddSeparator();
|
||||
bool any = false;
|
||||
foreach (var kv in w.Balances.OrderBy(k => k.Key))
|
||||
@@ -195,53 +230,58 @@ namespace Decentralized
|
||||
if (Pending != null) b.AddLabel(Pending.Contract ? L.T("hw.pending.sign", Pending.Summary) : L.T("hw.pending.bill", Pending.Summary));
|
||||
b.AddSpace(6f);
|
||||
b.BeginRow();
|
||||
b.AddButton(L.T("hw.send"), () => { _screen = Screen.Send; _note = null; Open(); });
|
||||
if (Pending != null && Pending.Contract) b.AddButton(L.T("hw.review"), () => { _screen = Screen.Review; _note = null; Open(); });
|
||||
b.AddButton(L.T("hw.send"), () => Go(dev, Screen.Send));
|
||||
if (Pending != null && Pending.Contract && w == Session.Data.Hot) b.AddButton(L.T("hw.review"), () => Go(dev, Screen.Review));
|
||||
b.End();
|
||||
}
|
||||
|
||||
private static void SendScreen(CustomUIBuilder b)
|
||||
private static void SendScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
var w = dev.Wallet;
|
||||
b.AddHeader(L.T("hw.send"));
|
||||
b.AddLabel(L.T("hw.to"));
|
||||
b.AddInput("0x", _to, s => _to = s, "to", s => _to = s);
|
||||
b.AddInput("0x", dev.To, s => dev.To = s, "to", s => dev.To = s);
|
||||
b.BeginRow();
|
||||
foreach (var a in SendAssets)
|
||||
{
|
||||
var asset = a;
|
||||
b.AddButton(asset == _asset ? $"[{asset}]" : asset, () => { _asset = asset; Open(); });
|
||||
b.AddButton(asset == dev.Asset ? $"[{asset}]" : asset, () => { dev.Asset = asset; Open(dev); });
|
||||
}
|
||||
b.End();
|
||||
b.AddLabel(L.T("hw.avail", _asset, Amt(W.Avail(_asset))), "avail_dim");
|
||||
b.AddLabel(L.T("hw.avail", dev.Asset, Amt(w.Avail(dev.Asset))), "avail_dim");
|
||||
b.AddLabel(L.T("hw.amount"));
|
||||
b.AddInput("0", _amount, s => _amount = s, "amount", s => _amount = s);
|
||||
b.AddInput("0", dev.Amount, s => dev.Amount = s, "amount", s => dev.Amount = s);
|
||||
if (Pending != null && !Pending.Contract)
|
||||
{
|
||||
b.AddLabel(L.T("hw.bill", Pending.Summary, Amt(Pending.Amount), Pending.Asset), "bill_dim");
|
||||
b.AddButton(L.T("hw.fill"), () => { _asset = Pending.Asset; _amount = Amt(Pending.Amount); Open(); });
|
||||
b.AddButton(L.T("hw.fill"), () => { dev.Asset = Pending.Asset; dev.Amount = Amt(Pending.Amount); Open(dev); });
|
||||
}
|
||||
b.AddLabel(L.T("hw.next"), "hint_dim");
|
||||
}
|
||||
|
||||
private static bool ReadAmount(out double v) =>
|
||||
double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) && v > 0;
|
||||
private static bool ReadAmount(Device dev, out double v) =>
|
||||
double.TryParse((dev.Amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) && v > 0;
|
||||
|
||||
/// <summary>从发送页进核对页之前的检查:地址格式、校验位、余额、收款方是否在等这笔钱。</summary>
|
||||
private static string CheckSend()
|
||||
private static bool IsPoinbase(string to) => ChainAddress.Same(to, Session.Data.Address("poinbase"));
|
||||
|
||||
/// <summary>从发送页进核对页之前的检查:地址格式、校验位、余额、收款方收不收。</summary>
|
||||
private static string CheckSend(Device dev)
|
||||
{
|
||||
string to = ChainAddress.Normalize(_to);
|
||||
var w = dev.Wallet;
|
||||
string to = ChainAddress.Normalize(dev.To);
|
||||
if (!ChainAddress.WellFormed(to)) return L.T("hw.err.format");
|
||||
if (!ChainAddress.Valid(to)) return L.T("hw.err.checksum");
|
||||
if (ChainAddress.Same(to, Me)) return L.T("hw.err.self");
|
||||
if (!ReadAmount(out double v)) return L.T("msg.amount");
|
||||
if (W.Avail(_asset) + 1e-9 < v) return L.T(W.Frozen.ContainsKey(_asset) ? "msg.frozen" : "msg.balance");
|
||||
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");
|
||||
var bill = Match(to);
|
||||
if (bill != null)
|
||||
{
|
||||
if (bill.Asset != _asset) return L.T("hw.err.asset", bill.Asset);
|
||||
if (bill.Asset != dev.Asset) return L.T("hw.err.asset", bill.Asset);
|
||||
if (v + 1e-9 < bill.Amount) return L.T("hw.err.short", Amt(bill.Amount), bill.Asset);
|
||||
}
|
||||
else if (Known(to)) return L.T("hw.err.nobill");
|
||||
else if (IsPoinbase(to)) { if (dev.Asset != Assets.CRC && dev.Asset != Assets.PSC) return L.T("hw.err.poinbase"); }
|
||||
else if (Session.Data.Own(to) == null && Known(to)) return L.T("hw.err.nobill");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -254,7 +294,7 @@ namespace Decentralized
|
||||
return d.Addresses.Values.Any(a => ChainAddress.Same(a, to)) || d.Black.Sellers.Any(s => ChainAddress.Same(s.Name, to));
|
||||
}
|
||||
|
||||
private static void ReviewScreen(CustomUIBuilder b)
|
||||
private static void ReviewScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
b.AddHeader(L.T("hw.review.h"));
|
||||
if (Pending != null && Pending.Contract)
|
||||
@@ -264,152 +304,172 @@ namespace Decentralized
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadAmount(out double v);
|
||||
string to = ChainAddress.Normalize(_to);
|
||||
ReadAmount(dev, out double v);
|
||||
string to = ChainAddress.Normalize(dev.To);
|
||||
b.AddLabel(L.T("hw.review.to"));
|
||||
b.AddLabel("<size=26><b>" + to + "</b></size>");
|
||||
b.AddLabel(L.T("hw.review.amount", Amt(v), _asset, Fmt.Credits(v * Session.Price(_asset))));
|
||||
b.AddLabel(L.T("hw.review.amount", Amt(v), dev.Asset, Fmt.Credits(v * Session.Price(dev.Asset))));
|
||||
var bill = Match(to);
|
||||
var own = Session.Data.Own(to);
|
||||
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 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), _asset), "over_note");
|
||||
if (bill != null && v > bill.Amount + 1e-6) b.AddLabel(L.T("hw.review.over", Amt(v - bill.Amount), dev.Asset), "over_note");
|
||||
}
|
||||
b.AddSpace(8f);
|
||||
b.AddLabel(L.T("hw.hold"), "hold_dim");
|
||||
}
|
||||
|
||||
private static void BroadcastScreen(CustomUIBuilder b)
|
||||
private static void BroadcastScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
b.AddHeader(L.T("hw.tx.h"));
|
||||
b.AddLabel(StatusText(), "status");
|
||||
b.AddProgressBar(Progress(), "progress");
|
||||
b.AddLabel(StatusText(dev), "status");
|
||||
b.AddProgressBar(Progress(dev), "progress");
|
||||
b.AddSpace(10f);
|
||||
b.AddButton(L.T("hw.skip"), Finish);
|
||||
b.AddButton(L.T("hw.skip"), () => Finish(dev));
|
||||
}
|
||||
|
||||
private static void DoneScreen(CustomUIBuilder b)
|
||||
private static void DoneScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
var tx = _tx;
|
||||
var tx = dev.Tx;
|
||||
b.AddHeader(L.T("hw.done.h"));
|
||||
if (tx != null)
|
||||
{
|
||||
b.AddLabel(L.T("hw.done.block", tx.Block));
|
||||
if (tx.To != null) b.AddLabel(L.T("hw.done.sent", Amt(tx.Amount), tx.Asset, tx.To));
|
||||
var own = tx.To != null ? Session.Data.Own(tx.To) : null;
|
||||
if (tx.Bill != null) b.AddLabel(tx.Bill.Summary);
|
||||
else if (own != null) b.AddLabel(L.T("hw.review.own", NameOf(own)));
|
||||
else if (tx.To != null && IsPoinbase(tx.To)) b.AddLabel(L.T("hw.review.poinbase"));
|
||||
else if (tx.To != null) b.AddLabel(L.T("hw.done.void"), "void_note");
|
||||
}
|
||||
b.AddSpace(8f);
|
||||
b.AddButton(L.T("hw.back"), () => { _screen = Screen.Home; _note = null; _tx = null; Open(); });
|
||||
b.AddButton(L.T("hw.back"), () => { dev.Tx = null; Go(dev, Screen.Home); });
|
||||
}
|
||||
|
||||
private static float Elapsed => _tx == null ? 0 : Time.unscaledTime - _tx.Start;
|
||||
private static float Progress() => Mathf.Clamp01(Elapsed / Phases[Phases.Length - 1]);
|
||||
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 string StatusText()
|
||||
private static string StatusText(Device dev)
|
||||
{
|
||||
float t = Elapsed;
|
||||
if (_tx == null) return "";
|
||||
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", _tx.Block);
|
||||
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", _tx.Block, conf, 3);
|
||||
return L.T("hw.tx.confirm", dev.Tx.Block, conf, 3);
|
||||
}
|
||||
|
||||
/// <summary>左键 ✕:拒绝签名 / 返回上一页。</summary>
|
||||
private static void Reject()
|
||||
/// <summary>左键 ×:拒绝签名 / 返回上一页。</summary>
|
||||
private static void Reject(Device dev)
|
||||
{
|
||||
_note = null;
|
||||
switch (_screen)
|
||||
dev.Note = null;
|
||||
switch (dev.Screen)
|
||||
{
|
||||
case Screen.Review:
|
||||
if (Pending != null && Pending.Contract) { Pending = null; _screen = Screen.Home; TerminalWindow.Refresh(L.T("hw.rejected")); }
|
||||
else _screen = Screen.Send;
|
||||
if (Pending != null && Pending.Contract) { Pending = null; dev.Screen = Screen.Home; TerminalWindow.Refresh(L.T("hw.rejected")); }
|
||||
else dev.Screen = Screen.Send;
|
||||
break;
|
||||
case Screen.Send: _screen = Screen.Home; break;
|
||||
case Screen.Done: _screen = Screen.Home; _tx = null; break;
|
||||
case Screen.Send: dev.Screen = Screen.Home; break;
|
||||
case Screen.Done: dev.Screen = Screen.Home; dev.Tx = null; break;
|
||||
default: break;
|
||||
}
|
||||
Open();
|
||||
Open(dev);
|
||||
}
|
||||
|
||||
/// <summary>右键 ✓:发送页上是"下一步";核对页上要按住(见 <see cref="HoldToSign"/>),单击只提示。</summary>
|
||||
private static void Confirm()
|
||||
private static void Confirm(Device dev)
|
||||
{
|
||||
switch (_screen)
|
||||
switch (dev.Screen)
|
||||
{
|
||||
case Screen.Home: _screen = Screen.Send; _note = null; break;
|
||||
case Screen.Home: dev.Screen = Screen.Send; dev.Note = null; break;
|
||||
case Screen.Send:
|
||||
_note = CheckSend();
|
||||
if (_note == null) _screen = Screen.Review;
|
||||
dev.Note = CheckSend(dev);
|
||||
if (dev.Note == null) dev.Screen = Screen.Review;
|
||||
break;
|
||||
case Screen.Review: _note = L.T("hw.hold"); break;
|
||||
case Screen.Done: _screen = Screen.Home; _tx = null; _note = null; break;
|
||||
case Screen.Review: dev.Note = L.T("hw.hold"); break;
|
||||
case Screen.Done: dev.Screen = Screen.Home; dev.Tx = null; dev.Note = null; break;
|
||||
default: return;
|
||||
}
|
||||
Open();
|
||||
Open(dev);
|
||||
}
|
||||
|
||||
/// <summary>按住 ✓ 满时间:签名并广播。</summary>
|
||||
internal static void Sign()
|
||||
internal static void Sign(string address)
|
||||
{
|
||||
if (_screen != Screen.Review) return;
|
||||
if (!Devices.TryGetValue(address, out var dev) || dev.Screen != Screen.Review || dev.Wallet == null) return;
|
||||
var d = Session.Data;
|
||||
d.Block += 1 + d.Rng.Range(0, 3);
|
||||
if (Pending != null && Pending.Contract)
|
||||
_tx = new Tx { Bill = Pending, Block = d.Block, Start = Time.unscaledTime };
|
||||
dev.Tx = new Tx { Bill = Pending, Block = d.Block, Start = Time.unscaledTime };
|
||||
else
|
||||
{
|
||||
string err = CheckSend();
|
||||
if (err != null) { _note = err; _screen = Screen.Send; Open(); return; }
|
||||
ReadAmount(out double v);
|
||||
string to = ChainAddress.Normalize(_to);
|
||||
_tx = new Tx { To = to, Asset = _asset, Amount = v, Bill = Match(to), Block = d.Block, Start = Time.unscaledTime };
|
||||
string err = CheckSend(dev);
|
||||
if (err != null) { dev.Note = err; dev.Screen = Screen.Send; Open(dev); return; }
|
||||
ReadAmount(dev, out double v);
|
||||
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 };
|
||||
}
|
||||
if (_tx.Bill != null) Pending = null;
|
||||
_screen = Screen.Broadcast;
|
||||
_note = null;
|
||||
Open();
|
||||
if (dev.Tx.Bill != null) Pending = null;
|
||||
dev.Screen = Screen.Broadcast;
|
||||
dev.Note = null;
|
||||
Open(dev);
|
||||
}
|
||||
|
||||
/// <summary>动画走完(或点了跳过):交易落地。付账单的执行终端那边的动作;没人用的地址,币就没了。</summary>
|
||||
internal static void Finish()
|
||||
/// <summary>动画走完(或点了跳过):交易落地。付账单的执行终端那边的动作;转给自己的钱包或 Poinbase 的按比例带上污点;没人用的地址,币就没了。</summary>
|
||||
private static void Finish(Device dev)
|
||||
{
|
||||
var tx = _tx;
|
||||
var tx = dev.Tx;
|
||||
if (tx == null || tx.Finished) return;
|
||||
tx.Finished = true;
|
||||
var d = Session.Data;
|
||||
var w = dev.Wallet;
|
||||
Log.Guard("钱包交易落地", () =>
|
||||
{
|
||||
if (w == null) return;
|
||||
double amt = tx.To != null ? Math.Min(tx.Amount, w.Avail(tx.Asset)) : 0;
|
||||
if (tx.Bill != null && !tx.Bill.Contract)
|
||||
{
|
||||
// 多付的部分对方不退
|
||||
double extra = tx.Amount - tx.Bill.Amount;
|
||||
if (extra > 1e-9) Wallet.Transfer(d.Hot, new Wallet(), tx.Asset, Math.Min(extra, d.Hot.Avail(tx.Asset)), Session.Price);
|
||||
}
|
||||
if (tx.Bill != null) tx.Bill.Execute();
|
||||
else if (tx.To != null)
|
||||
{
|
||||
Wallet.Transfer(d.Hot, new Wallet(), tx.Asset, Math.Min(tx.Amount, d.Hot.Avail(tx.Asset)), Session.Price);
|
||||
Log.Info($"转进没人用的地址 {tx.To}:{tx.Amount} {tx.Asset}");
|
||||
TerminalWindow.Refresh(null);
|
||||
double extra = amt - tx.Bill.Amount;
|
||||
if (extra > 1e-9) Wallet.Transfer(w, new Wallet(), tx.Asset, extra, Session.Price);
|
||||
}
|
||||
if (tx.Bill != null) { tx.Bill.Execute(w); return; }
|
||||
if (tx.To == null) return;
|
||||
var own = d.Own(tx.To);
|
||||
Wallet target = own ?? (IsPoinbase(tx.To) ? d.Poinbase : new Wallet());
|
||||
Wallet.Transfer(w, target, tx.Asset, amt, Session.Price);
|
||||
if (own == null && target != d.Poinbase) Log.Info($"转进没人用的地址 {tx.To}:{amt} {tx.Asset}");
|
||||
TerminalWindow.Refresh(null);
|
||||
if (own != null) RefreshDevice(own);
|
||||
});
|
||||
Session.Save();
|
||||
_screen = Screen.Done;
|
||||
_to = "";
|
||||
if (IsOpen) Open();
|
||||
dev.Screen = Screen.Done;
|
||||
dev.To = "";
|
||||
if (IsOpen(dev)) Open(dev);
|
||||
}
|
||||
|
||||
/// <summary>别的交易让这个钱包的余额变了:开着的话刷新主页。</summary>
|
||||
public static void RefreshDevice(Wallet w)
|
||||
{
|
||||
if (w == null || !Devices.TryGetValue(w.Address, out var dev) || !IsOpen(dev)) return;
|
||||
if (dev.Screen == Screen.Home || dev.Screen == Screen.Send) Open(dev);
|
||||
}
|
||||
|
||||
/// <summary>每帧推进出块动画:只改状态文字和进度条,不重建窗口。</summary>
|
||||
internal static void Tick()
|
||||
{
|
||||
if (_tx == null || _tx.Finished || _screen != Screen.Broadcast) return;
|
||||
if (Elapsed >= Phases[Phases.Length - 1]) { Finish(); return; }
|
||||
if (!IsOpen) return;
|
||||
var m = CustomUIManager.Instance;
|
||||
m.GetElement(Id, "status")?.SetText(StatusText());
|
||||
m.GetElement(Id, "progress")?.SetProgress(Progress());
|
||||
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 (!IsOpen(dev)) continue;
|
||||
var m = CustomUIManager.Instance;
|
||||
m.GetElement(dev.Id, "status")?.SetText(StatusText(dev));
|
||||
m.GetElement(dev.Id, "progress")?.SetProgress(Progress(dev));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,6 +500,7 @@ namespace Decentralized
|
||||
{
|
||||
private const float Seconds = 1.2f;
|
||||
private float held = -1f;
|
||||
public string Device;
|
||||
|
||||
public void OnPointerDown(PointerEventData e) => held = 0f;
|
||||
public void OnPointerUp(PointerEventData e) => Stop();
|
||||
@@ -456,7 +517,7 @@ namespace Decentralized
|
||||
if (held < 0f) return;
|
||||
held += Time.unscaledDeltaTime;
|
||||
CustomUIManager.Instance?.ShowHoldRing(held / Seconds);
|
||||
if (held >= Seconds) { Stop(); Log.Guard("签名", WalletWindow.Sign); }
|
||||
if (held >= Seconds) { Stop(); Log.Guard("签名", () => WalletWindow.Sign(Device)); }
|
||||
}
|
||||
|
||||
private void OnDisable() => Stop();
|
||||
|
||||
Reference in new issue
Block a user