Chain drama: fee tiers, congestion, mempool drops, address poisoning, clipboard hijack, phishing; selected-button style

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:29:54 +08:00
1 parent 2b781acd82
commit dbba8b4376
15 files changed
+357 -20

No files matched your search

+7
View File
@@ -18,6 +18,13 @@
- PLEDGER 主页余额改成三列(币种 | 数量 | 估值)右对齐,五种币全部显示(含 0,冻结的另注);点一行选中,整行反色高亮,发送时默认用选中的币。 - PLEDGER 主页余额改成三列(币种 | 数量 | 估值)右对齐,五种币全部显示(含 0,冻结的另注);点一行选中,整行反色高亮,发送时默认用选中的币。
- 钱包页:每个钱包可以改名(留空恢复默认,最长 16 字),名字在终端和 PLEDGER 上都用;非主钱包可以销毁(点两次确认,钱包里还有币时不让销毁)。钱包编号固定,销毁别的钱包后名字不会错位。 - 钱包页:每个钱包可以改名(留空恢复默认,最长 16 字),名字在终端和 PLEDGER 上都用;非主钱包可以销毁(点两次确认,钱包里还有币时不让销毁)。钱包编号固定,销毁别的钱包后名字不会错位。
- 游戏内验证(这一轮):复制钱包 3 地址后在主钱包发送页粘贴,地址正确;选中 CRT 行高亮,进发送页默认 CRT;向钱包 3 转 1 CRT 后点销毁被拦下;钱包 2 二次确认后销毁,钱包 3 仍叫"钱包 3";钱包 2 改名为 cold,终端与 PLEDGER 都显示新名字;三列余额对齐。PLEDGER 上名字被标题样式转成大写,已改成普通粗体(编译通过,没重进游戏看)。没有验证:游戏输入框里直接 Ctrl+V。 - 游戏内验证(这一轮):复制钱包 3 地址后在主钱包发送页粘贴,地址正确;选中 CRT 行高亮,进发送页默认 CRT;向钱包 3 转 1 CRT 后点销毁被拦下;钱包 2 二次确认后销毁,钱包 3 仍叫"钱包 3";钱包 2 改名为 cold,终端与 PLEDGER 都显示新名字;三列余额对齐。PLEDGER 上名字被标题样式转成大写,已改成普通粗体(编译通过,没重进游戏看)。没有验证:游戏输入框里直接 Ctrl+V。
- 转账手续费档位(核对页选 慢 / 普通 / 快):0.5 / 1.5 / 4 信用点等值,用转出的币扣(DEX 签名用付出的币);需要的确认数随金额:3 次,≥ 2000 时 6 次,≥ 10000 时 12 次,动画随之变长。
- 网络拥堵:每晚 12% 开始,持续 1–2 天,上报纸;拥堵时手续费 ×4,普通档确认 ×2 慢、慢档 ×3.5 慢,慢档 30% 被挤出内存池(未成交,手续费照扣,账单留着可以重付)。PLEDGER 主页和核对页显示"网络拥堵"。
- 地址投毒:每笔转给外部地址的转账有 35% 被盯上,1–2 天后钱包收到一笔来自相似地址的 0 币转入(前 3 位和末 2 位校验位一样,缩写完全相同)。PLEDGER 发送页新增"最近"列表(← 转入 / → 转出,缩写地址),点一下填入地址,投毒地址混在里面;核对页对有往来的地址显示"有往来记录",投毒地址也会显示。
- 剪贴板木马:被投毒的钱包上点"粘贴",30% 把地址换成相似地址(和一次转空并存)。
- 钓鱼公告:每晚 5%,报纸上"Poinbase 充值地址迁移"或黑市论坛上"混币器新地址",给的是没人用的地址,转进去就没了。
- PLEDGER 屏上的选择按钮(币种、手续费档位):选中的一个白底黑框黑字,四角带取景框角标,不再用方括号。
- 游戏内验证(第三批):手续费 1.5 CRT 扣除;拥堵时核对页手续费 ×4、慢档提示;最近列表出现投毒的"← 0x123…99 0 PSC",点它进核对页显示真实地址 0x12342499 与"有往来记录",签名后币转进投毒地址;慢档连续 13 次里 4 次"未成交",手续费照扣;论坛出现钓鱼帖;选中样式截图看过。没有在游戏里看到:报纸上的拥堵 / 钓鱼新闻(F9 面板那次没打开,代码走的是已用过的新闻接口)、剪贴板木马(只有被投毒的钱包触发,没专门测)、大额转账的 6 / 12 次确认动画(单元测试覆盖确认数)。
- 存档不兼容旧版:链上的币归到新建的主钱包,Poinbase 账户从 0 开始。 - 存档不兼容旧版:链上的币归到新建的主钱包,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 空账户和"账户污点比例"两处改字(已编译,没重进游戏看)。 - 游戏内验证(多钱包):两件硬件钱包(一正一投毒)作用于终端,钱包页出现钱包 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 空账户和"账户污点比例"两处改字(已编译,没重进游戏看)。
+13
View File
@@ -681,10 +681,23 @@ def rounded(size, r, fill, light=None, dark=None):
return c return c
def hw_selected():
"""PLEDGER 屏上选中的按钮(16×16,九宫格边 5):墨水屏白底、1 像素黑框,四角向外加粗的取景框角标(准星)。"""
c = Canvas(16, 16)
c.rect(0, 0, 15, 15, 'P')
for i in range(16):
c.put(i, 1, 'n'); c.put(i, 14, 'n'); c.put(1, i, 'n'); c.put(14, i, 'n')
for (x, y, dx, dy) in ((0, 0, 1, 1), (15, 0, -1, 1), (0, 15, 1, -1), (15, 15, -1, -1)):
for k in range(4):
c.put(x + dx * k, y, 'n'); c.put(x, y + dy * k, 'n')
return c
UI_ICONS = {'ui_bank': ui_bank, 'ui_poinbase': ui_poinbase, 'ui_web3': ui_web3} UI_ICONS = {'ui_bank': ui_bank, 'ui_poinbase': ui_poinbase, 'ui_web3': ui_web3}
UI_ICONS['ui_hw_shell'] = lambda: rounded(32, 7, 'K', 'E', 'z') UI_ICONS['ui_hw_shell'] = lambda: rounded(32, 7, 'K', 'E', 'z')
UI_ICONS['ui_hw_bezel'] = lambda: rounded(16, 4, 'n') UI_ICONS['ui_hw_bezel'] = lambda: rounded(16, 4, 'n')
UI_ICONS['ui_hw_paper'] = lambda: rounded(12, 2, 'P') UI_ICONS['ui_hw_paper'] = lambda: rounded(12, 2, 'P')
UI_ICONS['ui_hw_sel'] = hw_selected
UI_ICONS['ui_hw_ok'] = lambda: hw_button('ok') UI_ICONS['ui_hw_ok'] = lambda: hw_button('ok')
UI_ICONS['ui_hw_no'] = lambda: hw_button('no') UI_ICONS['ui_hw_no'] = lambda: hw_button('no')
+26
View File
@@ -41,5 +41,31 @@ namespace Decentralized.Domain
} }
public static bool Same(string a, string b) => Normalize(a) == Normalize(b); public static bool Same(string a, string b) => Normalize(a) == Normalize(b);
/// <summary>缩写(钱包列表里的样子):0x + 主体前 3 位 + … + 校验位,例如 0x4b9…17。</summary>
public static string Short(string a)
{
a = Normalize(a);
return WellFormed(a) ? a.Substring(0, 5) + "…" + a.Substring(2 + BodyLength) : a;
}
/// <summary>
/// 相似地址(地址投毒、剪贴板木马用):主体前 3 位和校验位都和原地址一样,缩写看起来完全相同,中间几位不同;本身是合法地址,
/// 币转过去就进了骗子手里。找不到校验位相同的就只保留前 3 位。
/// </summary>
public static string Lookalike(string a, Rng rng)
{
a = Normalize(a);
if (!Valid(a)) return Make(rng.NextUInt());
string prefix = a.Substring(2, 3), check = a.Substring(2 + BodyLength), body0 = a.Substring(2, BodyLength);
for (int i = 0; i < 20000; i++)
{
string body = prefix + (rng.NextUInt() & 0xFFF).ToString("x3");
if (body != body0 && Check(body) == check) return "0x" + body + check;
}
string fallback;
do fallback = prefix + (rng.NextUInt() & 0xFFF).ToString("x3"); while (fallback == body0);
return "0x" + fallback + Check(fallback);
}
} }
} }
+12
View File
@@ -142,6 +142,18 @@ namespace Decentralized.Domain
/// <summary>被投毒的钱包:币的总价值达到 DrainMin 起,每晚有概率被转空;价值越高越快:期望天数从门槛处的 SlowDays(7 天)按价值线性降到 DrainFull 处的 1 天,每晚概率 = 1 / 期望天数。</summary> /// <summary>被投毒的钱包:币的总价值达到 DrainMin 起,每晚有概率被转空;价值越高越快:期望天数从门槛处的 SlowDays(7 天)按价值线性降到 DrainFull 处的 1 天,每晚概率 = 1 / 期望天数。</summary>
public const double WalletDrainMin = 3000, WalletDrainFull = 20000, WalletDrainSlowDays = 7; public const double WalletDrainMin = 3000, WalletDrainFull = 20000, WalletDrainSlowDays = 7;
/// <summary>转账手续费(信用点等值,按转出的币扣):慢 / 普通 / 快;拥堵时乘以倍数。</summary>
public const double TxFeeSlow = 0.5, TxFeeNormal = 1.5, TxFeeFast = 4, TxCongestedFeeMult = 4;
/// <summary>网络拥堵:每晚开始拥堵的概率、持续天数上限;拥堵时普通档、慢档的确认间隔倍数,慢档被挤出内存池的概率。</summary>
public const double TxCongestionChance = 0.12, TxCongestedNormalSlow = 2, TxCongestedSlowSlow = 3.5, TxSlowDrop = 0.3;
public const int TxCongestionDaysMax = 2;
/// <summary>确认数:默认 3;金额(信用点)≥ MidValue 时 6,≥ BigValue 时 12。</summary>
public const int TxConfirmBase = 3, TxConfirmMid = 6, TxConfirmBig = 12;
public const double TxConfirmMidValue = 2000, TxConfirmBigValue = 10000;
/// <summary>地址投毒:每笔转出后被盯上的概率,0 币转入在 1–2 天后出现。剪贴板木马(被投毒的钱包):粘贴时换成相似地址的概率。钓鱼公告:每晚出现的概率。</summary>
public const double TxPoisonChance = 0.35, TxHijackChance = 0.3, TxPhishChance = 0.05;
public const int TxPoisonDelayMax = 2;
/// <summary>主交易对(信用点一侧)的深度随天数增长。</summary> /// <summary>主交易对(信用点一侧)的深度随天数增长。</summary>
public static double PoolDepth(int day) => PoolDepth0 * (1 + day / 60.0); public static double PoolDepth(int day) => PoolDepth0 * (1 + day / 60.0);
} }
+43
View File
@@ -0,0 +1,43 @@
using System.Collections.Generic;
namespace Decentralized.Domain
{
/// <summary>钱包里的一条转账记录:对方地址、币种、数量、哪天、转入还是转出。地址投毒的 0 币转入也记在这里。</summary>
public sealed class TxRecord
{
public string Addr, Asset;
public double Amount;
public int Day;
public bool In;
}
/// <summary>手续费档位:慢最便宜,网络拥堵时可能被挤出内存池;快最贵,拥堵时也照常确认。</summary>
public enum FeeTier { Slow, Normal, Fast }
/// <summary>链上转账的规则:手续费、需要的确认数、拥堵时慢档被丢弃的概率。</summary>
public static class TxRules
{
/// <summary>手续费(信用点等值,按转出的币扣):档位基础价 × 拥堵倍数。</summary>
public static double FeeCredits(FeeTier t, bool congested) =>
(t == FeeTier.Slow ? Tuning.TxFeeSlow : t == FeeTier.Normal ? Tuning.TxFeeNormal : Tuning.TxFeeFast) * (congested ? Tuning.TxCongestedFeeMult : 1);
/// <summary>需要的确认数:金额越大越多。</summary>
public static int Confirmations(double valueCredits) =>
valueCredits >= Tuning.TxConfirmBigValue ? Tuning.TxConfirmBig : valueCredits >= Tuning.TxConfirmMidValue ? Tuning.TxConfirmMid : Tuning.TxConfirmBase;
/// <summary>每次确认的间隔倍数:拥堵时慢档、普通档变慢。</summary>
public static double Slowdown(FeeTier t, bool congested) => !congested ? 1 : t == FeeTier.Fast ? 1 : t == FeeTier.Normal ? Tuning.TxCongestedNormalSlow : Tuning.TxCongestedSlowSlow;
/// <summary>拥堵时慢档交易被挤出内存池的概率(交易作废,手续费照扣)。</summary>
public static double DropChance(FeeTier t, bool congested) => congested && t == FeeTier.Slow ? Tuning.TxSlowDrop : 0;
/// <summary>转账记录最多保留几条。</summary>
public const int HistoryMax = 20;
public static void Record(List<TxRecord> history, TxRecord r)
{
history.Insert(0, r);
if (history.Count > HistoryMax) history.RemoveRange(HistoryMax, history.Count - HistoryMax);
}
}
}
+3
View File
@@ -22,6 +22,9 @@ namespace Decentralized.Domain
public string Name; public string Name;
/// <summary>玩家起的名字;空时用默认名。</summary> /// <summary>玩家起的名字;空时用默认名。</summary>
public string Label; public string Label;
/// <summary>转账记录(最新的在前),PLEDGER 发送页的"最近"列表从这里取。</summary>
public List<TxRecord> History = new List<TxRecord>();
/// <summary>硬件钱包被动过手脚(供应链投毒):别人手里有助记词。玩家看不到。</summary> /// <summary>硬件钱包被动过手脚(供应链投毒):别人手里有助记词。玩家看不到。</summary>
public bool Poisoned; public bool Poisoned;
+13
View File
@@ -69,6 +69,19 @@ namespace Decentralized
Log.Info($"买入测试:卡 #{item.uniqueId} 买前报价 {before},买下后价值 {item.unitValue},真实 {CardValue.True(Items.ToUnit(item))},说法 {(Items.GetClaim(item)?.Encode() ?? "无")}"); Log.Info($"买入测试:卡 #{item.uniqueId} 买前报价 {before},买下后价值 {item.unitValue},真实 {CardValue.True(Items.ToUnit(item))},说法 {(Items.GetClaim(item)?.Encode() ?? "无")}");
})); }));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Hardware wallets (test)", () => { Items.Give(Items.NewHwWallet(false)); Items.Give(Items.NewHwWallet(true)); Log.Info("测试:给了一个正品、一个被投毒的硬件钱包"); }); ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Hardware wallets (test)", () => { Items.Give(Items.NewHwWallet(false)); Items.Give(Items.NewHwWallet(true)); Log.Info("测试:给了一个正品、一个被投毒的硬件钱包"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Chain drama (test)", () => Log.Guard("链上事件测试", () =>
{
// 今天拥堵;当前钱包最近转出过的地址立刻收到相似地址的 0 币转入;报纸和论坛各来一条钓鱼
var d = Session.Data;
int day = StoreStation.GetDayCounter();
d.CongestedFrom = day; d.CongestedUntil = day + 1;
var last = d.Hot.History.FirstOrDefault(r => !r.In);
if (last != null) TxRules.Record(d.Hot.History, new TxRecord { Addr = ChainAddress.Lookalike(last.Addr, d.Rng), Asset = Assets.PSC, Amount = 0, Day = day, In = true });
string fake = ChainAddress.Make(d.Rng.NextUInt());
News.Show("pd_news_phish_test_" + day, L.T("news.phish.title"), L.T("news.phish.desc", fake), 2);
d.Black.Forum.Add("phish:" + ChainAddress.Make(d.Rng.NextUInt()));
Log.Info($"测试:拥堵到第 {d.CongestedUntil} 天;投毒 {(last != null ? last.Addr : "无转出记录")};钓鱼 {fake}");
}));
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dr. Jackson stock (test)", () => ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Dr. Jackson stock (test)", () =>
{ {
EmporiumEntry.Instance.ClearFrontInv(); EmporiumEntry.Instance.ClearFrontInv();
+21 -1
View File
@@ -268,7 +268,7 @@ black.kind.Contraband.3,Contraband (high)
black.kind.Tech,High-tech (markup) black.kind.Tech,High-tech (markup)
nightlog.bm.ready,"Black market: {0} arrived; a courier will come by once you open." nightlog.bm.ready,"Black market: {0} arrived; a courier will come by once you open."
item.web3guide.name,Web3 Handbook 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. 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." 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. Transfers pay a fee: slow, normal or fast; when the network is congested fees rise and slow transactions may be dropped from the mempool. Check the full address before sending. 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.odd1,...Package is here.
courier.odd2,"(His hand stays in his pocket.) Sign here." courier.odd2,"(His hand stays in his pocket.) Sign here."
courier.sign,Sign courier.sign,Sign
@@ -427,3 +427,23 @@ hw.balance.value,"≈ {0}"
wallets.rename,Rename wallets.rename,Rename
wallets.rename.h,"New name for {0}" wallets.rename.h,"New name for {0}"
wallets.rename.ok,OK wallets.rename.ok,OK
hw.fee.slow,Slow
hw.fee.normal,Normal
hw.fee.fast,Fast
hw.fee,"Fee {0} {1} (about {2} credits)"
hw.confs,"{0} confirmations needed"
hw.congested,Network congested
hw.congested.slow,Network congested: slow fee may be dropped from the mempool
hw.recent,Recent
hw.recent.out,"→ {0} {1} {2}"
hw.recent.in,"← {0} {1} {2}"
hw.review.history,Address in your history
hw.tx.stuck,Queued in the mempool…
hw.tx.dropped,Dropped from the mempool. Nothing was sent; the fee is still charged.
hw.dropped.h,Not sent
hw.err.fee,Not enough to cover the fee.
news.congest.title,Chain congestion
news.congest.desc,An airdrop rush has clogged the chain. Fees are several times higher and low-fee transactions may be dropped from the mempool.
news.phish.title,Poinbase notice: deposit address migration
news.phish.desc,"Poinbase has upgraded its wallet system. The old deposit address will be retired; please send assets to the new address {0}."
black.forum.phish,"Someone posted the mixer's new address: {0}, saying the old one is being watched."
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
427 wallets.rename
428 wallets.rename.h
429 wallets.rename.ok
430 hw.fee.slow
431 hw.fee.normal
432 hw.fee.fast
433 hw.fee
434 hw.confs
435 hw.congested
436 hw.congested.slow
437 hw.recent
438 hw.recent.out
439 hw.recent.in
440 hw.review.history
441 hw.tx.stuck
442 hw.tx.dropped
443 hw.dropped.h
444 hw.err.fee
445 news.congest.title
446 news.congest.desc
447 news.phish.title
448 news.phish.desc
449 black.forum.phish
+21 -1
View File
@@ -267,7 +267,7 @@ black.kind.Contraband.3,违禁品(高)
black.kind.Tech,高科技物品(加价) black.kind.Tech,高科技物品(加价)
nightlog.bm.ready,"黑市:{0} 到货,开店后会有快递员上门。" nightlog.bm.ready,"黑市:{0} 到货,开店后会有快递员上门。"
item.web3guide.name,Web3 手册 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.odd1,……东西到了。
courier.odd2,"(他的手一直放在口袋里)在这儿签个字。" courier.odd2,"(他的手一直放在口袋里)在这儿签个字。"
courier.sign,签收 courier.sign,签收
@@ -426,3 +426,23 @@ hw.balance.value,"≈ {0}"
wallets.rename,改名 wallets.rename,改名
wallets.rename.h,"{0} 新名字" wallets.rename.h,"{0} 新名字"
wallets.rename.ok,确定 wallets.rename.ok,确定
hw.fee.slow,慢
hw.fee.normal,普通
hw.fee.fast,快
hw.fee,"手续费 {0} {1}(约 {2} 信用点)"
hw.confs,"需要 {0} 次确认"
hw.congested,网络拥堵
hw.congested.slow,网络拥堵:慢档可能被挤出内存池
hw.recent,最近
hw.recent.out,"→ {0} {1} {2}"
hw.recent.in,"← {0} {1} {2}"
hw.review.history,有往来记录
hw.tx.stuck,内存池排队中…
hw.tx.dropped,交易被挤出内存池,没有成交,手续费照扣。
hw.dropped.h,未成交
hw.err.fee,余额不够付手续费。
news.congest.title,链上拥堵
news.congest.desc,一批空投抢跑让链上排起长队,手续费翻了几倍,低手续费的交易可能被挤出内存池。
news.phish.title,Poinbase 公告:充值地址迁移
news.phish.desc,"Poinbase 完成钱包系统升级,旧充值地址即将停用,请将资产转入新地址 {0}。"
black.forum.phish,"有人贴了混币器的新地址:{0},说旧的被盯上了。"
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
426 wallets.rename
427 wallets.rename.h
428 wallets.rename.ok
429 hw.fee.slow
430 hw.fee.normal
431 hw.fee.fast
432 hw.fee
433 hw.confs
434 hw.congested
435 hw.congested.slow
436 hw.recent
437 hw.recent.out
438 hw.recent.in
439 hw.review.history
440 hw.tx.stuck
441 hw.tx.dropped
442 hw.dropped.h
443 hw.err.fee
444 news.congest.title
445 news.congest.desc
446 news.phish.title
447 news.phish.desc
448 black.forum.phish
Binary file not shown.

After

Width:  |  Height:  |  Size: 112 B

+28
View File
@@ -77,6 +77,33 @@ namespace Decentralized
Log.Guard("夜间结算", Settle); Log.Guard("夜间结算", Settle);
} }
/// <summary>链上的夜间事件:网络拥堵、地址投毒的 0 币转入、钓鱼公告。</summary>
private static void ChainNight(SaveData d, int day)
{
if (day >= d.CongestedUntil && d.Rng.Chance(Tuning.TxCongestionChance))
{
d.CongestedFrom = day + 1;
d.CongestedUntil = day + d.Rng.Range(1, Tuning.TxCongestionDaysMax);
News.Show("pd_news_congest_" + day, L.T("news.congest.title"), L.T("news.congest.desc"), d.CongestedUntil - day);
}
foreach (var p in d.PoisonPlans.Where(x => x.Day <= day + 1).ToList())
{
d.PoisonPlans.Remove(p);
var w = d.Own(p.Wallet);
if (w == null) continue;
string fake = ChainAddress.Lookalike(p.Target, d.Rng);
TxRules.Record(w.History, new TxRecord { Addr = fake, Asset = Assets.PSC, Amount = 0, Day = day + 1, In = true });
Log.Info($"地址投毒:{p.Target} 的相似地址 {fake} 向 {w.Address} 转入 0 币");
}
if (d.Rng.Chance(Tuning.TxPhishChance))
{
string fake = ChainAddress.Make(d.Rng.NextUInt());
if (Unlock.BlackUnlocked && d.Rng.Chance(0.5)) d.Black.Forum.Add("phish:" + fake);
else News.Show("pd_news_phish_" + day, L.T("news.phish.title"), L.T("news.phish.desc", fake), 2);
Log.Info("钓鱼公告:" + fake);
}
}
private static void Settle() private static void Settle()
{ {
var d = Session.Data; var d = Session.Data;
@@ -158,6 +185,7 @@ namespace Decentralized
Log.Info($"投毒钱包 {w.Address} 被转空:{v:0}"); Log.Info($"投毒钱包 {w.Address} 被转空:{v:0}");
ps.AddNightLog(L.T("nightlog.drained", WalletWindow.NameOf(w), Fmt.Credits(v)), "cc7755"); ps.AddNightLog(L.T("nightlog.drained", WalletWindow.NameOf(w), Fmt.Credits(v)), "cc7755");
} }
ChainNight(d, day);
foreach (var name in report.Burnt) ps.AddNightLog(L.T("nightlog.burnt", name), "cc7755"); foreach (var name in report.Burnt) ps.AddNightLog(L.T("nightlog.burnt", name), "cc7755");
int unpowered = report.Rigs - report.PoweredRigs; int unpowered = report.Rigs - report.PoweredRigs;
if (unpowered > 0 && units.Count + unpowered > 0) ps.AddNightLog(L.T("nightlog.unpowered", unpowered), "cc7755"); if (unpowered > 0 && units.Count + unpowered > 0) ps.AddNightLog(L.T("nightlog.unpowered", unpowered), "cc7755");
+11
View File
@@ -14,6 +14,13 @@ namespace Decentralized
public List<string> Burnt = new List<string>(); public List<string> Burnt = new List<string>();
} }
/// <summary>地址投毒的计划:某个钱包转给某地址后,到了这一天收到一笔来自相似地址的 0 币转入。</summary>
public sealed class PoisonPlan
{
public string Wallet, Target;
public int Day;
}
/// <summary>一局的 mod 状态。整体序列化成 JSON 存进游戏的 mod 存档数据;显卡的健康度等存在显卡物品的标签上。</summary> /// <summary>一局的 mod 状态。整体序列化成 JSON 存进游戏的 mod 存档数据;显卡的健康度等存在显卡物品的标签上。</summary>
public sealed class SaveData public sealed class SaveData
{ {
@@ -63,6 +70,10 @@ namespace Decentralized
if (!Addresses.TryGetValue(key, out var a) || !ChainAddress.Valid(a)) { a = ChainAddress.Make(Rng.NextUInt()); Addresses[key] = a; } if (!Addresses.TryGetValue(key, out var a) || !ChainAddress.Valid(a)) { a = ChainAddress.Make(Rng.NextUInt()); Addresses[key] = a; }
return a; return a;
} }
/// <summary>网络拥堵的起止天(含)。</summary>
public int CongestedFrom = -1, CongestedUntil = -1;
public List<PoisonPlan> PoisonPlans = new List<PoisonPlan>();
/// <summary>链上的区块高度:每签一笔交易往上走几块。</summary> /// <summary>链上的区块高度:每签一笔交易往上走几块。</summary>
public long Block = 1204331; public long Block = 1204331;
/// <summary>当天用币付款的成交额(信用点),夜间清零。</summary> /// <summary>当天用币付款的成交额(信用点),夜间清零。</summary>
+2 -2
View File
@@ -497,7 +497,7 @@ namespace Decentralized
b.AddHeader(L.T("black.forum.h")); b.AddHeader(L.T("black.forum.h"));
if (d.Black.Forum.Count == 0) b.AddLabel(L.T("black.forum.quiet")); if (d.Black.Forum.Count == 0) b.AddLabel(L.T("black.forum.quiet"));
foreach (var f in d.Black.Forum) foreach (var f in d.Black.Forum)
b.AddLabel(f.StartsWith("scam:") ? L.T("black.forum.scam", BlackDelivery.DisplayName(f.Substring(5))) : f.StartsWith("busted:") ? L.T("black.forum.busted", f.Substring(7)) : L.T("black.forum." + f)); b.AddLabel(f.StartsWith("phish:") ? L.T("black.forum.phish", f.Substring(6)) : f.StartsWith("scam:") ? L.T("black.forum.scam", BlackDelivery.DisplayName(f.Substring(5))) : f.StartsWith("busted:") ? L.T("black.forum.busted", f.Substring(7)) : L.T("black.forum." + f));
} }
private static void PlatformShelf(CustomUIBuilder b, SaveData d) private static void PlatformShelf(CustomUIBuilder b, SaveData d)
@@ -989,7 +989,7 @@ namespace Decentralized
double quote = buy ? v / p.Price : v * p.Price; double quote = buy ? v / p.Price : v * p.Price;
_message = L.T("hw.wait"); _message = L.T("hw.wait");
Open(); Open();
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) }); WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Asset = pay, Amount = v, 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, Wallet w) private static void DoTrade(Pool p, bool buy, double v, Wallet w)
+136 -16
View File
@@ -20,7 +20,7 @@ namespace Decentralized
public static class WalletWindow public static class WalletWindow
{ {
private const string IdPrefix = "pd_wallet_"; private const string IdPrefix = "pd_wallet_";
private const float WinW = 452f, WinH = 624f, ScreenW = 384f, ScreenH = 470f, Bezel = 6f; private const float WinW = 452f, WinH = 694f, ScreenW = 384f, ScreenH = 540f, Bezel = 6f;
private enum Screen { Home, Send, Review, Broadcast, Done } private enum Screen { Home, Send, Review, Broadcast, Done }
/// <summary>终端开出的账单:转账(有收款地址)或合约调用(DEX 兑换,没有地址)。付清后用付款的钱包执行。</summary> /// <summary>终端开出的账单:转账(有收款地址)或合约调用(DEX 兑换,没有地址)。付清后用付款的钱包执行。</summary>
@@ -43,6 +43,13 @@ namespace Decentralized
public long Block; public long Block;
public float Start; public float Start;
public bool Finished; public bool Finished;
/// <summary>手续费(币的数量)与扣费的币。</summary>
public double Fee;
public string FeeAsset;
/// <summary>各阶段结束的时刻(秒):签名、内存池、打包、每次确认;被挤出内存池时只有前两段再加一段等待。</summary>
public float[] Times;
public int Confs;
public bool Dropped;
} }
/// <summary>一台硬件钱包的屏幕状态。</summary> /// <summary>一台硬件钱包的屏幕状态。</summary>
@@ -51,6 +58,7 @@ namespace Decentralized
public string Address; public string Address;
public Screen Screen = Screen.Home; public Screen Screen = Screen.Home;
public string To = "", Amount = "", Asset = Assets.PSC, Note; public string To = "", Amount = "", Asset = Assets.PSC, Note;
public FeeTier Tier = FeeTier.Normal;
public Tx Tx; public Tx Tx;
public string Id => IdPrefix + Address; public string Id => IdPrefix + Address;
public Wallet Wallet => Session.Data.Own(Address); public Wallet Wallet => Session.Data.Own(Address);
@@ -59,8 +67,24 @@ namespace Decentralized
private static readonly Dictionary<string, Device> Devices = new Dictionary<string, Device>(); 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 string[] SendAssets = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA, Assets.Ponero };
// 出块动画各阶段的结束时刻(秒):签名、内存池、打包、三次确认 /// <summary>网络拥堵:夜间结算随机开始,持续一两天,期间手续费变贵、慢档和普通档确认变慢。</summary>
private static readonly float[] Phases = { 0.8f, 2.0f, 3.0f, 3.7f, 4.4f, 5.1f }; public static bool Congested
{
get { var d = Session.Data; int day = StoreStation.GetDayCounter(); return d != null && day >= d.CongestedFrom && day <= d.CongestedUntil; }
}
/// <summary>手续费折成币的数量。</summary>
private static double FeeIn(string asset, FeeTier tier) { double p = Session.Price(asset); return p > 0 ? TxRules.FeeCredits(tier, Congested) / p : 0; }
/// <summary>出块动画的时间表:签名 0.8 秒,内存池 1.2 秒,打包 1 秒,每次确认 0.6 秒;拥堵时按档位放慢。</summary>
private static float[] Timeline(int confs, double slow, bool dropped)
{
var t = new List<float> { 0.8f, 0.8f + 1.2f * (float)slow };
if (dropped) { t.Add(t[1] + 1.5f * (float)slow); return t.ToArray(); }
t.Add(t[1] + 1.0f);
for (int i = 0; i < confs; i++) t.Add(t[t.Count - 1] + 0.6f * (float)slow);
return t.ToArray();
}
private static readonly Color Ink = new Color(0.12f, 0.12f, 0.14f), private static readonly Color Ink = new Color(0.12f, 0.12f, 0.14f),
Key = new Color(0.16f, 0.16f, 0.19f), KeyText = new Color(0.93f, 0.93f, 0.90f), Key = new Color(0.16f, 0.16f, 0.19f), KeyText = new Color(0.93f, 0.93f, 0.90f),
@@ -124,7 +148,7 @@ namespace Decentralized
WalletDriver.Ensure(); WalletDriver.Ensure();
var b = CustomUIManager.Window(dev.Id, "PLEDGER"); var b = CustomUIManager.Window(dev.Id, "PLEDGER");
if (b == null) return; if (b == null) return;
_rows.Clear(); _clear.Clear(); _rows.Clear(); _clear.Clear(); _picked.Clear();
int idx = Session.Data.Wallets.IndexOf(dev.Wallet); int idx = Session.Data.Wallets.IndexOf(dev.Wallet);
b.SetSize(WinW, WinH).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle(); b.SetSize(WinW, WinH).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
b.SetPosition(new Vector2(600f + 28f * idx, -24f * idx)); b.SetPosition(new Vector2(600f + 28f * idx, -24f * idx));
@@ -188,8 +212,9 @@ namespace Decentralized
foreach (var btn in screen.GetComponentsInChildren<Button>(true)) foreach (var btn in screen.GetComponentsInChildren<Button>(true))
{ {
var img = btn.GetComponent<Image>(); var img = btn.GetComponent<Image>();
if (img != null) img.color = Key; bool on = _picked.Contains(btn.gameObject);
foreach (var t in btn.GetComponentsInChildren<TextMeshProUGUI>(true)) t.color = KeyText; if (img != null) { if (on) Slice(img, "ui_hw_sel", 5); else img.color = Key; }
foreach (var t in btn.GetComponentsInChildren<TextMeshProUGUI>(true)) { t.color = on ? Ink : KeyText; if (on) t.fontStyle |= FontStyles.Bold; }
} }
foreach (var img in _clear) if (img != null) img.color = new Color(0, 0, 0, 0); foreach (var img in _clear) if (img != null) img.color = new Color(0, 0, 0, 0);
// 余额行:整行可点,选中的一行反色高亮 // 余额行:整行可点,选中的一行反色高亮
@@ -231,6 +256,14 @@ namespace Decentralized
private const float RowW = ScreenW - 28f; 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<(GameObject root, Image image, string asset)> _rows = new List<(GameObject, Image, string)>();
private static readonly List<Image> _clear = new List<Image>(); private static readonly List<Image> _clear = new List<Image>();
private static readonly List<GameObject> _picked = new List<GameObject>();
/// <summary>屏上的选择按钮(币种、手续费档位):选中的一个画成白底黑框黑字、四角带准星角标。</summary>
private static void Pick(CustomUIBuilder b, string text, bool selected, Action onClick)
{
b.AddButton(text, onClick);
if (selected) _picked.Add(b.Last.root);
}
public static string Amt(double v) => v.ToString(v >= 100 ? "0.##" : "0.####", CultureInfo.InvariantCulture); public static string Amt(double v) => v.ToString(v >= 100 ? "0.##" : "0.####", CultureInfo.InvariantCulture);
@@ -274,6 +307,7 @@ namespace Decentralized
b.AddLabel(L.T("hw.total", Fmt.Credits(w.Value(Session.Price)))); 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))); if (w.Taint > 0.5) b.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
if (Pending != null) b.AddLabel(Pending.Contract ? L.T("hw.pending.sign", Pending.Summary) : L.T("hw.pending.bill", Pending.Summary)); if (Pending != null) b.AddLabel(Pending.Contract ? L.T("hw.pending.sign", Pending.Summary) : L.T("hw.pending.bill", Pending.Summary));
if (Congested) b.AddLabel(L.T("hw.congested"), "congest_note");
b.AddSpace(6f); b.AddSpace(6f);
b.BeginRow(); b.BeginRow();
b.AddButton(L.T("hw.send"), () => Go(dev, Screen.Send)); b.AddButton(L.T("hw.send"), () => Go(dev, Screen.Send));
@@ -291,13 +325,19 @@ namespace Decentralized
_clear.Add(b.Last.image); _clear.Add(b.Last.image);
b.AddInput("0x", dev.To, s => dev.To = s, "to", s => dev.To = s); b.AddInput("0x", dev.To, s => dev.To = s, "to", s => dev.To = s);
b.End(); b.End();
b.AddButton(L.T("hw.paste"), () => { dev.To = Clipboard.Paste(); dev.Note = null; Open(dev); }); b.AddButton(L.T("hw.paste"), () =>
{
string clip = Clipboard.Paste();
// 被投毒的钱包:固件里的木马有时把粘贴的地址换成长得很像的骗子地址
if (w.Poisoned && ChainAddress.Valid(clip) && Session.Data.Rng.Chance(Tuning.TxHijackChance)) { clip = ChainAddress.Lookalike(clip, Session.Data.Rng); Log.Info($"剪贴板木马:{Clipboard.Paste()} → {clip}"); }
dev.To = clip; dev.Note = null; Open(dev);
});
b.End(); b.End();
b.BeginRow(); b.BeginRow();
foreach (var a in SendAssets) foreach (var a in SendAssets)
{ {
var asset = a; var asset = a;
b.AddButton(asset == dev.Asset ? $"[{asset}]" : asset, () => { dev.Asset = asset; Open(dev); }); Pick(b, asset, asset == dev.Asset, () => { dev.Asset = asset; Open(dev); });
} }
b.End(); b.End();
b.AddLabel(L.T("hw.avail", dev.Asset, Amt(w.Avail(dev.Asset))), "avail_dim"); b.AddLabel(L.T("hw.avail", dev.Asset, Amt(w.Avail(dev.Asset))), "avail_dim");
@@ -308,6 +348,17 @@ namespace Decentralized
b.AddLabel(L.T("hw.bill", Pending.Summary, Amt(Pending.Amount), Pending.Asset), "bill_dim"); b.AddLabel(L.T("hw.bill", Pending.Summary, Amt(Pending.Amount), Pending.Asset), "bill_dim");
b.AddButton(L.T("hw.fill"), () => { dev.Asset = Pending.Asset; dev.Amount = Amt(Pending.Amount); Open(dev); }); b.AddButton(L.T("hw.fill"), () => { dev.Asset = Pending.Asset; dev.Amount = Amt(Pending.Amount); Open(dev); });
} }
// 最近转账:点一下填入地址(地址投毒的 0 币转入也混在这里,缩写看起来一样)
var recent = w.History.GroupBy(r => r.Addr + (r.In ? "<" : ">")).Select(g => g.First()).Take(4).ToList();
if (recent.Count > 0)
{
b.AddLabel(L.T("hw.recent"), "recent_dim");
foreach (var r in recent)
{
var rec = r;
b.AddButton(L.T(rec.In ? "hw.recent.in" : "hw.recent.out", ChainAddress.Short(rec.Addr), Amt(rec.Amount), rec.Asset), () => { dev.To = rec.Addr; dev.Note = null; Open(dev); });
}
}
b.AddLabel(L.T("hw.next"), "hint_dim"); b.AddLabel(L.T("hw.next"), "hint_dim");
} }
@@ -326,6 +377,7 @@ namespace Decentralized
if (ChainAddress.Same(to, w.Address)) return L.T("hw.err.self"); if (ChainAddress.Same(to, w.Address)) return L.T("hw.err.self");
if (!ReadAmount(dev, out double v)) return L.T("msg.amount"); 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"); if (w.Avail(dev.Asset) + 1e-9 < v) return L.T(w.Frozen.ContainsKey(dev.Asset) ? "msg.frozen" : "msg.balance");
if (w.Avail(dev.Asset) + 1e-9 < v + FeeIn(dev.Asset, dev.Tier)) return L.T("hw.err.fee");
var bill = Match(to); var bill = Match(to);
if (bill != null) if (bill != null)
{ {
@@ -366,9 +418,25 @@ namespace Decentralized
if (bill != null) b.AddLabel(L.T("hw.review.bill", bill.Summary)); 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 (own != null) b.AddLabel(L.T("hw.review.own", NameOf(own)));
else if (IsPoinbase(to)) b.AddLabel(L.T("hw.review.poinbase")); else if (IsPoinbase(to)) b.AddLabel(L.T("hw.review.poinbase"));
else if (dev.Wallet.History.Any(r => ChainAddress.Same(r.Addr, to))) b.AddLabel(L.T("hw.review.history"));
else b.AddLabel(L.T("hw.review.unknown"), "warn_note"); 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), dev.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");
} }
// 手续费档位、确认数、拥堵
string feeAsset = FeeAssetOf(dev);
b.AddSeparator();
b.BeginRow();
foreach (FeeTier t in Enum.GetValues(typeof(FeeTier)))
{
var tier = t;
string n = L.T("hw.fee." + t.ToString().ToLowerInvariant());
Pick(b, n, tier == dev.Tier, () => { dev.Tier = tier; dev.Note = null; Open(dev); });
}
b.End();
double fee = FeeIn(feeAsset, dev.Tier);
b.AddLabel(L.T("hw.fee", Amt(fee), feeAsset, Fmt.Num(TxRules.FeeCredits(dev.Tier, Congested))));
b.AddLabel(L.T("hw.confs", TxRules.Confirmations(ValueOf(dev))));
if (Congested) b.AddLabel(dev.Tier == FeeTier.Slow ? L.T("hw.congested.slow") : L.T("hw.congested"), "congest_note");
b.AddSpace(8f); b.AddSpace(8f);
b.AddLabel(L.T("hw.hold"), "hold_dim"); b.AddLabel(L.T("hw.hold"), "hold_dim");
} }
@@ -385,6 +453,15 @@ namespace Decentralized
private static void DoneScreen(CustomUIBuilder b, Device dev) private static void DoneScreen(CustomUIBuilder b, Device dev)
{ {
var tx = dev.Tx; var tx = dev.Tx;
if (tx != null && tx.Dropped)
{
b.AddHeader(L.T("hw.dropped.h"));
b.AddLabel(L.T("hw.tx.dropped"));
b.AddLabel(L.T("hw.fee", Amt(tx.Fee), tx.FeeAsset, Fmt.Num(tx.Fee * Session.Price(tx.FeeAsset))));
b.AddSpace(8f);
b.AddButton(L.T("hw.back"), () => { dev.Tx = null; Go(dev, Screen.Home); });
return;
}
b.AddHeader(L.T("hw.done.h")); b.AddHeader(L.T("hw.done.h"));
if (tx != null) if (tx != null)
{ {
@@ -401,17 +478,32 @@ namespace Decentralized
} }
private static float Elapsed(Device dev) => dev.Tx == null ? 0 : Time.unscaledTime - dev.Tx.Start; 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 float Total(Device dev) => dev.Tx?.Times == null ? 0 : dev.Tx.Times[dev.Tx.Times.Length - 1];
private static float Progress(Device dev) => Total(dev) <= 0 ? 0 : Mathf.Clamp01(Elapsed(dev) / Total(dev));
/// <summary>扣手续费的币:转账用转出的币,合约调用用账单上的币。</summary>
private static string FeeAssetOf(Device dev) => Pending != null && Pending.Contract ? (Pending.Asset ?? Assets.PSC) : dev.Asset;
/// <summary>这笔交易的价值(信用点),决定确认数。</summary>
private static double ValueOf(Device dev)
{
if (Pending != null && Pending.Contract) return Pending.Amount * Session.Price(Pending.Asset ?? Assets.PSC);
ReadAmount(dev, out double v);
return v * Session.Price(dev.Asset);
}
private static string StatusText(Device dev) private static string StatusText(Device dev)
{ {
float t = Elapsed(dev); float t = Elapsed(dev);
if (dev.Tx == null) return ""; var tx = dev.Tx;
if (t < Phases[0]) return L.T("hw.tx.signing"); if (tx?.Times == null) return "";
if (t < Phases[1]) return L.T("hw.tx.mempool"); if (t < tx.Times[0]) return L.T("hw.tx.signing");
if (t < Phases[2]) return L.T("hw.tx.packed", dev.Tx.Block); if (t < tx.Times[1]) return L.T("hw.tx.mempool");
int conf = t < Phases[3] ? 1 : t < Phases[4] ? 2 : 3; if (tx.Dropped) return L.T("hw.tx.stuck");
return L.T("hw.tx.confirm", dev.Tx.Block, conf, 3); if (t < tx.Times[2]) return L.T("hw.tx.packed", tx.Block);
int conf = 1;
while (conf < tx.Confs && t >= tx.Times[2 + conf]) conf++;
return L.T("hw.tx.confirm", tx.Block, conf, tx.Confs);
} }
/// <summary>左键 ×:拒绝签名 / 返回上一页。</summary> /// <summary>左键 ×:拒绝签名 / 返回上一页。</summary>
@@ -455,7 +547,11 @@ namespace Decentralized
var d = Session.Data; var d = Session.Data;
d.Block += 1 + d.Rng.Range(0, 3); d.Block += 1 + d.Rng.Range(0, 3);
if (Pending != null && Pending.Contract) if (Pending != null && Pending.Contract)
{
string a = Pending.Asset ?? Assets.PSC;
if (dev.Wallet.Avail(a) + 1e-9 < Pending.Amount + FeeIn(a, dev.Tier)) { dev.Note = L.T("hw.err.fee"); Open(dev); return; }
dev.Tx = new Tx { Bill = Pending, Block = d.Block, Start = Time.unscaledTime }; dev.Tx = new Tx { Bill = Pending, Block = d.Block, Start = Time.unscaledTime };
}
else else
{ {
string err = CheckSend(dev); string err = CheckSend(dev);
@@ -464,6 +560,12 @@ namespace Decentralized
string to = ChainAddress.Normalize(dev.To); 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 }; dev.Tx = new Tx { To = to, Asset = dev.Asset, Amount = v, Bill = Match(to), Block = d.Block, Start = Time.unscaledTime };
} }
bool cong = Congested;
dev.Tx.FeeAsset = FeeAssetOf(dev);
dev.Tx.Fee = FeeIn(dev.Tx.FeeAsset, dev.Tier);
dev.Tx.Confs = TxRules.Confirmations(ValueOf(dev));
dev.Tx.Dropped = d.Rng.Chance(TxRules.DropChance(dev.Tier, cong));
dev.Tx.Times = Timeline(dev.Tx.Confs, TxRules.Slowdown(dev.Tier, cong), dev.Tx.Dropped);
if (dev.Tx.Bill != null) Pending = null; if (dev.Tx.Bill != null) Pending = null;
dev.Screen = Screen.Broadcast; dev.Screen = Screen.Broadcast;
dev.Note = null; dev.Note = null;
@@ -481,7 +583,25 @@ namespace Decentralized
Log.Guard("钱包交易落地", () => Log.Guard("钱包交易落地", () =>
{ {
if (w == null) return; if (w == null) return;
// 手续费先扣(被挤出内存池也照扣)
if (tx.Fee > 0 && tx.FeeAsset != null) Wallet.Transfer(w, new Wallet(), tx.FeeAsset, Math.Min(tx.Fee, w.Avail(tx.FeeAsset)), Session.Price);
if (tx.Dropped)
{
// 没成交:账单还在,可以重新付
if (tx.Bill != null && Pending == null) Pending = tx.Bill;
TerminalWindow.Refresh(L.T("hw.tx.dropped"));
return;
}
int day = StoreStation.GetDayCounter();
double amt = tx.To != null ? Math.Min(tx.Amount, w.Avail(tx.Asset)) : 0; double amt = tx.To != null ? Math.Min(tx.Amount, w.Avail(tx.Asset)) : 0;
if (tx.To != null)
{
TxRules.Record(w.History, new TxRecord { Addr = tx.To, Asset = tx.Asset, Amount = amt, Day = day });
var mine = d.Own(tx.To);
if (mine != null) TxRules.Record(mine.History, new TxRecord { Addr = w.Address, Asset = tx.Asset, Amount = amt, Day = day, In = true });
// 地址投毒:链上公开的转账被人盯上,过一两天会收到一笔来自相似地址的 0 币转入
else if (d.Rng.Chance(Tuning.TxPoisonChance)) d.PoisonPlans.Add(new PoisonPlan { Wallet = w.Address, Target = tx.To, Day = day + d.Rng.Range(1, Tuning.TxPoisonDelayMax) });
}
if (tx.Bill != null && !tx.Bill.Contract) if (tx.Bill != null && !tx.Bill.Contract)
{ {
// 多付的部分对方不退 // 多付的部分对方不退
@@ -516,7 +636,7 @@ namespace Decentralized
foreach (var dev in Devices.Values.ToList()) foreach (var dev in Devices.Values.ToList())
{ {
if (dev.Tx == null || dev.Tx.Finished || dev.Screen != Screen.Broadcast) continue; if (dev.Tx == null || dev.Tx.Finished || dev.Screen != Screen.Broadcast) continue;
if (Elapsed(dev) >= Phases[Phases.Length - 1]) { Finish(dev); continue; } if (Elapsed(dev) >= Total(dev)) { Finish(dev); continue; }
if (!IsOpen(dev)) continue; if (!IsOpen(dev)) continue;
var m = CustomUIManager.Instance; var m = CustomUIManager.Instance;
m.GetElement(dev.Id, "status")?.SetText(StatusText(dev)); m.GetElement(dev.Id, "status")?.SetText(StatusText(dev));
@@ -808,6 +808,27 @@ internal static class Program
Check(!ChainAddress.Valid("0x12345") && !ChainAddress.Valid("0xzzzzzzzz") && !ChainAddress.WellFormed("12345678ab"), "格式不对的地址不合法"); Check(!ChainAddress.Valid("0x12345") && !ChainAddress.Valid("0xzzzzzzzz") && !ChainAddress.WellFormed("12345678ab"), "格式不对的地址不合法");
}); });
Test("相似地址、手续费、确认数", () =>
{
var rng = new Rng(21);
int sameShort = 0;
for (int i = 0; i < 300; i++)
{
string a = ChainAddress.Make(rng.NextUInt());
string f = ChainAddress.Lookalike(a, rng);
Check(ChainAddress.Valid(f) && !ChainAddress.Same(a, f) && f.Substring(0, 5) == a.Substring(0, 5), $"相似地址合法、不同、前缀相同:{a} / {f}");
if (ChainAddress.Short(a) == ChainAddress.Short(f)) sameShort++;
}
Check(sameShort > 290, $"缩写看起来一样:{sameShort}/300");
Check(TxRules.FeeCredits(FeeTier.Slow, false) < TxRules.FeeCredits(FeeTier.Normal, false) && TxRules.FeeCredits(FeeTier.Normal, false) < TxRules.FeeCredits(FeeTier.Fast, false), "慢 < 普通 < 快");
Check(Math.Abs(TxRules.FeeCredits(FeeTier.Fast, true) - Tuning.TxFeeFast * Tuning.TxCongestedFeeMult) < 1e-9, "拥堵时手续费翻倍");
Check(TxRules.Confirmations(100) == 3 && TxRules.Confirmations(5000) == 6 && TxRules.Confirmations(50000) == 12, "大额要更多确认");
Check(TxRules.DropChance(FeeTier.Slow, true) > 0 && TxRules.DropChance(FeeTier.Slow, false) == 0 && TxRules.DropChance(FeeTier.Fast, true) == 0, "只有拥堵时的慢档会被丢");
var h = new List<TxRecord>();
for (int i = 0; i < 30; i++) TxRules.Record(h, new TxRecord { Day = i });
Check(h.Count == TxRules.HistoryMax && h[0].Day == 29, "记录最新在前、有上限");
});
Test("被投毒的钱包:门槛以下不转,越多越快,转空", () => Test("被投毒的钱包:门槛以下不转,越多越快,转空", () =>
{ {
Check(Wallet.DrainChance(Tuning.WalletDrainMin - 1) == 0, "门槛以下不会被转走"); Check(Wallet.DrainChance(Tuning.WalletDrainMin - 1) == 0, "门槛以下不会被转走");