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:
ZHU MINCHAOandClaude Opus 5.5 committed 2026-11-03 22:58:24 +08:00
1 parent add61a6b5e
commit 783f73989d
19 files changed
+557 -199

No files matched your search

+112 -44
View File
@@ -13,7 +13,7 @@ namespace Decentralized
public static class TerminalWindow
{
public const string Id = "pd_terminal";
private enum Page { Home, Bank, Poinbase, Web3 }
private enum Page { Home, Bank, Poinbase, Web3, Wallets }
private static Page _page = Page.Home;
private sealed class EditPrice { public GameItem Item; public BmSale Sale; public string Id; public long Ref; }
@@ -35,7 +35,6 @@ namespace Decentralized
string name = L.T("tab." + p.ToString().ToLowerInvariant());
b.AddButton(page == _page ? $"[{name}]" : name, () => { _page = page; _message = null; _edit = null; Open(); });
}
b.AddButton(L.T("tab.wallet"), WalletWindow.Open);
b.End();
b.AddSeparator();
b.BeginScroll(540);
@@ -45,6 +44,7 @@ namespace Decentralized
case Page.Bank: BankPage(b); break;
case Page.Poinbase: ExchangePage(b, Venue.Exchange); break;
case Page.Web3: Web3Page(b); break;
case Page.Wallets: WalletsPage(b); break;
default: break;
}
b.End();
@@ -76,7 +76,7 @@ namespace Decentralized
private static void HomePage(CustomUIBuilder b)
{
var d = Session.Data;
double chain = d.Hot.Value(Session.Price);
double chain = d.ChainValue(Session.Price);
b.AddLabel(L.T("home.summary", PlayerStore.Instance.playerCash, Fmt.Credits(chain)));
b.AddSpace(6f);
b.BeginRow(8f, TextAnchor.UpperCenter);
@@ -144,7 +144,7 @@ namespace Decentralized
l.AddHeader(L.T("entry.bank"));
TableRow(l, w, L.T("bank.cash.k"), ps.playerCash.ToString());
TableRow(l, w, L.T("bank.rent.k"), L.T("bank.rent.v", Session.Rent(), ps.GetRentDayCounter()));
TableRow(l, w, L.T("bank.crc.k"), Fmt.Num(Session.Data.Hot.Get(Assets.CRC)));
TableRow(l, w, L.T("bank.crc.k"), Fmt.Num(Session.Data.Poinbase.Get(Assets.CRC)));
}, r =>
{
r.AddHeader(L.T("bank.ops"));
@@ -164,14 +164,15 @@ namespace Decentralized
double recent = d.Exchange.History.Where(h => h.Key > day - Tuning.AmlWindowDays).Sum(h => h.Value);
b.AddSeparator();
b.AddLabel(L.T("aml.window", Tuning.AmlWindowDays, Fmt.Credits(recent), Fmt.Credits(Tuning.AmlCumRents * rent)));
double total = d.Hot.Value(Session.Price);
double taintRatio = total > 0 ? d.Hot.Taint / total : 0;
var pb = d.Poinbase;
double total = pb.Value(Session.Price);
double taintRatio = total > 0 ? pb.Taint / total : 0;
b.AddLabel(L.T("aml.taint", (int)Math.Round(taintRatio * 100)));
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double v) && v > 0)
{
double value = Math.Min(v, d.Hot.Get(Assets.CRC)) * Session.Price(Assets.CRC);
double taint = total > 0 ? d.Hot.Taint * Math.Min(1, value / total) : 0;
double chance = Exchange.HitChance(value, taint, recent, rent, d.Hot.Flagged);
double value = Math.Min(v, pb.Get(Assets.CRC)) * Session.Price(Assets.CRC);
double taint = total > 0 ? pb.Taint * Math.Min(1, value / total) : 0;
double chance = Exchange.HitChance(value, taint, recent, rent, pb.Flagged);
b.AddLabel(L.T("aml.chance", (int)Math.Round(chance * 100)));
}
}
@@ -221,7 +222,7 @@ namespace Decentralized
any = true;
TableRow(b, HoldW, kv.Key, Fmt.Num(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key)), AvgText(w, kv.Key));
}
if (!any) b.AddLabel(L.T("wallet.empty"));
if (!any) b.AddLabel(L.T(w == Session.Data.Poinbase ? "poin.empty" : "wallet.empty"));
foreach (var kv in w.Frozen.OrderBy(k => k.Key).Where(k => show(k.Key)))
b.AddLabel(L.T("wallet.frozen", kv.Key, Fmt.Num(kv.Value), w.FrozenUntil.TryGetValue(kv.Key, out var u) ? u : 0));
}
@@ -229,19 +230,87 @@ namespace Decentralized
/// <summary>Poinbase:合规交易所。左边账户持仓,右边交易;信用点的存取在银行。</summary>
private static void ExchangePage(CustomUIBuilder b, Venue venue)
{
var w = Session.Data.Hot;
var d = Session.Data;
var w = d.Poinbase;
Columns(b, l =>
{
l.AddHeader(L.T("poin.account.h"));
Holdings(l, w, a => a == Assets.CRC || a == Assets.PSC);
Holdings(l, w, a => true);
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
l.AddSeparator();
l.AddLabel(L.T("poin.deposit", d.Address("poinbase")));
}, r =>
{
r.AddHeader(L.T("trade.h"));
AmountRow(r);
TradeRows(r, venue);
r.AddSeparator();
r.BeginRow();
foreach (var a in new[] { Assets.CRC, Assets.PSC })
{
var asset = a;
r.AddButton(asset == _poinAsset ? $"[{asset}]" : asset, () => { _poinAsset = asset; Open(); });
}
r.End();
r.AddButton(L.T("poin.out", WalletWindow.NameOf(d.Hot)), PoinbaseOut);
});
}
private static string _poinAsset = Assets.CRC;
/// <summary>Poinbase 提到当前钱包:托管账户直接转,不用签名。污点按比例随币走。</summary>
private static void PoinbaseOut()
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
if (d.Poinbase.Avail(_poinAsset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
Wallet.Transfer(d.Poinbase, d.Hot, _poinAsset, v, Session.Price);
_message = L.T("poin.out.done", Fmt.Num(v), _poinAsset, WalletWindow.NameOf(d.Hot));
Session.Save();
WalletWindow.RefreshDevice(d.Hot);
Open();
}
/// <summary>钱包页:全部硬件钱包,连接(设为当前)、打开实体钱包。</summary>
private static void WalletsPage(CustomUIBuilder b)
{
var d = Session.Data;
b.AddLabel(L.T("wallets.count", d.Wallets.Count, Tuning.WalletMax));
var cols = new[] { 130f, 130f, 120f, 110f, 220f };
TableHeader(b, cols, L.T("wallets.col.name"), L.T("wallets.col.addr"), L.T("col.value"), L.T("wallets.col.taint"), "");
for (int i = 0; i < d.Wallets.Count; i++)
{
var w = d.Wallets[i];
int idx = i;
b.BeginRow(2f);
Cell(b, cols[0], (idx == d.Current ? "● " : "") + WalletWindow.NameOf(w));
Cell(b, cols[1], w.Address);
Cell(b, cols[2], Fmt.Credits(w.Value(Session.Price)));
Cell(b, cols[3], Fmt.Credits(w.Taint));
b.BeginPanel(cols[4], 30f, TextAnchor.MiddleLeft, 2f, 0f);
b.BeginRow(4f);
b.AddButton(L.T("wallets.open"), () => WalletWindow.Open(w));
if (idx != d.Current) b.AddButton(L.T("wallets.use"), () => { d.Current = idx; Session.Save(); Open(); });
else b.AddLabel(L.T("wallets.current"));
b.End();
b.End();
b.End();
}
}
/// <summary>硬件钱包拖到终端上:添加一个钱包(被投毒的钱包看起来一样)。</summary>
public static bool CanAddWallet() => Session.Data != null && Session.Data.Wallets.Count < Tuning.WalletMax;
public static void AddWallet(bool poisoned)
{
var w = Session.Data.AddWallet(poisoned);
Log.Info($"添加硬件钱包 {w.Address}{(poisoned ? "(被投毒)" : "")}");
Session.Save();
_page = Page.Wallets;
_message = L.T("wallets.added", WalletWindow.NameOf(w), w.Address);
Open();
}
/// <summary>Web3:链上钱包与去中心化交易。</summary>
private static void Web3Page(CustomUIBuilder b)
{
@@ -265,7 +334,7 @@ namespace Decentralized
}
Columns(b, l =>
{
l.AddHeader(L.T("web3.wallet.h"));
l.AddHeader(L.T("web3.wallet.h") + " " + WalletWindow.NameOf(w));
Holdings(l, w, a => true);
l.AddSeparator();
l.AddLabel(L.T("wallet.total", Fmt.Credits(w.Value(Session.Price))));
@@ -389,7 +458,7 @@ namespace Decentralized
GoodHead(c, g, L.T("black.stock", offer.Qty));
c.AddLabel(L.T("black.price", offer.Price * count) + (count > 1 ? " ×" + count : ""));
c.AddFlexibleSpace();
c.AddButton(L.T("black.buy"), () => { string asset = _payAsset; BeginPay(d.Address("platform"), ItemName(offer.Item) + (count > 1 ? " ×" + count : ""), asset, offer.Price * count, () => BuyOffer(offer, count, asset)); });
c.AddButton(L.T("black.buy"), () => { string asset = _payAsset; BeginPay(d.Address("platform"), ItemName(offer.Item) + (count > 1 ? " ×" + count : ""), asset, offer.Price * count, pw => BuyOffer(offer, count, asset, pw)); });
});
}
@@ -412,7 +481,7 @@ namespace Decentralized
b.AddButton(L.T("black.sell.all"), () =>
{
string asset = _payAsset; double mult = _sellMult;
BeginPay(d.Address("platform"), L.T("black.sell.all") + L.T("pay.listfee", mult.ToString("0.0", CultureInfo.InvariantCulture)), asset, BlackDelivery.Sellable().Sum(it => BlackMarket.ListFee(BlackMarket.ListPriceOf(BlackDelivery.RefValue(it), mult))), () => ListAll(asset, mult));
BeginPay(d.Address("platform"), L.T("black.sell.all") + L.T("pay.listfee", mult.ToString("0.0", CultureInfo.InvariantCulture)), asset, BlackDelivery.Sellable().Sum(it => BlackMarket.ListFee(BlackMarket.ListPriceOf(BlackDelivery.RefValue(it), mult))), pw => ListAll(asset, mult, pw));
});
b.AddSeparator();
var items = BlackDelivery.Sellable();
@@ -437,7 +506,7 @@ namespace Decentralized
private static readonly System.Collections.Generic.Dictionary<BmGood, GameItem> SellCards = new System.Collections.Generic.Dictionary<BmGood, GameItem>();
private static void ListAll(string asset, double mult)
private static void ListAll(string asset, double mult, Wallet pw)
{
var d = Session.Data;
int n = 0;
@@ -446,7 +515,7 @@ namespace Decentralized
string id = BlackDelivery.SellId(it);
if (id == null) continue;
var u = Items.IsGpu(it) ? Items.ToUnit(it) : null;
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price);
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, pw, asset, StoreStation.GetDayCounter(), Session.Price);
if (s == null) break;
it.Destroy();
n++;
@@ -456,13 +525,13 @@ namespace Decentralized
Open();
}
private static void ListItem(GameItem it, double mult, string asset)
private static void ListItem(GameItem it, double mult, string asset, Wallet pw)
{
var d = Session.Data;
string id = BlackDelivery.SellId(it);
if (id == null) { Open(); return; }
var u = Items.IsGpu(it) ? Items.ToUnit(it) : null;
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price);
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, pw, asset, StoreStation.GetDayCounter(), Session.Price);
if (s == null) { _message = d.Black.ActiveListings >= Tuning.BmMaxListings ? L.T("black.sell.full", Tuning.BmMaxListings) : L.T("msg.balance"); Open(); return; }
it.Destroy();
_message = L.T("black.sell.listed", ItemName(id), s.ListPrice);
@@ -496,7 +565,7 @@ namespace Decentralized
{
if (!ok) return;
string asset = _payAsset; var mode = _mode;
BeginPay(seller.Name, ItemName(listing.Item) + (count > 1 ? " ×" + count : ""), asset, BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, count), mode), () => PlaceOrder(listing, count, asset, mode));
BeginPay(seller.Name, ItemName(listing.Item) + (count > 1 ? " ×" + count : ""), asset, BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, count), mode), pw => PlaceOrder(listing, count, asset, mode, pw));
});
});
}
@@ -533,10 +602,10 @@ namespace Decentralized
private static string ItemName(string id) => BlackDelivery.ItemName(id);
/// <summary>开账单:收款地址、币种、金额(按信用点等值折成币),付款在硬件钱包里做。</summary>
private static void BeginPay(string address, string summary, string asset, double credits, Action execute) =>
private static void BeginPay(string address, string summary, string asset, double credits, Action<Wallet> execute) =>
BillFor(address, summary, asset, credits / Session.Price(asset), execute);
private static void BillFor(string address, string summary, string asset, double amount, Action execute)
private static void BillFor(string address, string summary, string asset, double amount, Action<Wallet> execute)
{
WalletWindow.Pending = new WalletWindow.Bill { Address = address, Summary = summary, Asset = asset, Amount = WalletWindow.RoundUp(amount), Execute = execute };
_message = null;
@@ -557,7 +626,7 @@ namespace Decentralized
b.End();
}
private static void BuyOffer(BmOffer o, int count, string asset)
private static void BuyOffer(BmOffer o, int count, string asset, Wallet pw)
{
var d = Session.Data;
string item = o.Item, model = o.Model, fake = o.FakeOf;
@@ -565,7 +634,7 @@ namespace Decentralized
int bought = 0;
for (int i = 0; i < count; i++)
{
if (!d.Black.Buy(o, d.Hot, asset, Session.Price)) break;
if (!d.Black.Buy(o, pw, asset, Session.Price)) break;
Items.Give(BlackDelivery.Make(item, model, fake, health));
bought++;
}
@@ -575,10 +644,10 @@ namespace Decentralized
Open();
}
private static void PlaceOrder(BmListing l, int count, string asset, PayMode mode)
private static void PlaceOrder(BmListing l, int count, string asset, PayMode mode, Wallet pw)
{
var d = Session.Data;
var o = d.Black.Place(l, count, mode, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price);
var o = d.Black.Place(l, count, mode, pw, asset, StoreStation.GetDayCounter(), Session.Price);
if (o == null) { _message = L.T("msg.balance"); Open(); return; }
_message = L.T("black.ordered", ItemName(o.Item) + (o.Count > 1 ? " ×" + o.Count : ""), o.DueDay);
Session.Save();
@@ -625,16 +694,16 @@ namespace Decentralized
_edit = null;
string asset = _payAsset;
BeginPay(d.Address("platform"), ItemName(e.Id) + L.T(e.Sale != null ? "pay.repricefee" : "pay.listfee", mult.ToString("0.00", CultureInfo.InvariantCulture)), asset, fee,
() => { if (e.Sale != null) RepriceSale(e.Sale, mult, asset); else ListItem(e.Item, mult, asset); });
pw => { if (e.Sale != null) RepriceSale(e.Sale, mult, asset, pw); else ListItem(e.Item, mult, asset, pw); });
});
b.AddButton(L.T("black.edit.cancel"), () => { _edit = null; _message = null; Open(); });
b.End();
}
private static void RepriceSale(BmSale s, double mult, string asset)
private static void RepriceSale(BmSale s, double mult, string asset, Wallet pw)
{
var d = Session.Data;
if (!d.Black.Reprice(s, mult, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price)) { _message = L.T("msg.balance"); Open(); return; }
if (!d.Black.Reprice(s, mult, pw, asset, StoreStation.GetDayCounter(), Session.Price)) { _message = L.T("msg.balance"); Open(); return; }
_message = L.T("black.sell.repriced", ItemName(s.Item), s.ListPrice);
Session.Save();
Open();
@@ -701,16 +770,16 @@ namespace Decentralized
bool into = _deskIn; string other = _deskAsset;
string asset = into ? other : Assets.Ponero;
_web3Tab = Web3Tab.Trade;
BillFor(d.Address("desk"), L.T("pay.swap", Fmt.Num(dv), asset), asset, dv, () => DeskSwap(dv, into, other));
BillFor(d.Address("desk"), L.T("pay.swap", Fmt.Num(dv), asset), asset, dv, pw => DeskSwap(dv, into, other, pw));
}
}
private static void DeskSwap(double v, bool into, string other)
private static void DeskSwap(double v, bool into, string other, Wallet pw)
{
var d = Session.Data;
_deskIn = into; _deskAsset = other;
int day = StoreStation.GetDayCounter();
double got = _deskIn ? d.Black.ToPonero(d.Hot, _deskAsset, v, day, Session.Price) : d.Black.FromPonero(d.Hot, _deskAsset, v, day, Session.Price);
double got = _deskIn ? d.Black.ToPonero(pw, _deskAsset, v, day, Session.Price) : d.Black.FromPonero(pw, _deskAsset, v, day, Session.Price);
if (got < 0) { _message = L.T("black.desk.fail"); Open(); return; }
// 从 Ponero 出来的币来源不明:出金审查按"来自混币器"加重
if (!_deskIn) d.Mixer.LastReturnDay = day;
@@ -774,17 +843,17 @@ namespace Decentralized
{
if (!ReadAmount(out double mv)) return;
string asset = _mixAsset; int rounds = _mixRounds;
BillFor(d.Address("mixer"), L.T("pay.mix", Fmt.Num(mv), asset), asset, mv, () => MixDeposit(mv, asset, rounds));
BillFor(d.Address("mixer"), L.T("pay.mix", Fmt.Num(mv), asset), asset, mv, pw => MixDeposit(mv, asset, rounds, pw));
});
});
}
private static void MixDeposit(double v, string asset, int rounds)
private static void MixDeposit(double v, string asset, int rounds, Wallet pw)
{
var d = Session.Data;
int day = StoreStation.GetDayCounter();
if (d.Hot.Avail(asset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
var dep = d.Mixer.Deposit(d.Hot, asset, v, rounds, day, Session.Price);
if (pw.Avail(asset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
var dep = d.Mixer.Deposit(pw, asset, v, rounds, day, Session.Price);
_message = dep == null ? L.T("mixer.min", Tuning.MixerMin) : L.T("mixer.done", Fmt.Num(v), asset, dep.ReadyDay);
Session.Save();
Open();
@@ -825,7 +894,7 @@ namespace Decentralized
int credits = (int)Math.Floor(v);
if (credits <= 0 || PlayerStore.Instance.playerCash < credits) { _message = L.T("msg.balance"); Open(); return; }
ModHelper.AddCredit(-credits, false);
var w = Session.Data.Hot;
var w = Session.Data.Poinbase;
w.Add(Assets.CRC, credits / Session.Data.Stable.Crc);
w.RecordBuy(Assets.CRC, credits / Session.Data.Stable.Crc, credits);
w.Basis += credits;
@@ -838,8 +907,8 @@ namespace Decentralized
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
if (d.Hot.Get(Assets.CRC) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
var r = d.Exchange.Cashout(d.Hot, v, Session.Price, StoreStation.GetDayCounter(), Session.Rent(), d.Mixer.RecentlyMixed(StoreStation.GetDayCounter()), d.Rng);
if (d.Poinbase.Avail(Assets.CRC) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
var r = d.Exchange.Cashout(d.Poinbase, v, Session.Price, StoreStation.GetDayCounter(), Session.Rent(), d.Mixer.RecentlyMixed(StoreStation.GetDayCounter()), d.Rng);
int got = (int)Math.Floor(r.Received - r.Tax);
ModHelper.AddCredit(got, true);
_message = L.T("msg.cashout", got, Fmt.Credits(r.Tax));
@@ -857,19 +926,18 @@ namespace Decentralized
private static void Trade(Pool p, bool buy)
{
if (!ReadAmount(out double v)) return;
var w = Session.Data.Hot;
var w = p.Venue == Venue.Dex ? Session.Data.Hot : Session.Data.Poinbase;
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
if (w.Avail(pay) + 1e-9 < v) { _message = L.T(frozenPay(w, pay) ? "msg.frozen" : "msg.balance"); Open(); return; }
if (p.Venue != Venue.Dex) { DoTrade(p, buy, v); return; }
if (p.Venue != Venue.Dex) { DoTrade(p, buy, v, w); return; }
double quote = buy ? v / p.Price : v * p.Price;
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Summary = L.T("hw.swap", Fmt.Num(v), pay, Fmt.Num(quote), get), Execute = () => DoTrade(p, buy, v) });
WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Summary = L.T("hw.swap", Fmt.Num(v), pay, Fmt.Num(quote), get), Execute = pw => DoTrade(p, buy, v, pw) });
}
private static void DoTrade(Pool p, bool buy, double v)
private static void DoTrade(Pool p, bool buy, double v, Wallet w)
{
var w = Session.Data.Hot;
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
if (w.Avail(pay) + 1e-9 < v) { _message = L.T(frozenPay(w, pay) ? "msg.frozen" : "msg.balance"); Open(); return; }
double got = buy ? p.BuyBase(v) : p.SellBase(v);