Wallets: expected-days drain, copy/paste, three-column selectable balances, rename and destroy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZHU MINCHAOandClaude Opus 5.5 committed 2026-11-03 23:05:13 +08:00
1 parent 783f73989d
commit 2b781acd82
10 files changed
+178 -22

No files matched your search

+5
View File
@@ -13,6 +13,11 @@
- 多钱包:每个硬件钱包的余额、污点、成本、冻结各算各的。终端新增"钱包"页:列出全部钱包(名称、地址、估值、污点),"打开"弹出这个钱包自己的 PLEDGER 窗口(几台可以同时开),"连接"设为当前钱包。Web3 页的交易、黑市、混币器用当前钱包;用币付款的顾客、挖矿收益进当前钱包;订单退款、出售回款、混币提回回到当初付款的钱包。账单可以用任何一台钱包付。钱包之间转账走签名,污点按比例跟着走。
- Poinbase 拆成托管账户:银行存入进 Poinbase,出金审查只看 Poinbase 账户的污点;Poinbase 页"提到当前钱包"直接转,不用签名;钱包充值要转到 Poinbase 的充值地址(页面上显示),只收 CRC 和 PSC。
- PLEDGER 硬件钱包(物品):和游戏的升级材料一样,拖到交易终端上用掉,钱包 +1,最多 5 个。杰克逊博士进货时(钱包没满)50% 上一个,价值 240,正品;Web3 黑市平台现货每周 50% 上 1–2 个,150–180,其中 20% 被投毒。外观和说明完全一样。被投毒的钱包币的总价值到 3000 起,每晚有概率被转空(1/7 起,按价值线性升到 20000 时当晚必转),晨间日志只写"钱包 n:币被转走了"。
- 被投毒的钱包按期望天数算:门槛(3000)处期望 7 天,按价值线性降到 20000 处 1 天,每晚概率 = 1 / 期望天数(中间值期望 4 天、每晚 25%,模拟 4000 次平均 4 天)。
- 复制粘贴:终端账单的收款地址、Poinbase 充值地址、钱包页每个钱包、PLEDGER 主页的本钱包地址都有"复制";PLEDGER 发送页收款地址旁有"粘贴"。用的是系统剪贴板。
- PLEDGER 主页余额改成三列(币种 | 数量 | 估值)右对齐,五种币全部显示(含 0,冻结的另注);点一行选中,整行反色高亮,发送时默认用选中的币。
- 钱包页:每个钱包可以改名(留空恢复默认,最长 16 字),名字在终端和 PLEDGER 上都用;非主钱包可以销毁(点两次确认,钱包里还有币时不让销毁)。钱包编号固定,销毁别的钱包后名字不会错位。
- 游戏内验证(这一轮):复制钱包 3 地址后在主钱包发送页粘贴,地址正确;选中 CRT 行高亮,进发送页默认 CRT;向钱包 3 转 1 CRT 后点销毁被拦下;钱包 2 二次确认后销毁,钱包 3 仍叫"钱包 3";钱包 2 改名为 cold,终端与 PLEDGER 都显示新名字;三列余额对齐。PLEDGER 上名字被标题样式转成大写,已改成普通粗体(编译通过,没重进游戏看)。没有验证:游戏输入框里直接 Ctrl+V。
- 存档不兼容旧版:链上的币归到新建的主钱包,Poinbase 账户从 0 开始。
- 游戏内验证(多钱包):两件硬件钱包(一正一投毒)作用于终端,钱包页出现钱包 2、钱包 3,标记看不出来;同时打开两台 PLEDGER;主钱包向钱包 3 转 100 PSC,核对页显示"自己的钱包:钱包 3",污点 5000 按 10000/16000 带走 3125;过一夜钱包 3 被转空(约 9393),晨间日志有记录;银行存入 500 进 Poinbase,提 200 CRC 到主钱包,再从主钱包向充值地址转 100 CRC,Poinbase 300 → 400,带过去污点 31。商城平台现货出现硬件钱包(158)。没有在游戏里验证:用鼠标拖拽(用探查通道执行了同一个"物品作用于物品"接口)、杰克逊柜台上的钱包和它的标价、Poinbase 空账户和"账户污点比例"两处改字(已编译,没重进游戏看)。
+1 -1
View File
@@ -139,7 +139,7 @@ namespace Decentralized.Domain
/// <summary>硬件钱包:最多几个(含主钱包);杰克逊博士的售价与每次进货上架的概率;Web3 商城的价格范围、每周平台现货上架的概率与件数上限;商城货里被投毒的比例。</summary>
public const int WalletMax = 5, WalletJacksonPrice = 240, WalletMallMin = 150, WalletMallMax = 180, WalletMallQtyMax = 2;
public const double WalletJacksonChance = 0.5, WalletMallChance = 0.5, WalletPoisonShare = 0.2;
/// <summary>被投毒的钱包:币的总价值达到 DrainMin 起,每晚有概率被转空;价值越高越快:每晚概率从门槛处的 1 / SlowDays(平均 7 天)按价值线性升到 DrainFull 处的 1(当晚就转走)。</summary>
/// <summary>被投毒的钱包:币的总价值达到 DrainMin 起,每晚有概率被转空;价值越高越快:期望天数从门槛处的 SlowDays(7 天)按价值线性降到 DrainFull 处的 1 天,每晚概率 = 1 / 期望天数。</summary>
public const double WalletDrainMin = 3000, WalletDrainFull = 20000, WalletDrainSlowDays = 7;
/// <summary>主交易对(信用点一侧)的深度随天数增长。</summary>
+10 -6
View File
@@ -18,19 +18,23 @@ namespace Decentralized.Domain
public double Basis;
/// <summary>被执法标记。</summary>
public bool Flagged;
/// <summary>显示名(终端钱包页、硬件钱包标题)。</summary>
/// <summary>钱包编号("钱包 n"),添加时定下,销毁别的钱包也不变。</summary>
public string Name;
/// <summary>玩家起的名字;空时用默认名。</summary>
public string Label;
/// <summary>硬件钱包被动过手脚(供应链投毒):别人手里有助记词。玩家看不到。</summary>
public bool Poisoned;
/// <summary>被投毒的钱包今晚被转空的概率:总价值不到门槛时为 0,越多越高(1–7 天)。</summary>
public static double DrainChance(double value)
/// <summary>被投毒的钱包被转空前的期望天数:门槛处 7 天,按价值线性降到满额处 1 天;不到门槛不会被转。</summary>
public static double DrainDays(double value)
{
if (value < Tuning.WalletDrainMin) return 0;
double slow = 1 / Tuning.WalletDrainSlowDays, t = Math.Min(1, (value - Tuning.WalletDrainMin) / (Tuning.WalletDrainFull - Tuning.WalletDrainMin));
return slow + (1 - slow) * t;
double t = Math.Min(1, (value - Tuning.WalletDrainMin) / (Tuning.WalletDrainFull - Tuning.WalletDrainMin));
return Tuning.WalletDrainSlowDays - (Tuning.WalletDrainSlowDays - 1) * t;
}
/// <summary>被投毒的钱包今晚被转空的概率 = 1 / 期望天数(每晚独立掷一次)。</summary>
public static double DrainChance(double value) => value < Tuning.WalletDrainMin ? 0 : 1 / DrainDays(value);
/// <summary>转空:全部余额(含冻结的)连同污点、成本一起转走,返回转走的价值(信用点)。</summary>
public double Drain(Func<string, double> price)
{
+7
View File
@@ -29,6 +29,13 @@ namespace Decentralized
}
}
/// <summary>系统剪贴板(游戏里的输入框也能 Ctrl+V)。</summary>
public static class Clipboard
{
public static void Copy(string s) => GUIUtility.systemCopyBuffer = s ?? "";
public static string Paste() => (GUIUtility.systemCopyBuffer ?? "").Trim();
}
public static class Fmt
{
public static string Num(double v) => Math.Abs(v) >= 100 ? v.ToString("N0") : Math.Abs(v) >= 1 ? v.ToString("0.##") : v.ToString("0.####");
+14 -2
View File
@@ -268,7 +268,7 @@ black.kind.Contraband.3,Contraband (high)
black.kind.Tech,High-tech (markup)
nightlog.bm.ready,"Black market: {0} arrived; a courier will come by once you open."
item.web3guide.name,Web3 Handbook
item.web3guide.desc,"Bank: depositing turns credits into CRC in your Poinbase account; withdrawing turns CRC back into credits and goes through compliance screening, which looks at the Poinbase account's taint. Trading: buys are paid in the quote coin, sells in the traded coin. Mixer: pick a coin, amount and rounds; each round takes a fee and lowers taint, rounds equal days of waiting, and the coins come back to the same address; the more you deposit, the worse the cleaning. Hardware wallets: on-chain coins sit in PLEDGER hardware wallets, each with its own balances and taint; open and connect them on the terminal's Wallets page. The Web3 page's trading, black market and mixer use the connected wallet, and coin-paying customers and mining rewards go into it too. Payments (black market, mixer, Ponero desk, listing fees) start as an invoice on the terminal; type the payee address on the wallet and hold ✓ to sign. The last two characters of an address are a checksum, so a one-character typo is usually caught; once in a while it happens to be valid and the coins go to an address nobody uses. DEX swaps also need a signature on the wallet. Poinbase is a custodial account and can withdraw straight to the connected wallet; depositing from a wallet means sending to Poinbase's deposit address, which takes only CRC and PSC. More hardware wallets are sold by Dr. Jackson; drag one onto the trading terminal to add it, up to 5. The black market has cheaper ones. Black market: coin only, no CRC, 30% above normal channels, with extra markup on high-tech goods and GPUs; the coin you pay leaves its share of taint with the seller and the goods carry no stolen tag; GPUs may be fake, inspect them yourself. Orders are cheaper than spot but take days and may be intercepted; merchants sell by type at their own price factor, 5+ items get 5% off and 10+ get 10% off, and a bulk merchant appears at random with the lowest prices but six days of waiting; escrow costs 5%, is released on delivery and refunded if the seller runs; advance release takes another 10% off, but a scammer keeps the money and only sellers with 30+ trades accept; the platform itself can rug, be taken over by enforcement, or a seller can be arrested, after which your orders get traced and crime logged; forum rumors can be warnings. An intercepted parcel sometimes turns into enforcement at your door: signing logs the crime on the spot and seizes the goods, refusing only costs the payment. Selling: on the black market's Sell tab, list contraband, high-tech goods and GPUs from storage at 0.6x to 1.8x the reference price. The higher the multiplier, the slower the sale (below 0.9x about a day, 1.0x about two, 1.8x about fourteen), and the likelier and larger the taint on the payout, which also grows with how much you sold in the last 7 days; at most 10 listings at a time, 5% listing fee, repricing a listing costs 2% and the sale speed follows the new multiplier, 8% commission, payout held 3 days before reaching your wallet; a fake card may be spotted and returned, hurting your rating; if the platform rugs, listed goods and unreleased payouts are gone. Anonymous-coin swap (Web3 trade page, needs black-market standing): Ponero is a privacy coin and swapping in wipes the taint on that value; 6% fee each way with a daily limit; coin swapped out of Ponero has no origin and cash-out screening gets stricter."
item.web3guide.desc,"Bank: depositing turns credits into CRC in your Poinbase account; withdrawing turns CRC back into credits and goes through compliance screening, which looks at the Poinbase account's taint. Trading: buys are paid in the quote coin, sells in the traded coin. Mixer: pick a coin, amount and rounds; each round takes a fee and lowers taint, rounds equal days of waiting, and the coins come back to the same address; the more you deposit, the worse the cleaning. Hardware wallets: on-chain coins sit in PLEDGER hardware wallets, each with its own balances and taint; open and connect them on the terminal's Wallets page. The Web3 page's trading, black market and mixer use the connected wallet, and coin-paying customers and mining rewards go into it too. Payments (black market, mixer, Ponero desk, listing fees) start as an invoice on the terminal; type the payee address on the wallet and hold ✓ to sign. The last two characters of an address are a checksum, so a one-character typo is usually caught; once in a while it happens to be valid and the coins go to an address nobody uses. DEX swaps also need a signature on the wallet. Poinbase is a custodial account and can withdraw straight to the connected wallet; depositing from a wallet means sending to Poinbase's deposit address, which takes only CRC and PSC. More hardware wallets are sold by Dr. Jackson; drag one onto the trading terminal to add it, up to 5. The black market has cheaper ones. A wallet you no longer want can be destroyed on the Wallets page once its coins are moved out. Black market: coin only, no CRC, 30% above normal channels, with extra markup on high-tech goods and GPUs; the coin you pay leaves its share of taint with the seller and the goods carry no stolen tag; GPUs may be fake, inspect them yourself. Orders are cheaper than spot but take days and may be intercepted; merchants sell by type at their own price factor, 5+ items get 5% off and 10+ get 10% off, and a bulk merchant appears at random with the lowest prices but six days of waiting; escrow costs 5%, is released on delivery and refunded if the seller runs; advance release takes another 10% off, but a scammer keeps the money and only sellers with 30+ trades accept; the platform itself can rug, be taken over by enforcement, or a seller can be arrested, after which your orders get traced and crime logged; forum rumors can be warnings. An intercepted parcel sometimes turns into enforcement at your door: signing logs the crime on the spot and seizes the goods, refusing only costs the payment. Selling: on the black market's Sell tab, list contraband, high-tech goods and GPUs from storage at 0.6x to 1.8x the reference price. The higher the multiplier, the slower the sale (below 0.9x about a day, 1.0x about two, 1.8x about fourteen), and the likelier and larger the taint on the payout, which also grows with how much you sold in the last 7 days; at most 10 listings at a time, 5% listing fee, repricing a listing costs 2% and the sale speed follows the new multiplier, 8% commission, payout held 3 days before reaching your wallet; a fake card may be spotted and returned, hurting your rating; if the platform rugs, listed goods and unreleased payouts are gone. Anonymous-coin swap (Web3 trade page, needs black-market standing): Ponero is a privacy coin and swapping in wipes the taint on that value; 6% fee each way with a daily limit; coin swapped out of Ponero has no origin and cash-out screening gets stricter."
courier.odd1,...Package is here.
courier.odd2,"(His hand stays in his pocket.) Sign here."
courier.sign,Sign
@@ -355,7 +355,6 @@ black.desk.pair,"PONERO [anonymous coin] price {0}"
black.pay.amount,"Amount: {0} {1} (about {2} credits)"
black.pay.wallet,Pay from wallet
hw.wallet.main,Main wallet
hw.balance,"{0} {1} ≈ {2}"
hw.total,"Total ≈ {0} credits"
hw.pending.bill,"Unpaid: {0}"
hw.pending.sign,"To sign: {0}"
@@ -415,3 +414,16 @@ nightlog.drained,"The coins in {0} were moved out, about {1} credits."
item.hwwallet.name,PLEDGER hardware wallet
item.hwwallet.desc,Large-screen hardware wallet. Drag it onto the trading terminal to add a wallet (up to 5).
poin.empty,The account is empty.
hw.copy,Copy
hw.paste,Paste
hw.copied,Address copied.
hw.copied.addr,"Copied {0}"
hw.balance.frozen," (frozen {0})"
wallets.destroy,Destroy
wallets.destroy.sure,Confirm
wallets.destroy.notempty,"{0} still holds coins. Move them out first."
wallets.destroyed,"Destroyed {0}: {1}"
hw.balance.value,"≈ {0}"
wallets.rename,Rename
wallets.rename.h,"New name for {0}"
wallets.rename.ok,OK
1 # Probably Decentralized localization table: English (en)
268 black.kind.Tech
269 nightlog.bm.ready
270 item.web3guide.name
271 item.web3guide.desc
272 courier.odd1
273 courier.odd2
274 courier.sign
355 black.pay.amount
356 black.pay.wallet
357 hw.wallet.main
hw.balance
358 hw.total
359 hw.pending.bill
360 hw.pending.sign
414 item.hwwallet.name
415 item.hwwallet.desc
416 poin.empty
417 hw.copy
418 hw.paste
419 hw.copied
420 hw.copied.addr
421 hw.balance.frozen
422 wallets.destroy
423 wallets.destroy.sure
424 wallets.destroy.notempty
425 wallets.destroyed
426 hw.balance.value
427 wallets.rename
428 wallets.rename.h
429 wallets.rename.ok
+14 -2
View File
@@ -267,7 +267,7 @@ black.kind.Contraband.3,违禁品(高)
black.kind.Tech,高科技物品(加价)
nightlog.bm.ready,"黑市:{0} 到货,开店后会有快递员上门。"
item.web3guide.name,Web3 手册
item.web3guide.desc,"银行:存入把信用点换成 Poinbase 账户里的 CRC;提取把 CRC 换回信用点,要过合规审查,审查看 Poinbase 账户的污点。交易:买入按计价币的数量支付,卖出按所交易币的数量卖出。混币器:选币种、填数量和轮数,每轮扣手续费并降低污点,轮数即等待天数,到期提回同一个地址;存入越多,清洗越差。硬件钱包:链上的币放在 PLEDGER 硬件钱包里,每个钱包的余额和污点各算各的;在终端的钱包页打开、连接。Web3 页的交易、黑市、混币器用已连接的钱包,用币付款的顾客和挖矿收益也进它。付款(黑市、混币器、Ponero 兑换处、上架费)先在终端开账单,再在钱包里手输收款地址、按住 ✓ 签名;地址末尾两位是校验位,输错一位多半会被拦下,偶尔碰巧合法,币就转进了没人用的地址。DEX 兑换也要在钱包上签名。Poinbase 是托管账户,可以直接提到已连接的钱包;从钱包充值要转到 Poinbase 的充值地址,只收 CRC 和 PSC。更多硬件钱包向杰克逊博士购买,拖到交易终端上添加,最多 5 个;黑市上也有更便宜的。黑市:只收币,不收 CRC,价格比正常渠道贵 30%,高科技物品和显卡另有加价;付出去的币带着按比例的污点留在卖家那边,货不带赃物标签;显卡不保证真假,自己鉴定。订单比现货便宜,要等几天,路上可能被截获;商家按类型卖货,价格系数各不相同,一次订 5 件以上 95 折、10 件以上 9 折,大宗商家随机出现,价格最低但要等六天;托管多付 5% 托管费,收货后放款,卖家跑路时退款;提前放款再打九折,卖家若是骗子钱就没了,只有成交 30 单以上的卖家接受;平台自己也可能跑路,也可能被执法接管,或者卖家落网,之后你的订单会被追查、记罪证;论坛上的传闻可以当预兆。被截获的包裹有时会变成执法人员上门:签收当场记罪证并查封,拒签只损失货款。出售:在黑市的卖货页把储藏区里的违禁品、高科技物品和显卡上架,标价是参考价的 0.6 到 1.8 倍。倍率越高成交越慢(0.9 以下约一天,1.0 约两天,1.8 约十四天),货款带污点的可能性和比例也越高,近 7 天出售的量越大,污点越重;同时最多上架 10 件,上架收 5% 上架费,上架后可以改价(收 2% 改价费,标价按参考价重算,成交的快慢跟着新倍率走),成交扣 8% 抽成,货款托管 3 天后进钱包;卖假卡可能被买家看穿而退货,拉低好评;平台跑路时上架的货和未放款的货款都没了。匿名币兑换(Web3 交易页,要黑市声望):Ponero 是隐私币,兑入时这部分价值的污点被清除;兑入、兑出各收 6% 手续费,每天有额度;从 Ponero 兑出的币来路不明,出金审查会加重。"
item.web3guide.desc,"银行:存入把信用点换成 Poinbase 账户里的 CRC;提取把 CRC 换回信用点,要过合规审查,审查看 Poinbase 账户的污点。交易:买入按计价币的数量支付,卖出按所交易币的数量卖出。混币器:选币种、填数量和轮数,每轮扣手续费并降低污点,轮数即等待天数,到期提回同一个地址;存入越多,清洗越差。硬件钱包:链上的币放在 PLEDGER 硬件钱包里,每个钱包的余额和污点各算各的;在终端的钱包页打开、连接。Web3 页的交易、黑市、混币器用已连接的钱包,用币付款的顾客和挖矿收益也进它。付款(黑市、混币器、Ponero 兑换处、上架费)先在终端开账单,再在钱包里手输收款地址、按住 ✓ 签名;地址末尾两位是校验位,输错一位多半会被拦下,偶尔碰巧合法,币就转进了没人用的地址。DEX 兑换也要在钱包上签名。Poinbase 是托管账户,可以直接提到已连接的钱包;从钱包充值要转到 Poinbase 的充值地址,只收 CRC 和 PSC。更多硬件钱包向杰克逊博士购买,拖到交易终端上添加,最多 5 个;黑市上也有更便宜的。不要的钱包可以在钱包页销毁,要先把币转走。黑市:只收币,不收 CRC,价格比正常渠道贵 30%,高科技物品和显卡另有加价;付出去的币带着按比例的污点留在卖家那边,货不带赃物标签;显卡不保证真假,自己鉴定。订单比现货便宜,要等几天,路上可能被截获;商家按类型卖货,价格系数各不相同,一次订 5 件以上 95 折、10 件以上 9 折,大宗商家随机出现,价格最低但要等六天;托管多付 5% 托管费,收货后放款,卖家跑路时退款;提前放款再打九折,卖家若是骗子钱就没了,只有成交 30 单以上的卖家接受;平台自己也可能跑路,也可能被执法接管,或者卖家落网,之后你的订单会被追查、记罪证;论坛上的传闻可以当预兆。被截获的包裹有时会变成执法人员上门:签收当场记罪证并查封,拒签只损失货款。出售:在黑市的卖货页把储藏区里的违禁品、高科技物品和显卡上架,标价是参考价的 0.6 到 1.8 倍。倍率越高成交越慢(0.9 以下约一天,1.0 约两天,1.8 约十四天),货款带污点的可能性和比例也越高,近 7 天出售的量越大,污点越重;同时最多上架 10 件,上架收 5% 上架费,上架后可以改价(收 2% 改价费,标价按参考价重算,成交的快慢跟着新倍率走),成交扣 8% 抽成,货款托管 3 天后进钱包;卖假卡可能被买家看穿而退货,拉低好评;平台跑路时上架的货和未放款的货款都没了。匿名币兑换(Web3 交易页,要黑市声望):Ponero 是隐私币,兑入时这部分价值的污点被清除;兑入、兑出各收 6% 手续费,每天有额度;从 Ponero 兑出的币来路不明,出金审查会加重。"
courier.odd1,……东西到了。
courier.odd2,"(他的手一直放在口袋里)在这儿签个字。"
courier.sign,签收
@@ -354,7 +354,6 @@ black.desk.pair,"PONERO [匿名币] 价格 {0}"
black.pay.amount,"金额:{0} {1}(约 {2} 信用点)"
black.pay.wallet,打开钱包转账
hw.wallet.main,主钱包
hw.balance,"{0} {1} ≈ {2}"
hw.total,"合计 ≈ {0} 信用点"
hw.pending.bill,"待付:{0}"
hw.pending.sign,"待签名:{0}"
@@ -414,3 +413,16 @@ nightlog.drained,"{0}:币被转走了,约 {1} 信用点。"
item.hwwallet.name,PLEDGER 硬件钱包
item.hwwallet.desc,大屏硬件钱包。拖到交易终端上,添加一个钱包(最多 5 个)。
poin.empty,账户为空。
hw.copy,复制
hw.paste,粘贴
hw.copied,地址已复制。
hw.copied.addr,"已复制 {0}"
hw.balance.frozen,"(冻结 {0})"
wallets.destroy,销毁
wallets.destroy.sure,确认销毁
wallets.destroy.notempty,"{0}:还有币,先转走。"
wallets.destroyed,"已销毁{0}:{1}"
hw.balance.value,"≈ {0}"
wallets.rename,改名
wallets.rename.h,"{0} 新名字"
wallets.rename.ok,确定
1 # Probably Decentralized 本地化表:简体中文(zh)
267 black.kind.Tech
268 nightlog.bm.ready
269 item.web3guide.name
270 item.web3guide.desc
271 courier.odd1
272 courier.odd2
273 courier.sign
354 black.pay.amount
355 black.pay.wallet
356 hw.wallet.main
hw.balance
357 hw.total
358 hw.pending.bill
359 hw.pending.sign
413 item.hwwallet.name
414 item.hwwallet.desc
415 poin.empty
416 hw.copy
417 hw.paste
418 hw.copied
419 hw.copied.addr
420 hw.balance.frozen
421 wallets.destroy
422 wallets.destroy.sure
423 wallets.destroy.notempty
424 wallets.destroyed
425 hw.balance.value
426 wallets.rename
427 wallets.rename.h
428 wallets.rename.ok
+4 -1
View File
@@ -37,9 +37,12 @@ namespace Decentralized
public Wallet Own(string address) => Wallets.Find(w => ChainAddress.Same(w.Address, address));
/// <summary>钱包编号:每添加一个加一,销毁后不回收,名字不会错位。</summary>
public int WalletSeq;
public Wallet AddWallet(bool poisoned)
{
var w = new Wallet { Address = ChainAddress.Make(Rng.NextUInt()), Poisoned = poisoned };
var w = new Wallet { Address = ChainAddress.Make(Rng.NextUInt()), Poisoned = poisoned, Name = (++WalletSeq).ToString() };
Wallets.Add(w);
return w;
}
+57 -1
View File
@@ -238,7 +238,10 @@ namespace Decentralized
Holdings(l, w, a => true);
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
l.AddSeparator();
l.BeginRow(8f, TextAnchor.MiddleLeft);
l.AddLabel(L.T("poin.deposit", d.Address("poinbase")));
l.AddButton(L.T("hw.copy"), () => { Clipboard.Copy(d.Address("poinbase")); _message = L.T("hw.copied.addr", d.Address("poinbase")); Open(); });
l.End();
}, r =>
{
r.AddHeader(L.T("trade.h"));
@@ -276,7 +279,7 @@ namespace Decentralized
{
var d = Session.Data;
b.AddLabel(L.T("wallets.count", d.Wallets.Count, Tuning.WalletMax));
var cols = new[] { 130f, 130f, 120f, 110f, 220f };
var cols = new[] { 100f, 110f, 85f, 70f, 375f };
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++)
{
@@ -290,12 +293,62 @@ namespace Decentralized
b.BeginPanel(cols[4], 30f, TextAnchor.MiddleLeft, 2f, 0f);
b.BeginRow(4f);
b.AddButton(L.T("wallets.open"), () => WalletWindow.Open(w));
b.AddButton(L.T("hw.copy"), () => { Clipboard.Copy(w.Address); _message = L.T("hw.copied.addr", w.Address); Open(); });
b.AddButton(L.T("wallets.rename"), () => { _renamePick = w.Address; _renameText = w.Label ?? ""; _message = null; Open(); });
if (idx != d.Current) b.AddButton(L.T("wallets.use"), () => { d.Current = idx; Session.Save(); Open(); });
else b.AddLabel(L.T("wallets.current"));
if (idx > 0) b.AddButton(_destroyPick == w.Address ? L.T("wallets.destroy.sure") : L.T("wallets.destroy"), () => DestroyWallet(w));
b.End();
b.End();
b.End();
}
if (_renamePick != null) RenameRow(b);
}
private static string _destroyPick, _renamePick, _renameText = "";
/// <summary>改名:表格下方一行输入框;留空恢复默认名。</summary>
private static void RenameRow(CustomUIBuilder b)
{
var w = Session.Data.Own(_renamePick);
if (w == null) { _renamePick = null; return; }
b.AddSeparator();
b.BeginRow(8f, TextAnchor.MiddleLeft);
b.AddLabel(L.T("wallets.rename.h", w.Address));
b.BeginPanel(220f, -1f, TextAnchor.MiddleLeft, 0f, 0f);
b.AddInput(WalletWindow.NameOf(w), _renameText, s => _renameText = s, "rename", s => _renameText = s);
b.End();
b.AddButton(L.T("wallets.rename.ok"), () =>
{
string name = (_renameText ?? "").Trim();
w.Label = name.Length == 0 ? null : name.Length > 16 ? name.Substring(0, 16) : name;
_renamePick = null;
Session.Save();
WalletWindow.RefreshDevice(w, true);
Open();
});
b.AddButton(L.T("black.edit.cancel"), () => { _renamePick = null; Open(); });
b.End();
}
/// <summary>销毁钱包(主钱包不能销毁):要先把币转走;点两次确认。之后回到这个地址的退款、回款、混币提回改进当前钱包。</summary>
private static void DestroyWallet(Wallet w)
{
var d = Session.Data;
int idx = d.Wallets.IndexOf(w);
if (idx <= 0) return;
if (w.Balances.Values.Any(v => v > 1e-9) || w.Frozen.Values.Any(v => v > 1e-9)) { _destroyPick = null; _message = L.T("wallets.destroy.notempty", WalletWindow.NameOf(w)); Open(); return; }
if (_destroyPick != w.Address) { _destroyPick = w.Address; _message = null; Open(); return; }
_destroyPick = null;
string name = WalletWindow.NameOf(w);
WalletWindow.Close(w);
var current = d.Hot;
d.Wallets.RemoveAt(idx);
d.Current = Math.Max(0, d.Wallets.IndexOf(current));
Log.Info($"销毁硬件钱包 {w.Address}{(w.Poisoned ? "(被投毒)" : "")}");
Session.Save();
_message = L.T("wallets.destroyed", name, w.Address);
Open();
}
/// <summary>硬件钱包拖到终端上:添加一个钱包(被投毒的钱包看起来一样)。</summary>
@@ -618,7 +671,10 @@ namespace Decentralized
var p = WalletWindow.Pending;
b.AddHeader(L.T("black.pay.confirm"));
b.AddLabel(p.Summary);
b.BeginRow(8f, TextAnchor.MiddleLeft);
b.AddLabel(L.T("black.pay.to", p.Address));
b.AddButton(L.T("hw.copy"), () => { Clipboard.Copy(p.Address); _message = L.T("hw.copied.addr", p.Address); Open(); });
b.End();
b.AddLabel(L.T("black.pay.amount", WalletWindow.Amt(p.Amount), p.Asset, Fmt.Credits(p.Amount * Session.Price(p.Asset))));
b.BeginRow();
b.AddButton(L.T("black.pay.wallet"), () => WalletWindow.OpenForBill(p));
+61 -9
View File
@@ -68,8 +68,9 @@ namespace Decentralized
public static string NameOf(Wallet w)
{
if (!string.IsNullOrWhiteSpace(w.Label)) return w.Label;
int i = Session.Data.Wallets.IndexOf(w);
return i == 0 ? L.T("hw.wallet.main") : L.T("hw.wallet.n", i + 1);
return i == 0 ? L.T("hw.wallet.main") : L.T("hw.wallet.n", w.Name ?? (i + 1).ToString());
}
private static Device Dev(Wallet w)
@@ -80,6 +81,14 @@ namespace Decentralized
private static bool IsOpen(Device dev) => CustomUIManager.Instance != null && CustomUIManager.Instance.IsOpen(dev.Id);
/// <summary>钱包被销毁:关掉它的窗口,丢掉设备状态。</summary>
public static void Close(Wallet w)
{
if (w == null || !Devices.TryGetValue(w.Address, out var dev)) return;
if (IsOpen(dev)) CustomUIManager.Instance.CloseWindow(dev.Id);
Devices.Remove(w.Address);
}
/// <summary>读档 / 新游戏后清掉上一局的设备状态并关窗。</summary>
public static void Reset()
{
@@ -115,6 +124,7 @@ namespace Decentralized
WalletDriver.Ensure();
var b = CustomUIManager.Window(dev.Id, "PLEDGER");
if (b == null) return;
_rows.Clear(); _clear.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));
@@ -181,6 +191,21 @@ namespace Decentralized
if (img != null) img.color = Key;
foreach (var t in btn.GetComponentsInChildren<TextMeshProUGUI>(true)) t.color = KeyText;
}
foreach (var img in _clear) if (img != null) img.color = new Color(0, 0, 0, 0);
// 余额行:整行可点,选中的一行反色高亮
foreach (var (root, img, asset) in _rows)
{
if (root == null || img == null) continue;
bool on = asset == dev.Asset;
img.sprite = MachineryStyle.White; img.type = Image.Type.Simple; img.raycastTarget = true;
img.color = on ? Ink : new Color(0, 0, 0, 0.04f);
foreach (var t in root.GetComponentsInChildren<TextMeshProUGUI>(true)) t.color = on ? KeyText : Ink;
var btn = root.GetComponent<Button>() ?? root.AddComponent<Button>();
btn.transition = Selectable.Transition.None;
btn.onClick.RemoveAllListeners();
string a = asset;
btn.onClick.AddListener(() => { dev.Asset = a; dev.Note = null; Open(dev); });
}
// 签名要按住 ✓:在按键上挂一个按住计时
if (ok != null && dev.Screen == Screen.Review && ok.GetComponent<HoldToSign>() == null) ok.AddComponent<HoldToSign>().Device = dev.Address;
}
@@ -203,6 +228,9 @@ namespace Decentralized
}
private static readonly Dictionary<string, Sprite> Sliced = new Dictionary<string, Sprite>();
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>();
public static string Amt(double v) => v.ToString(v >= 100 ? "0.##" : "0.####", CultureInfo.InvariantCulture);
@@ -214,16 +242,34 @@ namespace Decentralized
private static void HomeScreen(CustomUIBuilder b, Device dev)
{
var w = dev.Wallet;
b.AddHeader(NameOf(w));
b.AddLabel("<size=22><b>" + NameOf(w) + "</b></size>");
b.BeginRow(8f, TextAnchor.MiddleLeft);
b.AddLabel(w.Address, "addr_dim");
b.AddButton(L.T("hw.copy"), () => { Clipboard.Copy(w.Address); dev.Note = L.T("hw.copied"); Open(dev); });
b.End();
b.AddSeparator();
bool any = false;
foreach (var kv in w.Balances.OrderBy(k => k.Key))
// 余额:每个币一行,左边币种、右边数量与估值,全部显示(含 0);点一行选中,发送时默认用它
foreach (var a in SendAssets)
{
any = true;
b.AddLabel(L.T("hw.balance", kv.Key, Amt(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key))));
double q = w.Get(a);
b.BeginPanel(RowW, 32f, TextAnchor.MiddleLeft, 6f, 0f);
_rows.Add((b.Last.root, b.Last.image, a));
b.BeginRow(0f, TextAnchor.MiddleLeft);
b.BeginPanel(90f, 26f, TextAnchor.MiddleLeft, 0f, 0f);
_clear.Add(b.Last.image);
b.AddLabel(a);
b.End();
b.BeginPanel(130f, 26f, TextAnchor.MiddleRight, 0f, 0f);
_clear.Add(b.Last.image);
b.AddLabel(Amt(q) + (w.Frozen.TryGetValue(a, out var fz) && fz > 0 ? L.T("hw.balance.frozen", Amt(fz)) : ""));
b.End();
b.BeginPanel(RowW - 12f - 90f - 130f, 26f, TextAnchor.MiddleRight, 0f, 0f);
_clear.Add(b.Last.image);
b.AddLabel(L.T("hw.balance.value", Fmt.Credits(q * Session.Price(a))));
b.End();
b.End();
b.End();
}
if (!any) b.AddLabel(L.T("wallet.empty"));
b.AddSeparator();
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)));
@@ -240,7 +286,13 @@ namespace Decentralized
var w = dev.Wallet;
b.AddHeader(L.T("hw.send"));
b.AddLabel(L.T("hw.to"));
b.BeginRow(6f, TextAnchor.MiddleLeft);
b.BeginPanel(RowW - 70f, -1f, TextAnchor.MiddleLeft, 0f, 0f);
_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.End();
b.BeginRow();
foreach (var a in SendAssets)
{
@@ -452,10 +504,10 @@ namespace Decentralized
}
/// <summary>别的交易让这个钱包的余额变了:开着的话刷新主页。</summary>
public static void RefreshDevice(Wallet w)
public static void RefreshDevice(Wallet w, bool any = false)
{
if (w == null || !Devices.TryGetValue(w.Address, out var dev) || !IsOpen(dev)) return;
if (dev.Screen == Screen.Home || dev.Screen == Screen.Send) Open(dev);
if (any || dev.Screen == Screen.Home || dev.Screen == Screen.Send) Open(dev);
}
/// <summary>每帧推进出块动画:只改状态文字和进度条,不重建窗口。</summary>
@@ -814,6 +814,11 @@ internal static class Program
Check(Math.Abs(Wallet.DrainChance(Tuning.WalletDrainMin) - 1 / 7.0) < 1e-9, "刚到门槛:每晚 1/7(平均 7 天)");
Check(Wallet.DrainChance(Tuning.WalletDrainFull) == 1 && Wallet.DrainChance(Tuning.WalletDrainFull * 3) == 1, "到满额当晚就转走");
Check(Wallet.DrainChance(10000) > Wallet.DrainChance(5000), "钱越多越快");
double mid = (Tuning.WalletDrainMin + Tuning.WalletDrainFull) / 2;
Check(Math.Abs(Wallet.DrainDays(mid) - 4) < 1e-9 && Math.Abs(Wallet.DrainChance(mid) - 0.25) < 1e-9, "中间值:期望 4 天,每晚 25%");
var rng = new Rng(9); double sum = 0; int n = 4000;
for (int i = 0; i < n; i++) { int d = 1; while (!rng.Chance(Wallet.DrainChance(mid))) d++; sum += d; }
Check(Math.Abs(sum / n - 4) < 0.25, $"模拟平均 {sum / n:0.00} 天被转走(期望 4)");
var w = new Wallet { Address = "a" };
w.Add(Assets.PSC, 50); w.Add(Assets.CRT, 1000); w.Taint = 300; w.Basis = 900; w.Freeze(Assets.CRT, 0.5, 9);
double v = w.Drain(a => a == Assets.PSC ? 100 : 1);