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:
1 parent
2b781acd82
commit
dbba8b4376
15 files changed
+357
-20
No files matched your search
@@ -18,6 +18,13 @@
|
||||
- PLEDGER 主页余额改成三列(币种 | 数量 | 估值)右对齐,五种币全部显示(含 0,冻结的另注);点一行选中,整行反色高亮,发送时默认用选中的币。
|
||||
- 钱包页:每个钱包可以改名(留空恢复默认,最长 16 字),名字在终端和 PLEDGER 上都用;非主钱包可以销毁(点两次确认,钱包里还有币时不让销毁)。钱包编号固定,销毁别的钱包后名字不会错位。
|
||||
- 游戏内验证(这一轮):复制钱包 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 开始。
|
||||
- 游戏内验证(多钱包):两件硬件钱包(一正一投毒)作用于终端,钱包页出现钱包 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 空账户和"账户污点比例"两处改字(已编译,没重进游戏看)。
|
||||
|
||||
|
||||
@@ -681,10 +681,23 @@ def rounded(size, r, fill, light=None, dark=None):
|
||||
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_hw_shell'] = lambda: rounded(32, 7, 'K', 'E', 'z')
|
||||
UI_ICONS['ui_hw_bezel'] = lambda: rounded(16, 4, 'n')
|
||||
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_no'] = lambda: hw_button('no')
|
||||
|
||||
|
||||
@@ -41,5 +41,31 @@ namespace Decentralized.Domain
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -142,6 +142,18 @@ namespace Decentralized.Domain
|
||||
/// <summary>被投毒的钱包:币的总价值达到 DrainMin 起,每晚有概率被转空;价值越高越快:期望天数从门槛处的 SlowDays(7 天)按价值线性降到 DrainFull 处的 1 天,每晚概率 = 1 / 期望天数。</summary>
|
||||
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>
|
||||
public static double PoolDepth(int day) => PoolDepth0 * (1 + day / 60.0);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,9 @@ namespace Decentralized.Domain
|
||||
public string Name;
|
||||
/// <summary>玩家起的名字;空时用默认名。</summary>
|
||||
public string Label;
|
||||
/// <summary>转账记录(最新的在前),PLEDGER 发送页的"最近"列表从这里取。</summary>
|
||||
public List<TxRecord> History = new List<TxRecord>();
|
||||
|
||||
/// <summary>硬件钱包被动过手脚(供应链投毒):别人手里有助记词。玩家看不到。</summary>
|
||||
public bool Poisoned;
|
||||
|
||||
|
||||
@@ -69,6 +69,19 @@ namespace Decentralized
|
||||
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, "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)", () =>
|
||||
{
|
||||
EmporiumEntry.Instance.ClearFrontInv();
|
||||
|
||||
@@ -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. 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.odd2,"(His hand stays in his pocket.) Sign here."
|
||||
courier.sign,Sign
|
||||
@@ -427,3 +427,23 @@ hw.balance.value,"≈ {0}"
|
||||
wallets.rename,Rename
|
||||
wallets.rename.h,"New name for {0}"
|
||||
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."
|
||||
|
@@ -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,签收
|
||||
@@ -426,3 +426,23 @@ hw.balance.value,"≈ {0}"
|
||||
wallets.rename,改名
|
||||
wallets.rename.h,"{0} 新名字"
|
||||
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},说旧的被盯上了。"
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 112 B |
@@ -77,6 +77,33 @@ namespace Decentralized
|
||||
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()
|
||||
{
|
||||
var d = Session.Data;
|
||||
@@ -158,6 +185,7 @@ namespace Decentralized
|
||||
Log.Info($"投毒钱包 {w.Address} 被转空:{v:0}");
|
||||
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");
|
||||
int unpowered = report.Rigs - report.PoweredRigs;
|
||||
if (unpowered > 0 && units.Count + unpowered > 0) ps.AddNightLog(L.T("nightlog.unpowered", unpowered), "cc7755");
|
||||
|
||||
@@ -14,6 +14,13 @@ namespace Decentralized
|
||||
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>
|
||||
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; }
|
||||
return a;
|
||||
}
|
||||
/// <summary>网络拥堵的起止天(含)。</summary>
|
||||
public int CongestedFrom = -1, CongestedUntil = -1;
|
||||
public List<PoisonPlan> PoisonPlans = new List<PoisonPlan>();
|
||||
|
||||
/// <summary>链上的区块高度:每签一笔交易往上走几块。</summary>
|
||||
public long Block = 1204331;
|
||||
/// <summary>当天用币付款的成交额(信用点),夜间清零。</summary>
|
||||
|
||||
@@ -497,7 +497,7 @@ namespace Decentralized
|
||||
b.AddHeader(L.T("black.forum.h"));
|
||||
if (d.Black.Forum.Count == 0) b.AddLabel(L.T("black.forum.quiet"));
|
||||
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)
|
||||
@@ -989,7 +989,7 @@ namespace Decentralized
|
||||
double quote = buy ? v / p.Price : v * p.Price;
|
||||
_message = L.T("hw.wait");
|
||||
Open();
|
||||
WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Summary = L.T("hw.swap", Fmt.Num(v), pay, Fmt.Num(quote), get), Execute = 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)
|
||||
|
||||
@@ -20,7 +20,7 @@ namespace Decentralized
|
||||
public static class WalletWindow
|
||||
{
|
||||
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 }
|
||||
|
||||
/// <summary>终端开出的账单:转账(有收款地址)或合约调用(DEX 兑换,没有地址)。付清后用付款的钱包执行。</summary>
|
||||
@@ -43,6 +43,13 @@ namespace Decentralized
|
||||
public long Block;
|
||||
public float Start;
|
||||
public bool Finished;
|
||||
/// <summary>手续费(币的数量)与扣费的币。</summary>
|
||||
public double Fee;
|
||||
public string FeeAsset;
|
||||
/// <summary>各阶段结束的时刻(秒):签名、内存池、打包、每次确认;被挤出内存池时只有前两段再加一段等待。</summary>
|
||||
public float[] Times;
|
||||
public int Confs;
|
||||
public bool Dropped;
|
||||
}
|
||||
|
||||
/// <summary>一台硬件钱包的屏幕状态。</summary>
|
||||
@@ -51,6 +58,7 @@ namespace Decentralized
|
||||
public string Address;
|
||||
public Screen Screen = Screen.Home;
|
||||
public string To = "", Amount = "", Asset = Assets.PSC, Note;
|
||||
public FeeTier Tier = FeeTier.Normal;
|
||||
public Tx Tx;
|
||||
public string Id => IdPrefix + 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 string[] SendAssets = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA, Assets.Ponero };
|
||||
|
||||
// 出块动画各阶段的结束时刻(秒):签名、内存池、打包、三次确认
|
||||
private static readonly float[] Phases = { 0.8f, 2.0f, 3.0f, 3.7f, 4.4f, 5.1f };
|
||||
/// <summary>网络拥堵:夜间结算随机开始,持续一两天,期间手续费变贵、慢档和普通档确认变慢。</summary>
|
||||
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),
|
||||
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();
|
||||
var b = CustomUIManager.Window(dev.Id, "PLEDGER");
|
||||
if (b == null) return;
|
||||
_rows.Clear(); _clear.Clear();
|
||||
_rows.Clear(); _clear.Clear(); _picked.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));
|
||||
@@ -188,8 +212,9 @@ namespace Decentralized
|
||||
foreach (var btn in screen.GetComponentsInChildren<Button>(true))
|
||||
{
|
||||
var img = btn.GetComponent<Image>();
|
||||
if (img != null) img.color = Key;
|
||||
foreach (var t in btn.GetComponentsInChildren<TextMeshProUGUI>(true)) t.color = KeyText;
|
||||
bool on = _picked.Contains(btn.gameObject);
|
||||
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);
|
||||
// 余额行:整行可点,选中的一行反色高亮
|
||||
@@ -231,6 +256,14 @@ namespace Decentralized
|
||||
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>();
|
||||
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);
|
||||
|
||||
@@ -274,6 +307,7 @@ namespace Decentralized
|
||||
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 (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.BeginRow();
|
||||
b.AddButton(L.T("hw.send"), () => Go(dev, Screen.Send));
|
||||
@@ -291,13 +325,19 @@ namespace Decentralized
|
||||
_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.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.BeginRow();
|
||||
foreach (var a in SendAssets)
|
||||
{
|
||||
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.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.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");
|
||||
}
|
||||
|
||||
@@ -326,6 +377,7 @@ namespace Decentralized
|
||||
if (ChainAddress.Same(to, w.Address)) return L.T("hw.err.self");
|
||||
if (!ReadAmount(dev, out double v)) return L.T("msg.amount");
|
||||
if (w.Avail(dev.Asset) + 1e-9 < v) return L.T(w.Frozen.ContainsKey(dev.Asset) ? "msg.frozen" : "msg.balance");
|
||||
if (w.Avail(dev.Asset) + 1e-9 < v + FeeIn(dev.Asset, dev.Tier)) return L.T("hw.err.fee");
|
||||
var bill = Match(to);
|
||||
if (bill != null)
|
||||
{
|
||||
@@ -366,9 +418,25 @@ namespace Decentralized
|
||||
if (bill != null) b.AddLabel(L.T("hw.review.bill", bill.Summary));
|
||||
else if (own != null) b.AddLabel(L.T("hw.review.own", NameOf(own)));
|
||||
else if (IsPoinbase(to)) b.AddLabel(L.T("hw.review.poinbase"));
|
||||
else 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");
|
||||
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.AddLabel(L.T("hw.hold"), "hold_dim");
|
||||
}
|
||||
@@ -385,6 +453,15 @@ namespace Decentralized
|
||||
private static void DoneScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
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"));
|
||||
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 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)
|
||||
{
|
||||
float t = Elapsed(dev);
|
||||
if (dev.Tx == null) return "";
|
||||
if (t < Phases[0]) return L.T("hw.tx.signing");
|
||||
if (t < Phases[1]) return L.T("hw.tx.mempool");
|
||||
if (t < Phases[2]) return L.T("hw.tx.packed", dev.Tx.Block);
|
||||
int conf = t < Phases[3] ? 1 : t < Phases[4] ? 2 : 3;
|
||||
return L.T("hw.tx.confirm", dev.Tx.Block, conf, 3);
|
||||
var tx = dev.Tx;
|
||||
if (tx?.Times == null) return "";
|
||||
if (t < tx.Times[0]) return L.T("hw.tx.signing");
|
||||
if (t < tx.Times[1]) return L.T("hw.tx.mempool");
|
||||
if (tx.Dropped) return L.T("hw.tx.stuck");
|
||||
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>
|
||||
@@ -455,7 +547,11 @@ namespace Decentralized
|
||||
var d = Session.Data;
|
||||
d.Block += 1 + d.Rng.Range(0, 3);
|
||||
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 };
|
||||
}
|
||||
else
|
||||
{
|
||||
string err = CheckSend(dev);
|
||||
@@ -464,6 +560,12 @@ namespace Decentralized
|
||||
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 };
|
||||
}
|
||||
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;
|
||||
dev.Screen = Screen.Broadcast;
|
||||
dev.Note = null;
|
||||
@@ -481,7 +583,25 @@ namespace Decentralized
|
||||
Log.Guard("钱包交易落地", () =>
|
||||
{
|
||||
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;
|
||||
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)
|
||||
{
|
||||
// 多付的部分对方不退
|
||||
@@ -516,7 +636,7 @@ namespace Decentralized
|
||||
foreach (var dev in Devices.Values.ToList())
|
||||
{
|
||||
if (dev.Tx == null || dev.Tx.Finished || dev.Screen != Screen.Broadcast) continue;
|
||||
if (Elapsed(dev) >= Phases[Phases.Length - 1]) { Finish(dev); continue; }
|
||||
if (Elapsed(dev) >= Total(dev)) { Finish(dev); continue; }
|
||||
if (!IsOpen(dev)) continue;
|
||||
var m = CustomUIManager.Instance;
|
||||
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"), "格式不对的地址不合法");
|
||||
});
|
||||
|
||||
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("被投毒的钱包:门槛以下不转,越多越快,转空", () =>
|
||||
{
|
||||
Check(Wallet.DrainChance(Tuning.WalletDrainMin - 1) == 0, "门槛以下不会被转走");
|
||||
|
||||
Reference in new issue
Block a user