diff --git a/CHANGELOG.md b/CHANGELOG.md index 28ba6b1..a61cb19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 空账户和"账户污点比例"两处改字(已编译,没重进游戏看)。 diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index 643ee16..e0ee2fc 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -139,7 +139,7 @@ namespace Decentralized.Domain /// 硬件钱包:最多几个(含主钱包);杰克逊博士的售价与每次进货上架的概率;Web3 商城的价格范围、每周平台现货上架的概率与件数上限;商城货里被投毒的比例。 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; - /// 被投毒的钱包:币的总价值达到 DrainMin 起,每晚有概率被转空;价值越高越快:每晚概率从门槛处的 1 / SlowDays(平均 7 天)按价值线性升到 DrainFull 处的 1(当晚就转走)。 + /// 被投毒的钱包:币的总价值达到 DrainMin 起,每晚有概率被转空;价值越高越快:期望天数从门槛处的 SlowDays(7 天)按价值线性降到 DrainFull 处的 1 天,每晚概率 = 1 / 期望天数。 public const double WalletDrainMin = 3000, WalletDrainFull = 20000, WalletDrainSlowDays = 7; /// 主交易对(信用点一侧)的深度随天数增长。 diff --git a/src/Decentralized.Domain/Wallet.cs b/src/Decentralized.Domain/Wallet.cs index b6a6cf9..51e8259 100644 --- a/src/Decentralized.Domain/Wallet.cs +++ b/src/Decentralized.Domain/Wallet.cs @@ -18,19 +18,23 @@ namespace Decentralized.Domain public double Basis; /// 被执法标记。 public bool Flagged; - /// 显示名(终端钱包页、硬件钱包标题)。 + /// 钱包编号("钱包 n"),添加时定下,销毁别的钱包也不变。 public string Name; + /// 玩家起的名字;空时用默认名。 + public string Label; /// 硬件钱包被动过手脚(供应链投毒):别人手里有助记词。玩家看不到。 public bool Poisoned; - /// 被投毒的钱包今晚被转空的概率:总价值不到门槛时为 0,越多越高(1–7 天)。 - public static double DrainChance(double value) + /// 被投毒的钱包被转空前的期望天数:门槛处 7 天,按价值线性降到满额处 1 天;不到门槛不会被转。 + 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; } + /// 被投毒的钱包今晚被转空的概率 = 1 / 期望天数(每晚独立掷一次)。 + public static double DrainChance(double value) => value < Tuning.WalletDrainMin ? 0 : 1 / DrainDays(value); + /// 转空:全部余额(含冻结的)连同污点、成本一起转走,返回转走的价值(信用点)。 public double Drain(Func price) { diff --git a/src/Decentralized/Core.cs b/src/Decentralized/Core.cs index 4c3652d..77cc00d 100644 --- a/src/Decentralized/Core.cs +++ b/src/Decentralized/Core.cs @@ -29,6 +29,13 @@ namespace Decentralized } } + /// 系统剪贴板(游戏里的输入框也能 Ctrl+V)。 + 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.####"); diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index 90902ea..e9057dc 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -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 diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv index 6f9208c..e09b794 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -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,确定 diff --git a/src/Decentralized/Session.cs b/src/Decentralized/Session.cs index d65a8a1..231b799 100644 --- a/src/Decentralized/Session.cs +++ b/src/Decentralized/Session.cs @@ -37,9 +37,12 @@ namespace Decentralized public Wallet Own(string address) => Wallets.Find(w => ChainAddress.Same(w.Address, address)); + /// 钱包编号:每添加一个加一,销毁后不回收,名字不会错位。 + 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; } diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs index 02c6b31..1adf504 100644 --- a/src/Decentralized/TerminalWindow.cs +++ b/src/Decentralized/TerminalWindow.cs @@ -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 = ""; + + /// 改名:表格下方一行输入框;留空恢复默认名。 + 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(); + } + + /// 销毁钱包(主钱包不能销毁):要先把币转走;点两次确认。之后回到这个地址的退款、回款、混币提回改进当前钱包。 + 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(); } /// 硬件钱包拖到终端上:添加一个钱包(被投毒的钱包看起来一样)。 @@ -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)); diff --git a/src/Decentralized/WalletWindow.cs b/src/Decentralized/WalletWindow.cs index c02ab8d..df2433c 100644 --- a/src/Decentralized/WalletWindow.cs +++ b/src/Decentralized/WalletWindow.cs @@ -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); + /// 钱包被销毁:关掉它的窗口,丢掉设备状态。 + 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); + } + /// 读档 / 新游戏后清掉上一局的设备状态并关窗。 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(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(true)) t.color = on ? KeyText : Ink; + var btn = root.GetComponent