Multiple hardware wallets: per-wallet devices, wallets page, Poinbase custodial account, PLEDGER item with supply-chain poisoning
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
add61a6b5e
commit
783f73989d
19 files changed
+555
-197
No files matched your search
@@ -10,6 +10,11 @@
|
||||
- DEX 兑换改成合约调用:在终端点买入 / 卖出后钱包弹出签名请求,按住 ✓ 签名后才成交,× 拒绝。Poinbase 是托管账户,仍直接成交。
|
||||
- 签名后的动画:签名中 → 已广播(内存池)→ 打包进区块 #N → 确认 1/3、2/3、3/3,约 5 秒,可跳过;窗口关着也会走完。区块高度存档。
|
||||
- 游戏内验证:DEX 10 CRT 兑 PSC(签名 → 确认 → CRT 4000 → 3990);黑市现货手枪:输错末位被校验位拦下,改对后付 156 CRT,库存 3 → 2,货进储藏区,污点按比例减少;向 0x12345699 转 1 CRT,提示转不回来,余额减 1;Ponero 兑换 10 PSC 得 18.8 Ponero,当天额度用完。没有在游戏里验证:混币器账单(测试存档没解锁混币器)、商家订单、上架 / 改价费账单(代码路径和现货相同)。
|
||||
- 多钱包:每个硬件钱包的余额、污点、成本、冻结各算各的。终端新增"钱包"页:列出全部钱包(名称、地址、估值、污点),"打开"弹出这个钱包自己的 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:币被转走了"。
|
||||
- 存档不兼容旧版:链上的币归到新建的主钱包,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 空账户和"账户污点比例"两处改字(已编译,没重进游戏看)。
|
||||
|
||||
### 第四阶段:Web3 黑市与 Ponero(进行中)
|
||||
|
||||
|
||||
@@ -607,6 +607,21 @@ def guide():
|
||||
return c
|
||||
|
||||
|
||||
def hw_wallet():
|
||||
"""PLEDGER 硬件钱包(16×16,一格):竖放的石墨卡片,大块浅色墨水屏,右侧一个侧键。"""
|
||||
c = Canvas(16, 16)
|
||||
c.rect(3, 1, 12, 14, 'K')
|
||||
for (x, y) in ((3, 1), (12, 1), (3, 14), (12, 14)):
|
||||
c.put(x, y, None)
|
||||
c.rect(4, 2, 4, 13, 'E'); c.rect(4, 2, 11, 2, 'E') # 左上受光
|
||||
c.rect(11, 3, 11, 13, 'z'); c.rect(5, 13, 11, 13, 'z') # 右下背光
|
||||
c.rect(5, 3, 10, 11, 'P') # 墨水屏
|
||||
c.rect(6, 5, 9, 5, 'm'); c.rect(6, 7, 8, 7, 'm'); c.rect(6, 9, 9, 9, 'm') # 屏上的字
|
||||
c.rect(13, 5, 13, 7, 'x') # 侧键
|
||||
outline(c)
|
||||
return c
|
||||
|
||||
|
||||
SPRITES = {
|
||||
'gpu_guide': guide,
|
||||
'gpu_burnt_f': lambda: gpu_front('e', 'c', burnt=True),
|
||||
@@ -615,6 +630,7 @@ SPRITES = {
|
||||
'gpu_pmd_b': lambda: gpu_back('R', 'r2'),
|
||||
'rig': rig,
|
||||
'terminal': terminal,
|
||||
'hw_wallet': hw_wallet,
|
||||
}
|
||||
for _tier in range(4):
|
||||
SPRITES['gpu_probidia_f_t%d' % _tier] = (lambda t=_tier: gpu_front('G', 'g2', tier=t))
|
||||
|
||||
@@ -44,6 +44,8 @@ namespace Decentralized.Domain
|
||||
public int Price;
|
||||
public string Asset;
|
||||
public double Qty;
|
||||
/// <summary>付款的钱包地址:退款回这里。</summary>
|
||||
public string Wallet;
|
||||
/// <summary>付款时留在卖家那边的污点与成本:退款时一并还回,免得"付脏币再退款"洗钱。</summary>
|
||||
public double Taint, Basis;
|
||||
public int PlacedDay, DueDay;
|
||||
@@ -232,7 +234,7 @@ namespace Decentralized.Domain
|
||||
var o = new BmOrder
|
||||
{
|
||||
Id = NextOrderId++, Item = l.Item, Seller = seller.Name, SellerScam = seller.Scam, Mode = mode, Count = count, Model = l.Model, FakeOf = l.FakeOf, Health = l.Health, Price = credits, Asset = asset, Qty = qty,
|
||||
Taint = taint, Basis = basis, PlacedDay = day, DueDay = day + seller.Days,
|
||||
Taint = taint, Basis = basis, PlacedDay = day, DueDay = day + seller.Days, Wallet = w.Address,
|
||||
};
|
||||
l.Qty -= count;
|
||||
if (l.Qty <= 0) Catalog.Remove(l);
|
||||
@@ -248,7 +250,10 @@ namespace Decentralized.Domain
|
||||
}
|
||||
|
||||
/// <summary>每晚推进订单:平台跑路、到期的订单(卖家跑路、被截获、死投被挖走)、转入可取状态。镇压期间到期顺延一天。</summary>
|
||||
public List<BmEvent> Night(int day, bool crackdown, Wallet w, Rng rng, int secRep = 0, Func<string, double> price = null)
|
||||
public List<BmEvent> Night(int day, bool crackdown, Wallet w, Rng rng, int secRep = 0, Func<string, double> price = null) => Night(day, crackdown, _ => w, rng, secRep, price);
|
||||
|
||||
/// <summary>每晚(多钱包):退款回付款的钱包,出售回款进上架时付费的钱包。</summary>
|
||||
public List<BmEvent> Night(int day, bool crackdown, Func<string, Wallet> w, Rng rng, int secRep = 0, Func<string, double> price = null)
|
||||
{
|
||||
var ev = new List<BmEvent>();
|
||||
if (!Honeypot(day) && rng.Chance(HoneypotChance(secRep))) { HoneypotUntil = day + Tuning.BmHoneypotDays; HoneypotStart = day; if (!Forum.Contains("easy")) Forum.Add("easy"); }
|
||||
@@ -275,7 +280,7 @@ namespace Decentralized.Domain
|
||||
if (crackdown) { o.DueDay = day + 1; continue; }
|
||||
if (o.SellerScam)
|
||||
{
|
||||
if (o.Mode == PayMode.Escrow) { Refund(w, o); o.State = OrderState.Refunded; o.Why = "scam"; DropTraps(o); ev.Add(new BmEvent { Kind = "scam_refund", Order = o, Amount = o.Price }); }
|
||||
if (o.Mode == PayMode.Escrow) { Refund(w(o.Wallet), o); o.State = OrderState.Refunded; o.Why = "scam"; DropTraps(o); ev.Add(new BmEvent { Kind = "scam_refund", Order = o, Amount = o.Price }); }
|
||||
else { o.State = OrderState.Lost; o.Why = "scam"; DropTraps(o); ev.Add(new BmEvent { Kind = "scam_loss", Order = o, Amount = o.Price }); }
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,8 @@ namespace Decentralized.Domain
|
||||
/// <summary>参考价:改价时按它重算标价。</summary>
|
||||
public long Ref;
|
||||
public int ListPrice;
|
||||
/// <summary>上架时付费的钱包地址:回款进这里。</summary>
|
||||
public string Wallet;
|
||||
public int ListedDay, PayDay;
|
||||
public SaleState State = SaleState.Listed;
|
||||
/// <summary>成交后扣掉抽成的货款(信用点)与其中带的污点。</summary>
|
||||
@@ -73,7 +75,7 @@ namespace Decentralized.Domain
|
||||
double share = total > 0 ? Math.Min(1, ListFee(listPrice) / total) : 0;
|
||||
w.Taint -= w.Taint * share; w.Basis -= w.Basis * share;
|
||||
w.Add(feeAsset, -qty);
|
||||
var s = new BmSale { Item = item, Model = model, FakeOf = fakeOf, Health = health, Ref = refValue, Mult = m, ListPrice = listPrice, ListedDay = day };
|
||||
var s = new BmSale { Item = item, Model = model, FakeOf = fakeOf, Health = health, Ref = refValue, Mult = m, ListPrice = listPrice, ListedDay = day, Wallet = w.Address };
|
||||
Sales.Add(s);
|
||||
return s;
|
||||
}
|
||||
@@ -104,7 +106,7 @@ namespace Decentralized.Domain
|
||||
public double HeldSales => Sales.Where(x => x.State == SaleState.Sold).Sum(x => (double)x.Proceeds);
|
||||
|
||||
/// <summary>每晚推进出售:上架的货按概率成交(假卡可能被看穿退货),成交的货款托管到期后进钱包。镇压期间平台下线,不推进。</summary>
|
||||
private void SalesNight(int day, bool crackdown, Wallet w, Func<string, double> price, Rng rng, List<BmEvent> ev)
|
||||
private void SalesNight(int day, bool crackdown, Func<string, Wallet> wallet, Func<string, double> price, Rng rng, List<BmEvent> ev)
|
||||
{
|
||||
if (crackdown) return;
|
||||
foreach (var s in Sales.Where(x => x.State == SaleState.Listed && x.ListedDay < day).ToList())
|
||||
@@ -130,6 +132,7 @@ namespace Decentralized.Domain
|
||||
{
|
||||
double p = price(Assets.CRT);
|
||||
if (p <= 0) continue;
|
||||
var w = wallet(s.Wallet);
|
||||
w.Add(Assets.CRT, s.Proceeds / p);
|
||||
w.Taint += s.Taint;
|
||||
w.Basis += s.Proceeds;
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace Decentralized.Domain
|
||||
public double Taint, Basis;
|
||||
public int Rounds;
|
||||
public int ReadyDay;
|
||||
/// <summary>存入的钱包地址:到期提回这里。</summary>
|
||||
public string Wallet;
|
||||
}
|
||||
|
||||
/// <summary>一笔混币结束时的结果,供夜间日志使用。</summary>
|
||||
@@ -56,7 +58,7 @@ namespace Decentralized.Domain
|
||||
w.Add(asset, -amount);
|
||||
w.Taint -= taint;
|
||||
w.Basis -= basis;
|
||||
var d = new MixDeposit { Asset = asset, Amount = amount, Taint = taint, Basis = basis, Rounds = rounds, ReadyDay = day + rounds };
|
||||
var d = new MixDeposit { Asset = asset, Amount = amount, Taint = taint, Basis = basis, Rounds = rounds, ReadyDay = day + rounds, Wallet = w.Address };
|
||||
Pending.Add(d);
|
||||
return d;
|
||||
}
|
||||
@@ -68,7 +70,10 @@ namespace Decentralized.Domain
|
||||
}
|
||||
|
||||
/// <summary>每晚:到期的存入提回钱包(污点按轮数降低),在途的各有小概率跑路。</summary>
|
||||
public List<MixResult> Night(Wallet w, int day, Func<string, double> price, Rng rng)
|
||||
public List<MixResult> Night(Wallet w, int day, Func<string, double> price, Rng rng) => Night(_ => w, day, price, rng);
|
||||
|
||||
/// <summary>每晚(多钱包):到期的存入按存入时的地址提回原钱包。</summary>
|
||||
public List<MixResult> Night(Func<string, Wallet> wallet, int day, Func<string, double> price, Rng rng)
|
||||
{
|
||||
var results = new List<MixResult>();
|
||||
foreach (var d in Pending.ToList())
|
||||
@@ -85,6 +90,7 @@ namespace Decentralized.Domain
|
||||
double taint = d.Taint * Math.Pow(1 - rate, d.Rounds);
|
||||
double amount = d.Amount * Math.Pow(1 - Tuning.MixerFee, d.Rounds);
|
||||
bool flagged = Sanctioned(day);
|
||||
var w = wallet(d.Wallet);
|
||||
w.Add(d.Asset, amount);
|
||||
w.Taint += taint;
|
||||
w.Basis += d.Basis;
|
||||
|
||||
@@ -136,6 +136,12 @@ namespace Decentralized.Domain
|
||||
/// <summary>单个金融违规事件记入的罪证上限。</summary>
|
||||
public const double CrimeEventCap = 400;
|
||||
|
||||
/// <summary>硬件钱包:最多几个(含主钱包);杰克逊博士的售价与每次进货上架的概率;Web3 商城的价格范围、每周平台现货上架的概率与件数上限;商城货里被投毒的比例。</summary>
|
||||
public const int WalletMax = 5, WalletJacksonPrice = 240, WalletMallMin = 150, WalletMallMax = 180, WalletMallQtyMax = 2;
|
||||
public const double WalletJacksonChance = 0.5, WalletMallChance = 0.5, WalletPoisonShare = 0.2;
|
||||
/// <summary>被投毒的钱包:币的总价值达到 DrainMin 起,每晚有概率被转空;价值越高越快:每晚概率从门槛处的 1 / SlowDays(平均 7 天)按价值线性升到 DrainFull 处的 1(当晚就转走)。</summary>
|
||||
public const double WalletDrainMin = 3000, WalletDrainFull = 20000, WalletDrainSlowDays = 7;
|
||||
|
||||
/// <summary>主交易对(信用点一侧)的深度随天数增长。</summary>
|
||||
public static double PoolDepth(int day) => PoolDepth0 * (1 + day / 60.0);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,27 @@ namespace Decentralized.Domain
|
||||
public double Basis;
|
||||
/// <summary>被执法标记。</summary>
|
||||
public bool Flagged;
|
||||
/// <summary>显示名(终端钱包页、硬件钱包标题)。</summary>
|
||||
public string Name;
|
||||
/// <summary>硬件钱包被动过手脚(供应链投毒):别人手里有助记词。玩家看不到。</summary>
|
||||
public bool Poisoned;
|
||||
|
||||
/// <summary>被投毒的钱包今晚被转空的概率:总价值不到门槛时为 0,越多越高(1–7 天)。</summary>
|
||||
public static double DrainChance(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;
|
||||
}
|
||||
|
||||
/// <summary>转空:全部余额(含冻结的)连同污点、成本一起转走,返回转走的价值(信用点)。</summary>
|
||||
public double Drain(Func<string, double> price)
|
||||
{
|
||||
double v = Value(price);
|
||||
Balances.Clear(); Frozen.Clear(); FrozenUntil.Clear(); BuyQty.Clear(); BuyCost.Clear();
|
||||
Taint = 0; Basis = 0;
|
||||
return v;
|
||||
}
|
||||
|
||||
/// <summary>买入记录:每种币买入的数量与花费(信用点等值),用来显示买入均价。卖出或转出时按比例减少。</summary>
|
||||
public Dictionary<string, double> BuyQty = new Dictionary<string, double>(), BuyCost = new Dictionary<string, double>();
|
||||
|
||||
@@ -39,6 +39,7 @@ namespace Decentralized
|
||||
public static GameItem Make(string id, string model, string fakeOf, double health)
|
||||
{
|
||||
if (id.StartsWith("gpu:")) return Items.NewCard(model, health, fakeOf, Session.Data.Rng);
|
||||
if (id == Items.HwWallet) return Items.NewHwWallet(Session.Data.Rng.Chance(Tuning.WalletPoisonShare));
|
||||
return ModHelper.SpawnItem(id);
|
||||
}
|
||||
|
||||
@@ -90,6 +91,9 @@ namespace Decentralized
|
||||
if (force) d.Black.RefreshedDay = -999;
|
||||
if (!d.Black.Refresh(day, rep, d.Rng, Value)) return false;
|
||||
d.Black.RefreshOrders(day, rep, d.Rng, Value);
|
||||
// 平台现货偶尔有硬件钱包:比杰克逊博士便宜,外观一样,其中一部分被动过手脚
|
||||
if (d.Rng.Chance(Tuning.WalletMallChance))
|
||||
d.Black.Offers.Add(new BmOffer { Item = Items.HwWallet, Kind = GoodKind.Tech, Price = d.Rng.Range(Tuning.WalletMallMin, Tuning.WalletMallMax), Qty = d.Rng.Range(1, Tuning.WalletMallQtyMax) });
|
||||
Log.Info($"黑市刷新(第 {day} 天):现货 {d.Black.Offers.Count} 种,商家 {d.Black.Sellers.Count} 家({string.Join("/", d.Black.Sellers.Select(x => x.Type))}),货架 {d.Black.Catalog.Count} 种共 {d.Black.CatalogValue:0} 信用点,高科技 {d.Black.Offers.Count(o => o.Kind == GoodKind.Tech)},显卡 {d.Black.Offers.Count(o => o.Kind == GoodKind.Gpu)}");
|
||||
return true;
|
||||
}
|
||||
@@ -97,7 +101,7 @@ namespace Decentralized
|
||||
public static void Night(SaveData d, int day, bool crackdown)
|
||||
{
|
||||
var ps = PlayerStore.Instance;
|
||||
foreach (var e in d.Black.Night(day, crackdown, d.Hot, d.Rng, StoreReputation.GetSecReputation(), Session.Price))
|
||||
foreach (var e in d.Black.Night(day, crackdown, d.WalletAt, d.Rng, StoreReputation.GetSecReputation(), Session.Price))
|
||||
{
|
||||
if (e.Sale != null)
|
||||
{
|
||||
|
||||
@@ -68,6 +68,7 @@ namespace Decentralized
|
||||
Items.Give(item);
|
||||
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, "Dr. Jackson stock (test)", () =>
|
||||
{
|
||||
EmporiumEntry.Instance.ClearFrontInv();
|
||||
@@ -96,6 +97,13 @@ namespace Decentralized
|
||||
bool ok = PlayerStore.Instance.TryAddDirectSellingItemToTable(item);
|
||||
placed.Add(ok ? $"{kind}#{item.uniqueId}" : kind + "(柜台已满)");
|
||||
}
|
||||
// 硬件钱包(正品):钱包和手上的硬件钱包加起来还没到上限时,有一定概率上一个
|
||||
int wallets = Session.Data.Wallets.Count + (owned?.Count(i => i.identifier == Items.HwWallet) ?? 0);
|
||||
if (wallets < Tuning.WalletMax && Session.Data.Rng.Chance(Tuning.WalletJacksonChance))
|
||||
{
|
||||
var hw = Items.NewHwWallet(false);
|
||||
placed.Add(PlayerStore.Instance.TryAddDirectSellingItemToTable(hw) ? $"硬件钱包#{hw.uniqueId}" : "硬件钱包(柜台已满)");
|
||||
}
|
||||
Log.Info("杰克逊博士上货:" + string.Join(",", placed));
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,6 +18,9 @@ namespace Decentralized
|
||||
public const string Guide = "pd_gpu_guide";
|
||||
public const string Web3Guide = "pd_web3_guide";
|
||||
public const string GpuPrefix = "pd_gpu_";
|
||||
public const string HwWallet = "pd_hw_wallet";
|
||||
/// <summary>硬件钱包被动过手脚(供应链投毒)。外观、说明都不显示。</summary>
|
||||
public const string PoisonTag = "PD_POISONED";
|
||||
|
||||
public const string HealthTag = "PD_HEALTH";
|
||||
public const string FakeTag = "PD_FAKE_OF";
|
||||
@@ -26,6 +29,9 @@ namespace Decentralized
|
||||
public const string LookTag = "PD_LOOK", BackTag = "PD_BACK", SeedTag = "PD_SEED", ClaimTag = "PD_CLAIM", ExposedTag = "PD_EXPOSED", MarkupTag = "PD_MARKUP", EditedTag = "PD_EDITED";
|
||||
|
||||
public static Action OpenTerminal;
|
||||
/// <summary>硬件钱包拖到交易终端上:添加一个钱包(参数:是否被投毒)。</summary>
|
||||
public static Action<bool> AddWallet;
|
||||
public static Func<bool> CanAddWallet;
|
||||
|
||||
public static void Subscribe() => ModHook.OnModItemDirectoryInit += Register;
|
||||
public static void Unsubscribe() => ModHook.OnModItemDirectoryInit -= Register;
|
||||
@@ -40,6 +46,7 @@ namespace Decentralized
|
||||
dir.Add(Rig, CreateRig);
|
||||
dir.Add(Guide, CreateGuide);
|
||||
dir.Add(Web3Guide, CreateWeb3Guide);
|
||||
dir.Add(HwWallet, CreateHwWallet);
|
||||
foreach (var spec in Hardware.Specs.Values.Where(s => !s.Asic && !s.Scrap))
|
||||
{
|
||||
var s = spec;
|
||||
@@ -93,6 +100,42 @@ namespace Decentralized
|
||||
return item;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// PLEDGER 硬件钱包:和游戏里的升级材料一样,拖到交易终端上用掉,钱包 +1。杰克逊博士卖的是正品;
|
||||
/// Web3 商城卖的便宜一些,外观和说明完全一样,其中一部分被动过手脚(<see cref="PoisonTag"/>)。
|
||||
/// </summary>
|
||||
private static GameItem CreateHwWallet()
|
||||
{
|
||||
var item = ItemDirectory.CreateEmptyItem(HwWallet);
|
||||
item.SetSpriteAndShapeFromMod(ModInfo.Id, "hw_wallet");
|
||||
item.name = L.T("item.hwwallet.name");
|
||||
item.shortDescription = L.T("item.hwwallet.desc");
|
||||
item.unitValue = Tuning.WalletJacksonPrice;
|
||||
item.SetGameItemType("MATERIAL");
|
||||
item.maySelectSlotItemFunc = (_, _, _) => true;
|
||||
item.canSelectSlotItemFunc = (_, _, _) => true;
|
||||
item.mayThisTargetItemFunc = (Func<GameItem, GameItem, bool>)Delegate.Combine(item.mayThisTargetItemFunc, (Func<GameItem, GameItem, bool>)((s, t) => CanTarget(t)));
|
||||
item.canThisTargetItemFunc = (Func<GameItem, GameItem, bool>)Delegate.Combine(item.canThisTargetItemFunc, (Func<GameItem, GameItem, bool>)((s, t) => CanTarget(t)));
|
||||
item.onThisTargetItemFunc = (Action<GameItem, GameItem>)Delegate.Combine(item.onThisTargetItemFunc, (Action<GameItem, GameItem>)((s, t) => Log.Guard("添加硬件钱包", () =>
|
||||
{
|
||||
if (!CanTarget(t)) return;
|
||||
bool poisoned = s.IsTag(PoisonTag);
|
||||
s.Destroy();
|
||||
AddWallet?.Invoke(poisoned);
|
||||
})));
|
||||
ModHelper.SetSource(item, ModInfo.Id, ModInfo.Name);
|
||||
return item;
|
||||
}
|
||||
|
||||
private static bool CanTarget(GameItem t) => t != null && t.identifier == Terminal && (CanAddWallet?.Invoke() ?? false);
|
||||
|
||||
public static GameItem NewHwWallet(bool poisoned)
|
||||
{
|
||||
var item = ModHelper.SpawnItem(HwWallet);
|
||||
if (item != null && poisoned) item.EnableTag(PoisonTag);
|
||||
return item;
|
||||
}
|
||||
|
||||
// ---------- 显卡 ----------
|
||||
|
||||
/// <summary>RTX70 → "RTX 70",RX800 → "RX 800"。</summary>
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace Decentralized
|
||||
{
|
||||
Items.Subscribe();
|
||||
Items.OpenTerminal = TerminalWindow.Open;
|
||||
Items.AddWallet = TerminalWindow.AddWallet;
|
||||
Items.CanAddWallet = TerminalWindow.CanAddWallet;
|
||||
Unlock.Register();
|
||||
Night.Subscribe();
|
||||
DealerStock.Subscribe();
|
||||
|
||||
@@ -171,7 +171,7 @@ stable.depeg,"Depegged"
|
||||
stable.warn,"Warning: staking subsidy pool low"
|
||||
stable.spiral,"Collapsing"
|
||||
aml.window,"Withdrawn in the last {0} days: {1} credits (cumulative line {2})"
|
||||
aml.taint,"Wallet taint share: {0}%"
|
||||
aml.taint,"Account taint share: {0}%"
|
||||
aml.chance,"At this amount, estimated review chance: {0}%"
|
||||
mixer.h,Mixer
|
||||
mixer.ok,Status: operating normally
|
||||
@@ -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; withdrawing turns CRC back into credits and goes through compliance screening. 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. Payments and transfers (black market, mixer, Ponero desk, listing fees) require typing the payee address (the 0x string in the merchant name, or the platform's own) and a wrong address pays nothing. 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. 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
|
||||
@@ -354,7 +354,6 @@ black.orders.out,Selling
|
||||
black.desk.pair,"PONERO [anonymous coin] price {0}"
|
||||
black.pay.amount,"Amount: {0} {1} (about {2} credits)"
|
||||
black.pay.wallet,Pay from wallet
|
||||
tab.wallet,Wallet
|
||||
hw.wallet.main,Main wallet
|
||||
hw.balance,"{0} {1} ≈ {2}"
|
||||
hw.total,"Total ≈ {0} credits"
|
||||
@@ -396,3 +395,23 @@ hw.back,Back
|
||||
hw.rejected,Signature rejected on the wallet.
|
||||
hw.wait,Confirm on the wallet.
|
||||
hw.swap,"Swap {0} {1} → about {2} {3}"
|
||||
tab.wallets,Wallets
|
||||
hw.wallet.n,"Wallet {0}"
|
||||
hw.err.poinbase,Poinbase only accepts CRC and PSC.
|
||||
hw.review.own,"Own wallet: {0}"
|
||||
hw.review.poinbase,Poinbase deposit address
|
||||
poin.deposit,"Deposit address: {0}"
|
||||
poin.out,"Withdraw to {0}"
|
||||
poin.out.done,"{0} {1} withdrawn to {2}."
|
||||
wallets.count,"Wallets {0}/{1}"
|
||||
wallets.col.name,Name
|
||||
wallets.col.addr,Address
|
||||
wallets.col.taint,Taint
|
||||
wallets.open,Open
|
||||
wallets.use,Connect
|
||||
wallets.current,Connected
|
||||
wallets.added,"Added {0}: {1}"
|
||||
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.
|
||||
|
@@ -170,7 +170,7 @@ stable.depeg,"脱锚"
|
||||
stable.warn,"预兆:质押补贴池告急"
|
||||
stable.spiral,"崩盘中"
|
||||
aml.window,"近 {0} 天已提取 {1} 信用点(累计线 {2})"
|
||||
aml.taint,"钱包污点比例:{0}%"
|
||||
aml.taint,"账户污点比例:{0}%"
|
||||
aml.chance,"按这个数量,预计审查概率:{0}%"
|
||||
mixer.h,混币器
|
||||
mixer.ok,状态:运行正常
|
||||
@@ -267,7 +267,7 @@ black.kind.Contraband.3,违禁品(高)
|
||||
black.kind.Tech,高科技物品(加价)
|
||||
nightlog.bm.ready,"黑市:{0} 到货,开店后会有快递员上门。"
|
||||
item.web3guide.name,Web3 手册
|
||||
item.web3guide.desc,"银行:存入把信用点换成 CRC;提取把 CRC 换回信用点,提取要过合规审查。交易:买入按计价币的数量支付,卖出按所交易币的数量卖出。混币器:选币种、填数量和轮数,每轮扣手续费并降低污点,轮数即等待天数,到期提回同一个地址;存入越多,清洗越差。付款与转账(黑市、混币器、Ponero 兑换处、上架费)前要手输收款地址(商家名里的 0x 开头那串,或平台自己的地址),输错不会付款。黑市:只收币,不收 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,签收
|
||||
@@ -353,7 +353,6 @@ black.orders.out,卖出
|
||||
black.desk.pair,"PONERO [匿名币] 价格 {0}"
|
||||
black.pay.amount,"金额:{0} {1}(约 {2} 信用点)"
|
||||
black.pay.wallet,打开钱包转账
|
||||
tab.wallet,钱包
|
||||
hw.wallet.main,主钱包
|
||||
hw.balance,"{0} {1} ≈ {2}"
|
||||
hw.total,"合计 ≈ {0} 信用点"
|
||||
@@ -395,3 +394,23 @@ hw.back,返回
|
||||
hw.rejected,已在钱包上拒绝签名。
|
||||
hw.wait,请在钱包上确认。
|
||||
hw.swap,"兑换 {0} {1} → 约 {2} {3}"
|
||||
tab.wallets,钱包
|
||||
hw.wallet.n,"钱包 {0}"
|
||||
hw.err.poinbase,Poinbase 只收 CRC 和 PSC。
|
||||
hw.review.own,"自己的钱包:{0}"
|
||||
hw.review.poinbase,Poinbase 充值地址
|
||||
poin.deposit,"充值地址:{0}"
|
||||
poin.out,"提到{0}"
|
||||
poin.out.done,"{0} {1} 已提到{2}。"
|
||||
wallets.count,"钱包 {0}/{1}"
|
||||
wallets.col.name,名称
|
||||
wallets.col.addr,地址
|
||||
wallets.col.taint,污点
|
||||
wallets.open,打开
|
||||
wallets.use,连接
|
||||
wallets.current,已连接
|
||||
wallets.added,"已添加{0}:{1}"
|
||||
nightlog.drained,"{0}:币被转走了,约 {1} 信用点。"
|
||||
item.hwwallet.name,PLEDGER 硬件钱包
|
||||
item.hwwallet.desc,大屏硬件钱包。拖到交易终端上,添加一个钱包(最多 5 个)。
|
||||
poin.empty,账户为空。
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 143 B |
@@ -90,7 +90,7 @@ namespace Decentralized
|
||||
// 镇压开始:黑市为换取放行,按声望出卖一批 CRT 地址
|
||||
if (ev == MarketEvent.Crackdown && Stablecoins.CrtFrozenByCrackdown(StoreReputation.GetBMReputation(), d.Rng))
|
||||
{
|
||||
double frozen = d.Hot.Freeze(Assets.CRT, Tuning.CrtFreezeShare, day + Tuning.CrtUnfreezeAfter);
|
||||
double frozen = d.Wallets.Sum(w => w.Freeze(Assets.CRT, Tuning.CrtFreezeShare, day + Tuning.CrtUnfreezeAfter));
|
||||
if (frozen > 0)
|
||||
{
|
||||
PlayerStore.Instance.AddNightLog(L.T("nightlog.crt_freeze", Fmt.Num(frozen), day + Tuning.CrtUnfreezeAfter), "cc7755");
|
||||
@@ -98,7 +98,8 @@ namespace Decentralized
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach (var t in d.Hot.Thaw(day)) PlayerStore.Instance.AddNightLog(L.T("nightlog.thaw", Fmt.Num(t.Value), t.Key));
|
||||
foreach (var w in d.Wallets)
|
||||
foreach (var t in w.Thaw(day)) PlayerStore.Instance.AddNightLog(L.T("nightlog.thaw", Fmt.Num(t.Value), t.Key));
|
||||
if (!wasSanctioned && d.Mixer.Sanctioned(day) && Unlock.MixerUnlocked) News.Show("pd_news_sanction_" + day, L.T("news.sanction.title"), L.T("news.sanction.desc"), 3);
|
||||
d.ActiveEvents = events;
|
||||
if (Unlock.BlackUnlocked) BlackDelivery.Night(d, day, events.Contains(MarketEvent.Crackdown));
|
||||
@@ -126,7 +127,9 @@ namespace Decentralized
|
||||
if (d.Network.Reward < rewardBefore) d.Market.OnHalving(d.Rng);
|
||||
if (d.Network.Reward < rewardBefore) News.Show("pd_news_halving_" + day, L.T("news.halving.title"), L.T("news.halving.desc"), 2);
|
||||
double crcFactor = d.Stable.Night(day, d.Rng, 0);
|
||||
if (crcFactor < 1) d.Hot.Balances[Assets.CRC] = d.Hot.Get(Assets.CRC) * crcFactor;
|
||||
if (crcFactor < 1)
|
||||
foreach (var w in d.Wallets.Append(d.Poinbase))
|
||||
if (w.Get(Assets.CRC) > 0) w.Balances[Assets.CRC] = w.Get(Assets.CRC) * crcFactor;
|
||||
d.Market.UpdateSupply(day);
|
||||
if (Tuning.HalvingDays - day % Tuning.HalvingDays == 3) News.Show("pd_news_prehalving_" + day, L.T("news.prehalving.title"), L.T("news.prehalving.desc"), 3);
|
||||
d.Market.Night(events, d.Rng, day, Session.Price);
|
||||
@@ -136,7 +139,7 @@ namespace Decentralized
|
||||
|
||||
var ps = PlayerStore.Instance;
|
||||
if (mined > 0) ps.AddNightLog(L.T("nightlog.mined", Fmt.Num(mined)));
|
||||
foreach (var m in d.Mixer.Night(d.Hot, day, Session.Price, d.Rng))
|
||||
foreach (var m in d.Mixer.Night(d.WalletAt, day, Session.Price, d.Rng))
|
||||
{
|
||||
Log.Info($"混币器:{(m.Rugged ? "跑路" : "提回")} {m.Amount:0.##} {m.Asset},污点 {m.TaintBefore:0} → {m.TaintAfter:0},标记 {m.Flagged}");
|
||||
if (m.Rugged) ps.AddNightLog(L.T("nightlog.mixer_rug", Fmt.Num(m.Amount), m.Asset), "cc7755");
|
||||
@@ -146,6 +149,15 @@ namespace Decentralized
|
||||
if (m.Flagged) ps.AddNightLog(line, "cc7755"); else ps.AddNightLog(line);
|
||||
}
|
||||
}
|
||||
// 被投毒的硬件钱包:钱够多以后,别人用手里的助记词把它转空
|
||||
foreach (var w in d.Wallets.Where(x => x.Poisoned))
|
||||
{
|
||||
double v = w.Value(Session.Price);
|
||||
if (!d.Rng.Chance(Wallet.DrainChance(v))) continue;
|
||||
w.Drain(Session.Price);
|
||||
Log.Info($"投毒钱包 {w.Address} 被转空:{v:0}");
|
||||
ps.AddNightLog(L.T("nightlog.drained", WalletWindow.NameOf(w), Fmt.Credits(v)), "cc7755");
|
||||
}
|
||||
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");
|
||||
|
||||
@@ -23,7 +23,29 @@ namespace Decentralized
|
||||
public Market Market;
|
||||
public Stablecoins Stable = new Stablecoins();
|
||||
public Network Network = new Network();
|
||||
public Wallet Hot = new Wallet { Address = "hot" };
|
||||
/// <summary>链上钱包(硬件钱包,第一个是主钱包),各自的余额、污点、成本、冻结独立。</summary>
|
||||
public List<Wallet> Wallets = new List<Wallet>();
|
||||
/// <summary>终端当前连接的钱包:Web3 页的交易、黑市、混币器都用它,用币付款的顾客、挖矿收益也进它。</summary>
|
||||
public int Current;
|
||||
/// <summary>Poinbase 托管账户:银行存取、Poinbase 交易、出金审查都在这里。</summary>
|
||||
public Wallet Poinbase = new Wallet { Address = "poinbase" };
|
||||
|
||||
[JsonIgnore] public Wallet Hot => Wallets[Math.Max(0, Math.Min(Wallets.Count - 1, Current))];
|
||||
|
||||
/// <summary>按地址找自己的钱包;找不到(钱包没了)退回当前钱包。</summary>
|
||||
public Wallet WalletAt(string address) => Wallets.Find(w => ChainAddress.Same(w.Address, address)) ?? Hot;
|
||||
|
||||
public Wallet Own(string address) => Wallets.Find(w => ChainAddress.Same(w.Address, address));
|
||||
|
||||
public Wallet AddWallet(bool poisoned)
|
||||
{
|
||||
var w = new Wallet { Address = ChainAddress.Make(Rng.NextUInt()), Poisoned = poisoned };
|
||||
Wallets.Add(w);
|
||||
return w;
|
||||
}
|
||||
|
||||
/// <summary>链上资产合计(全部钱包)。</summary>
|
||||
public double ChainValue(Func<string, double> price) { double v = 0; foreach (var w in Wallets) v += w.Value(price); return v; }
|
||||
public Exchange Exchange = new Exchange();
|
||||
public Mixer Mixer = new Mixer();
|
||||
public BlackMarket Black = new BlackMarket();
|
||||
@@ -58,6 +80,7 @@ namespace Decentralized
|
||||
public static void Load()
|
||||
{
|
||||
Data = null;
|
||||
WalletWindow.Reset();
|
||||
try
|
||||
{
|
||||
string json = ModHelper.GetModData(ModInfo.Id, Key);
|
||||
@@ -68,6 +91,7 @@ namespace Decentralized
|
||||
if (Data.Market == null) Data.Market = Market.CreateDefault(StoreStation.GetDayCounter());
|
||||
// 旧存档里的商家地址没有校验位:换成合法地址
|
||||
foreach (var s in Data.Black.Sellers) if (!ChainAddress.Valid(s.Name)) s.Name = ChainAddress.Make(Data.Rng.NextUInt());
|
||||
if (Data.Wallets.Count == 0) Data.AddWallet(false);
|
||||
Log.Info($"状态已载入(第 {Data.Day} 天,PSC {Data.Market.Price:0.00})");
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Decentralized
|
||||
public static class TerminalWindow
|
||||
{
|
||||
public const string Id = "pd_terminal";
|
||||
private enum Page { Home, Bank, Poinbase, Web3 }
|
||||
private enum Page { Home, Bank, Poinbase, Web3, Wallets }
|
||||
|
||||
private static Page _page = Page.Home;
|
||||
private sealed class EditPrice { public GameItem Item; public BmSale Sale; public string Id; public long Ref; }
|
||||
@@ -35,7 +35,6 @@ namespace Decentralized
|
||||
string name = L.T("tab." + p.ToString().ToLowerInvariant());
|
||||
b.AddButton(page == _page ? $"[{name}]" : name, () => { _page = page; _message = null; _edit = null; Open(); });
|
||||
}
|
||||
b.AddButton(L.T("tab.wallet"), WalletWindow.Open);
|
||||
b.End();
|
||||
b.AddSeparator();
|
||||
b.BeginScroll(540);
|
||||
@@ -45,6 +44,7 @@ namespace Decentralized
|
||||
case Page.Bank: BankPage(b); break;
|
||||
case Page.Poinbase: ExchangePage(b, Venue.Exchange); break;
|
||||
case Page.Web3: Web3Page(b); break;
|
||||
case Page.Wallets: WalletsPage(b); break;
|
||||
default: break;
|
||||
}
|
||||
b.End();
|
||||
@@ -76,7 +76,7 @@ namespace Decentralized
|
||||
private static void HomePage(CustomUIBuilder b)
|
||||
{
|
||||
var d = Session.Data;
|
||||
double chain = d.Hot.Value(Session.Price);
|
||||
double chain = d.ChainValue(Session.Price);
|
||||
b.AddLabel(L.T("home.summary", PlayerStore.Instance.playerCash, Fmt.Credits(chain)));
|
||||
b.AddSpace(6f);
|
||||
b.BeginRow(8f, TextAnchor.UpperCenter);
|
||||
@@ -144,7 +144,7 @@ namespace Decentralized
|
||||
l.AddHeader(L.T("entry.bank"));
|
||||
TableRow(l, w, L.T("bank.cash.k"), ps.playerCash.ToString());
|
||||
TableRow(l, w, L.T("bank.rent.k"), L.T("bank.rent.v", Session.Rent(), ps.GetRentDayCounter()));
|
||||
TableRow(l, w, L.T("bank.crc.k"), Fmt.Num(Session.Data.Hot.Get(Assets.CRC)));
|
||||
TableRow(l, w, L.T("bank.crc.k"), Fmt.Num(Session.Data.Poinbase.Get(Assets.CRC)));
|
||||
}, r =>
|
||||
{
|
||||
r.AddHeader(L.T("bank.ops"));
|
||||
@@ -164,14 +164,15 @@ namespace Decentralized
|
||||
double recent = d.Exchange.History.Where(h => h.Key > day - Tuning.AmlWindowDays).Sum(h => h.Value);
|
||||
b.AddSeparator();
|
||||
b.AddLabel(L.T("aml.window", Tuning.AmlWindowDays, Fmt.Credits(recent), Fmt.Credits(Tuning.AmlCumRents * rent)));
|
||||
double total = d.Hot.Value(Session.Price);
|
||||
double taintRatio = total > 0 ? d.Hot.Taint / total : 0;
|
||||
var pb = d.Poinbase;
|
||||
double total = pb.Value(Session.Price);
|
||||
double taintRatio = total > 0 ? pb.Taint / total : 0;
|
||||
b.AddLabel(L.T("aml.taint", (int)Math.Round(taintRatio * 100)));
|
||||
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double v) && v > 0)
|
||||
{
|
||||
double value = Math.Min(v, d.Hot.Get(Assets.CRC)) * Session.Price(Assets.CRC);
|
||||
double taint = total > 0 ? d.Hot.Taint * Math.Min(1, value / total) : 0;
|
||||
double chance = Exchange.HitChance(value, taint, recent, rent, d.Hot.Flagged);
|
||||
double value = Math.Min(v, pb.Get(Assets.CRC)) * Session.Price(Assets.CRC);
|
||||
double taint = total > 0 ? pb.Taint * Math.Min(1, value / total) : 0;
|
||||
double chance = Exchange.HitChance(value, taint, recent, rent, pb.Flagged);
|
||||
b.AddLabel(L.T("aml.chance", (int)Math.Round(chance * 100)));
|
||||
}
|
||||
}
|
||||
@@ -221,7 +222,7 @@ namespace Decentralized
|
||||
any = true;
|
||||
TableRow(b, HoldW, kv.Key, Fmt.Num(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key)), AvgText(w, kv.Key));
|
||||
}
|
||||
if (!any) b.AddLabel(L.T("wallet.empty"));
|
||||
if (!any) b.AddLabel(L.T(w == Session.Data.Poinbase ? "poin.empty" : "wallet.empty"));
|
||||
foreach (var kv in w.Frozen.OrderBy(k => k.Key).Where(k => show(k.Key)))
|
||||
b.AddLabel(L.T("wallet.frozen", kv.Key, Fmt.Num(kv.Value), w.FrozenUntil.TryGetValue(kv.Key, out var u) ? u : 0));
|
||||
}
|
||||
@@ -229,19 +230,87 @@ namespace Decentralized
|
||||
/// <summary>Poinbase:合规交易所。左边账户持仓,右边交易;信用点的存取在银行。</summary>
|
||||
private static void ExchangePage(CustomUIBuilder b, Venue venue)
|
||||
{
|
||||
var w = Session.Data.Hot;
|
||||
var d = Session.Data;
|
||||
var w = d.Poinbase;
|
||||
Columns(b, l =>
|
||||
{
|
||||
l.AddHeader(L.T("poin.account.h"));
|
||||
Holdings(l, w, a => a == Assets.CRC || a == Assets.PSC);
|
||||
Holdings(l, w, a => true);
|
||||
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
|
||||
l.AddSeparator();
|
||||
l.AddLabel(L.T("poin.deposit", d.Address("poinbase")));
|
||||
}, r =>
|
||||
{
|
||||
r.AddHeader(L.T("trade.h"));
|
||||
AmountRow(r);
|
||||
TradeRows(r, venue);
|
||||
r.AddSeparator();
|
||||
r.BeginRow();
|
||||
foreach (var a in new[] { Assets.CRC, Assets.PSC })
|
||||
{
|
||||
var asset = a;
|
||||
r.AddButton(asset == _poinAsset ? $"[{asset}]" : asset, () => { _poinAsset = asset; Open(); });
|
||||
}
|
||||
r.End();
|
||||
r.AddButton(L.T("poin.out", WalletWindow.NameOf(d.Hot)), PoinbaseOut);
|
||||
});
|
||||
}
|
||||
|
||||
private static string _poinAsset = Assets.CRC;
|
||||
|
||||
/// <summary>Poinbase 提到当前钱包:托管账户直接转,不用签名。污点按比例随币走。</summary>
|
||||
private static void PoinbaseOut()
|
||||
{
|
||||
if (!ReadAmount(out double v)) return;
|
||||
var d = Session.Data;
|
||||
if (d.Poinbase.Avail(_poinAsset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
||||
Wallet.Transfer(d.Poinbase, d.Hot, _poinAsset, v, Session.Price);
|
||||
_message = L.T("poin.out.done", Fmt.Num(v), _poinAsset, WalletWindow.NameOf(d.Hot));
|
||||
Session.Save();
|
||||
WalletWindow.RefreshDevice(d.Hot);
|
||||
Open();
|
||||
}
|
||||
|
||||
/// <summary>钱包页:全部硬件钱包,连接(设为当前)、打开实体钱包。</summary>
|
||||
private static void WalletsPage(CustomUIBuilder b)
|
||||
{
|
||||
var d = Session.Data;
|
||||
b.AddLabel(L.T("wallets.count", d.Wallets.Count, Tuning.WalletMax));
|
||||
var cols = new[] { 130f, 130f, 120f, 110f, 220f };
|
||||
TableHeader(b, cols, L.T("wallets.col.name"), L.T("wallets.col.addr"), L.T("col.value"), L.T("wallets.col.taint"), "");
|
||||
for (int i = 0; i < d.Wallets.Count; i++)
|
||||
{
|
||||
var w = d.Wallets[i];
|
||||
int idx = i;
|
||||
b.BeginRow(2f);
|
||||
Cell(b, cols[0], (idx == d.Current ? "● " : "") + WalletWindow.NameOf(w));
|
||||
Cell(b, cols[1], w.Address);
|
||||
Cell(b, cols[2], Fmt.Credits(w.Value(Session.Price)));
|
||||
Cell(b, cols[3], Fmt.Credits(w.Taint));
|
||||
b.BeginPanel(cols[4], 30f, TextAnchor.MiddleLeft, 2f, 0f);
|
||||
b.BeginRow(4f);
|
||||
b.AddButton(L.T("wallets.open"), () => WalletWindow.Open(w));
|
||||
if (idx != d.Current) b.AddButton(L.T("wallets.use"), () => { d.Current = idx; Session.Save(); Open(); });
|
||||
else b.AddLabel(L.T("wallets.current"));
|
||||
b.End();
|
||||
b.End();
|
||||
b.End();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>硬件钱包拖到终端上:添加一个钱包(被投毒的钱包看起来一样)。</summary>
|
||||
public static bool CanAddWallet() => Session.Data != null && Session.Data.Wallets.Count < Tuning.WalletMax;
|
||||
|
||||
public static void AddWallet(bool poisoned)
|
||||
{
|
||||
var w = Session.Data.AddWallet(poisoned);
|
||||
Log.Info($"添加硬件钱包 {w.Address}{(poisoned ? "(被投毒)" : "")}");
|
||||
Session.Save();
|
||||
_page = Page.Wallets;
|
||||
_message = L.T("wallets.added", WalletWindow.NameOf(w), w.Address);
|
||||
Open();
|
||||
}
|
||||
|
||||
/// <summary>Web3:链上钱包与去中心化交易。</summary>
|
||||
private static void Web3Page(CustomUIBuilder b)
|
||||
{
|
||||
@@ -265,7 +334,7 @@ namespace Decentralized
|
||||
}
|
||||
Columns(b, l =>
|
||||
{
|
||||
l.AddHeader(L.T("web3.wallet.h"));
|
||||
l.AddHeader(L.T("web3.wallet.h") + " " + WalletWindow.NameOf(w));
|
||||
Holdings(l, w, a => true);
|
||||
l.AddSeparator();
|
||||
l.AddLabel(L.T("wallet.total", Fmt.Credits(w.Value(Session.Price))));
|
||||
@@ -389,7 +458,7 @@ namespace Decentralized
|
||||
GoodHead(c, g, L.T("black.stock", offer.Qty));
|
||||
c.AddLabel(L.T("black.price", offer.Price * count) + (count > 1 ? " ×" + count : ""));
|
||||
c.AddFlexibleSpace();
|
||||
c.AddButton(L.T("black.buy"), () => { string asset = _payAsset; BeginPay(d.Address("platform"), ItemName(offer.Item) + (count > 1 ? " ×" + count : ""), asset, offer.Price * count, () => BuyOffer(offer, count, asset)); });
|
||||
c.AddButton(L.T("black.buy"), () => { string asset = _payAsset; BeginPay(d.Address("platform"), ItemName(offer.Item) + (count > 1 ? " ×" + count : ""), asset, offer.Price * count, pw => BuyOffer(offer, count, asset, pw)); });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -412,7 +481,7 @@ namespace Decentralized
|
||||
b.AddButton(L.T("black.sell.all"), () =>
|
||||
{
|
||||
string asset = _payAsset; double mult = _sellMult;
|
||||
BeginPay(d.Address("platform"), L.T("black.sell.all") + L.T("pay.listfee", mult.ToString("0.0", CultureInfo.InvariantCulture)), asset, BlackDelivery.Sellable().Sum(it => BlackMarket.ListFee(BlackMarket.ListPriceOf(BlackDelivery.RefValue(it), mult))), () => ListAll(asset, mult));
|
||||
BeginPay(d.Address("platform"), L.T("black.sell.all") + L.T("pay.listfee", mult.ToString("0.0", CultureInfo.InvariantCulture)), asset, BlackDelivery.Sellable().Sum(it => BlackMarket.ListFee(BlackMarket.ListPriceOf(BlackDelivery.RefValue(it), mult))), pw => ListAll(asset, mult, pw));
|
||||
});
|
||||
b.AddSeparator();
|
||||
var items = BlackDelivery.Sellable();
|
||||
@@ -437,7 +506,7 @@ namespace Decentralized
|
||||
|
||||
private static readonly System.Collections.Generic.Dictionary<BmGood, GameItem> SellCards = new System.Collections.Generic.Dictionary<BmGood, GameItem>();
|
||||
|
||||
private static void ListAll(string asset, double mult)
|
||||
private static void ListAll(string asset, double mult, Wallet pw)
|
||||
{
|
||||
var d = Session.Data;
|
||||
int n = 0;
|
||||
@@ -446,7 +515,7 @@ namespace Decentralized
|
||||
string id = BlackDelivery.SellId(it);
|
||||
if (id == null) continue;
|
||||
var u = Items.IsGpu(it) ? Items.ToUnit(it) : null;
|
||||
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price);
|
||||
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, pw, asset, StoreStation.GetDayCounter(), Session.Price);
|
||||
if (s == null) break;
|
||||
it.Destroy();
|
||||
n++;
|
||||
@@ -456,13 +525,13 @@ namespace Decentralized
|
||||
Open();
|
||||
}
|
||||
|
||||
private static void ListItem(GameItem it, double mult, string asset)
|
||||
private static void ListItem(GameItem it, double mult, string asset, Wallet pw)
|
||||
{
|
||||
var d = Session.Data;
|
||||
string id = BlackDelivery.SellId(it);
|
||||
if (id == null) { Open(); return; }
|
||||
var u = Items.IsGpu(it) ? Items.ToUnit(it) : null;
|
||||
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price);
|
||||
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, pw, asset, StoreStation.GetDayCounter(), Session.Price);
|
||||
if (s == null) { _message = d.Black.ActiveListings >= Tuning.BmMaxListings ? L.T("black.sell.full", Tuning.BmMaxListings) : L.T("msg.balance"); Open(); return; }
|
||||
it.Destroy();
|
||||
_message = L.T("black.sell.listed", ItemName(id), s.ListPrice);
|
||||
@@ -496,7 +565,7 @@ namespace Decentralized
|
||||
{
|
||||
if (!ok) return;
|
||||
string asset = _payAsset; var mode = _mode;
|
||||
BeginPay(seller.Name, ItemName(listing.Item) + (count > 1 ? " ×" + count : ""), asset, BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, count), mode), () => PlaceOrder(listing, count, asset, mode));
|
||||
BeginPay(seller.Name, ItemName(listing.Item) + (count > 1 ? " ×" + count : ""), asset, BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, count), mode), pw => PlaceOrder(listing, count, asset, mode, pw));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -533,10 +602,10 @@ namespace Decentralized
|
||||
private static string ItemName(string id) => BlackDelivery.ItemName(id);
|
||||
|
||||
/// <summary>开账单:收款地址、币种、金额(按信用点等值折成币),付款在硬件钱包里做。</summary>
|
||||
private static void BeginPay(string address, string summary, string asset, double credits, Action execute) =>
|
||||
private static void BeginPay(string address, string summary, string asset, double credits, Action<Wallet> execute) =>
|
||||
BillFor(address, summary, asset, credits / Session.Price(asset), execute);
|
||||
|
||||
private static void BillFor(string address, string summary, string asset, double amount, Action execute)
|
||||
private static void BillFor(string address, string summary, string asset, double amount, Action<Wallet> execute)
|
||||
{
|
||||
WalletWindow.Pending = new WalletWindow.Bill { Address = address, Summary = summary, Asset = asset, Amount = WalletWindow.RoundUp(amount), Execute = execute };
|
||||
_message = null;
|
||||
@@ -557,7 +626,7 @@ namespace Decentralized
|
||||
b.End();
|
||||
}
|
||||
|
||||
private static void BuyOffer(BmOffer o, int count, string asset)
|
||||
private static void BuyOffer(BmOffer o, int count, string asset, Wallet pw)
|
||||
{
|
||||
var d = Session.Data;
|
||||
string item = o.Item, model = o.Model, fake = o.FakeOf;
|
||||
@@ -565,7 +634,7 @@ namespace Decentralized
|
||||
int bought = 0;
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
if (!d.Black.Buy(o, d.Hot, asset, Session.Price)) break;
|
||||
if (!d.Black.Buy(o, pw, asset, Session.Price)) break;
|
||||
Items.Give(BlackDelivery.Make(item, model, fake, health));
|
||||
bought++;
|
||||
}
|
||||
@@ -575,10 +644,10 @@ namespace Decentralized
|
||||
Open();
|
||||
}
|
||||
|
||||
private static void PlaceOrder(BmListing l, int count, string asset, PayMode mode)
|
||||
private static void PlaceOrder(BmListing l, int count, string asset, PayMode mode, Wallet pw)
|
||||
{
|
||||
var d = Session.Data;
|
||||
var o = d.Black.Place(l, count, mode, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price);
|
||||
var o = d.Black.Place(l, count, mode, pw, asset, StoreStation.GetDayCounter(), Session.Price);
|
||||
if (o == null) { _message = L.T("msg.balance"); Open(); return; }
|
||||
_message = L.T("black.ordered", ItemName(o.Item) + (o.Count > 1 ? " ×" + o.Count : ""), o.DueDay);
|
||||
Session.Save();
|
||||
@@ -625,16 +694,16 @@ namespace Decentralized
|
||||
_edit = null;
|
||||
string asset = _payAsset;
|
||||
BeginPay(d.Address("platform"), ItemName(e.Id) + L.T(e.Sale != null ? "pay.repricefee" : "pay.listfee", mult.ToString("0.00", CultureInfo.InvariantCulture)), asset, fee,
|
||||
() => { if (e.Sale != null) RepriceSale(e.Sale, mult, asset); else ListItem(e.Item, mult, asset); });
|
||||
pw => { if (e.Sale != null) RepriceSale(e.Sale, mult, asset, pw); else ListItem(e.Item, mult, asset, pw); });
|
||||
});
|
||||
b.AddButton(L.T("black.edit.cancel"), () => { _edit = null; _message = null; Open(); });
|
||||
b.End();
|
||||
}
|
||||
|
||||
private static void RepriceSale(BmSale s, double mult, string asset)
|
||||
private static void RepriceSale(BmSale s, double mult, string asset, Wallet pw)
|
||||
{
|
||||
var d = Session.Data;
|
||||
if (!d.Black.Reprice(s, mult, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price)) { _message = L.T("msg.balance"); Open(); return; }
|
||||
if (!d.Black.Reprice(s, mult, pw, asset, StoreStation.GetDayCounter(), Session.Price)) { _message = L.T("msg.balance"); Open(); return; }
|
||||
_message = L.T("black.sell.repriced", ItemName(s.Item), s.ListPrice);
|
||||
Session.Save();
|
||||
Open();
|
||||
@@ -701,16 +770,16 @@ namespace Decentralized
|
||||
bool into = _deskIn; string other = _deskAsset;
|
||||
string asset = into ? other : Assets.Ponero;
|
||||
_web3Tab = Web3Tab.Trade;
|
||||
BillFor(d.Address("desk"), L.T("pay.swap", Fmt.Num(dv), asset), asset, dv, () => DeskSwap(dv, into, other));
|
||||
BillFor(d.Address("desk"), L.T("pay.swap", Fmt.Num(dv), asset), asset, dv, pw => DeskSwap(dv, into, other, pw));
|
||||
}
|
||||
}
|
||||
|
||||
private static void DeskSwap(double v, bool into, string other)
|
||||
private static void DeskSwap(double v, bool into, string other, Wallet pw)
|
||||
{
|
||||
var d = Session.Data;
|
||||
_deskIn = into; _deskAsset = other;
|
||||
int day = StoreStation.GetDayCounter();
|
||||
double got = _deskIn ? d.Black.ToPonero(d.Hot, _deskAsset, v, day, Session.Price) : d.Black.FromPonero(d.Hot, _deskAsset, v, day, Session.Price);
|
||||
double got = _deskIn ? d.Black.ToPonero(pw, _deskAsset, v, day, Session.Price) : d.Black.FromPonero(pw, _deskAsset, v, day, Session.Price);
|
||||
if (got < 0) { _message = L.T("black.desk.fail"); Open(); return; }
|
||||
// 从 Ponero 出来的币来源不明:出金审查按"来自混币器"加重
|
||||
if (!_deskIn) d.Mixer.LastReturnDay = day;
|
||||
@@ -774,17 +843,17 @@ namespace Decentralized
|
||||
{
|
||||
if (!ReadAmount(out double mv)) return;
|
||||
string asset = _mixAsset; int rounds = _mixRounds;
|
||||
BillFor(d.Address("mixer"), L.T("pay.mix", Fmt.Num(mv), asset), asset, mv, () => MixDeposit(mv, asset, rounds));
|
||||
BillFor(d.Address("mixer"), L.T("pay.mix", Fmt.Num(mv), asset), asset, mv, pw => MixDeposit(mv, asset, rounds, pw));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private static void MixDeposit(double v, string asset, int rounds)
|
||||
private static void MixDeposit(double v, string asset, int rounds, Wallet pw)
|
||||
{
|
||||
var d = Session.Data;
|
||||
int day = StoreStation.GetDayCounter();
|
||||
if (d.Hot.Avail(asset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
||||
var dep = d.Mixer.Deposit(d.Hot, asset, v, rounds, day, Session.Price);
|
||||
if (pw.Avail(asset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
||||
var dep = d.Mixer.Deposit(pw, asset, v, rounds, day, Session.Price);
|
||||
_message = dep == null ? L.T("mixer.min", Tuning.MixerMin) : L.T("mixer.done", Fmt.Num(v), asset, dep.ReadyDay);
|
||||
Session.Save();
|
||||
Open();
|
||||
@@ -825,7 +894,7 @@ namespace Decentralized
|
||||
int credits = (int)Math.Floor(v);
|
||||
if (credits <= 0 || PlayerStore.Instance.playerCash < credits) { _message = L.T("msg.balance"); Open(); return; }
|
||||
ModHelper.AddCredit(-credits, false);
|
||||
var w = Session.Data.Hot;
|
||||
var w = Session.Data.Poinbase;
|
||||
w.Add(Assets.CRC, credits / Session.Data.Stable.Crc);
|
||||
w.RecordBuy(Assets.CRC, credits / Session.Data.Stable.Crc, credits);
|
||||
w.Basis += credits;
|
||||
@@ -838,8 +907,8 @@ namespace Decentralized
|
||||
{
|
||||
if (!ReadAmount(out double v)) return;
|
||||
var d = Session.Data;
|
||||
if (d.Hot.Get(Assets.CRC) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
||||
var r = d.Exchange.Cashout(d.Hot, v, Session.Price, StoreStation.GetDayCounter(), Session.Rent(), d.Mixer.RecentlyMixed(StoreStation.GetDayCounter()), d.Rng);
|
||||
if (d.Poinbase.Avail(Assets.CRC) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
||||
var r = d.Exchange.Cashout(d.Poinbase, v, Session.Price, StoreStation.GetDayCounter(), Session.Rent(), d.Mixer.RecentlyMixed(StoreStation.GetDayCounter()), d.Rng);
|
||||
int got = (int)Math.Floor(r.Received - r.Tax);
|
||||
ModHelper.AddCredit(got, true);
|
||||
_message = L.T("msg.cashout", got, Fmt.Credits(r.Tax));
|
||||
@@ -857,19 +926,18 @@ namespace Decentralized
|
||||
private static void Trade(Pool p, bool buy)
|
||||
{
|
||||
if (!ReadAmount(out double v)) return;
|
||||
var w = Session.Data.Hot;
|
||||
var w = p.Venue == Venue.Dex ? Session.Data.Hot : Session.Data.Poinbase;
|
||||
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
|
||||
if (w.Avail(pay) + 1e-9 < v) { _message = L.T(frozenPay(w, pay) ? "msg.frozen" : "msg.balance"); Open(); return; }
|
||||
if (p.Venue != Venue.Dex) { DoTrade(p, buy, v); return; }
|
||||
if (p.Venue != Venue.Dex) { DoTrade(p, buy, v, w); return; }
|
||||
double quote = buy ? v / p.Price : v * p.Price;
|
||||
_message = L.T("hw.wait");
|
||||
Open();
|
||||
WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Summary = L.T("hw.swap", Fmt.Num(v), pay, Fmt.Num(quote), get), Execute = () => DoTrade(p, buy, v) });
|
||||
WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Summary = L.T("hw.swap", Fmt.Num(v), pay, Fmt.Num(quote), get), Execute = pw => DoTrade(p, buy, v, pw) });
|
||||
}
|
||||
|
||||
private static void DoTrade(Pool p, bool buy, double v)
|
||||
private static void DoTrade(Pool p, bool buy, double v, Wallet w)
|
||||
{
|
||||
var w = Session.Data.Hot;
|
||||
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
|
||||
if (w.Avail(pay) + 1e-9 < v) { _message = L.T(frozenPay(w, pay) ? "msg.frozen" : "msg.balance"); Open(); return; }
|
||||
double got = buy ? p.BuyBase(v) : p.SellBase(v);
|
||||
|
||||
+195
-134
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Decentralized.Domain;
|
||||
@@ -10,27 +11,28 @@ using UnityEngine.UI;
|
||||
namespace Decentralized
|
||||
{
|
||||
/// <summary>
|
||||
/// 硬件钱包:从终端打开的独立窗口,石墨机身、墨水屏、下面两个物理按键(左 ✕ 拒绝,右 ✓ 确认,签名要按住)。
|
||||
/// 链上的转账都从这里发:终端只开账单(收款地址、币种、金额),玩家在钱包里手输地址、核对、按住 ✓ 签名,
|
||||
/// 然后看着交易进内存池、打包、确认。地址带校验位,输错一位几乎都会被拦下;碰巧合法的错地址没人用,币就没了。
|
||||
/// DEX 兑换是合约调用,没有收款地址,只要签名。
|
||||
/// 硬件钱包 PLEDGER:每个钱包一台,从终端的钱包页打开,各是一个独立窗口。石墨机身、墨水屏、下面两个物理按键
|
||||
/// (左 × 拒绝 / 返回,右 ✓ 下一步,签名要按住)。链上的转账都从这里发:终端只开账单(收款地址、币种、金额),
|
||||
/// 玩家在钱包里手输地址、核对、按住 ✓ 签名,然后看着交易进内存池、打包、确认。地址带校验位,输错一位几乎都会被拦下;
|
||||
/// 碰巧合法的错地址没人用,币就没了。DEX 兑换是合约调用,没有收款地址,只要签名。
|
||||
/// 收款方:有账单的地址(执行账单)、自己的另一个钱包、Poinbase 充值地址(只收 CRC / PSC);其余地址币转进去就回不来。
|
||||
/// </summary>
|
||||
public static class WalletWindow
|
||||
{
|
||||
public const string Id = "pd_wallet";
|
||||
private const string IdPrefix = "pd_wallet_";
|
||||
private const float WinW = 452f, WinH = 624f, ScreenW = 384f, ScreenH = 470f, Bezel = 6f;
|
||||
private enum Screen { Home, Send, Review, Broadcast, Done }
|
||||
|
||||
/// <summary>终端开出的账单:转账(有收款地址)或合约调用(DEX 兑换,没有地址)。付清后执行。</summary>
|
||||
/// <summary>终端开出的账单:转账(有收款地址)或合约调用(DEX 兑换,没有地址)。付清后用付款的钱包执行。</summary>
|
||||
public sealed class Bill
|
||||
{
|
||||
public string Address, Asset, Summary;
|
||||
public double Amount;
|
||||
public bool Contract;
|
||||
public Action Execute;
|
||||
public Action<Wallet> Execute;
|
||||
}
|
||||
|
||||
/// <summary>当前待付的账单(同一时间一张,新的覆盖旧的)。</summary>
|
||||
/// <summary>当前待付的账单(同一时间一张,新的覆盖旧的)。任何一台钱包都可以付。</summary>
|
||||
public static Bill Pending;
|
||||
|
||||
private sealed class Tx
|
||||
@@ -43,84 +45,118 @@ namespace Decentralized
|
||||
public bool Finished;
|
||||
}
|
||||
|
||||
private static Screen _screen = Screen.Home;
|
||||
private static string _to = "", _amount = "", _asset = Assets.PSC, _note;
|
||||
private static Tx _tx;
|
||||
/// <summary>一台硬件钱包的屏幕状态。</summary>
|
||||
private sealed class Device
|
||||
{
|
||||
public string Address;
|
||||
public Screen Screen = Screen.Home;
|
||||
public string To = "", Amount = "", Asset = Assets.PSC, Note;
|
||||
public Tx Tx;
|
||||
public string Id => IdPrefix + Address;
|
||||
public Wallet Wallet => Session.Data.Own(Address);
|
||||
}
|
||||
|
||||
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 };
|
||||
|
||||
private static readonly Color Ink = new Color(0.12f, 0.12f, 0.14f),
|
||||
Paper = new Color(0.86f, 0.87f, 0.83f), 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),
|
||||
Dim = new Color(0.38f, 0.39f, 0.40f), Alert = new Color(0.62f, 0.16f, 0.12f);
|
||||
|
||||
public static bool IsOpen => CustomUIManager.Instance != null && CustomUIManager.Instance.IsOpen(Id);
|
||||
public static string NameOf(Wallet w)
|
||||
{
|
||||
int i = Session.Data.Wallets.IndexOf(w);
|
||||
return i == 0 ? L.T("hw.wallet.main") : L.T("hw.wallet.n", i + 1);
|
||||
}
|
||||
|
||||
/// <summary>终端"打开钱包转账":账单的币种和金额填好,地址留给玩家手输。</summary>
|
||||
private static Device Dev(Wallet w)
|
||||
{
|
||||
if (!Devices.TryGetValue(w.Address, out var dev)) Devices[w.Address] = dev = new Device { Address = w.Address };
|
||||
return dev;
|
||||
}
|
||||
|
||||
private static bool IsOpen(Device dev) => CustomUIManager.Instance != null && CustomUIManager.Instance.IsOpen(dev.Id);
|
||||
|
||||
/// <summary>读档 / 新游戏后清掉上一局的设备状态并关窗。</summary>
|
||||
public static void Reset()
|
||||
{
|
||||
foreach (var dev in Devices.Values)
|
||||
if (IsOpen(dev)) CustomUIManager.Instance.CloseWindow(dev.Id);
|
||||
Devices.Clear();
|
||||
Pending = null;
|
||||
}
|
||||
|
||||
/// <summary>终端"打开钱包转账" / DEX 签名请求:在当前连接的钱包上打开。转账填好币种和金额,地址留给玩家手输。</summary>
|
||||
public static void OpenForBill(Bill bill)
|
||||
{
|
||||
Pending = bill;
|
||||
if (_tx != null && !_tx.Finished) { Open(); return; }
|
||||
_note = null;
|
||||
if (bill.Contract) _screen = Screen.Review;
|
||||
var dev = Dev(Session.Data.Hot);
|
||||
if (dev.Tx != null && !dev.Tx.Finished) { Open(dev); return; }
|
||||
dev.Note = null;
|
||||
if (bill.Contract) dev.Screen = Screen.Review;
|
||||
else
|
||||
{
|
||||
_screen = Screen.Send;
|
||||
_to = "";
|
||||
_asset = bill.Asset;
|
||||
_amount = Amt(bill.Amount);
|
||||
dev.Screen = Screen.Send;
|
||||
dev.To = "";
|
||||
dev.Asset = bill.Asset;
|
||||
dev.Amount = Amt(bill.Amount);
|
||||
}
|
||||
Open();
|
||||
Open(dev);
|
||||
}
|
||||
|
||||
public static void Open()
|
||||
public static void Open(Wallet w) { if (w != null) Open(Dev(w)); }
|
||||
|
||||
private static void Open(Device dev)
|
||||
{
|
||||
if (Session.Data == null) return;
|
||||
if (Session.Data == null || dev.Wallet == null) return;
|
||||
WalletDriver.Ensure();
|
||||
var b = CustomUIManager.Window(Id, "PLEDGER");
|
||||
var b = CustomUIManager.Window(dev.Id, "PLEDGER");
|
||||
if (b == null) return;
|
||||
int idx = Session.Data.Wallets.IndexOf(dev.Wallet);
|
||||
b.SetSize(WinW, WinH).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
|
||||
b.SetPosition(new Vector2(600f, 0f));
|
||||
b.SetPosition(new Vector2(600f + 28f * idx, -24f * idx));
|
||||
// 屏幕外面一圈黑色边框,里面是墨水屏
|
||||
b.BeginPanel(ScreenW + Bezel * 2, ScreenH + Bezel * 2, TextAnchor.MiddleCenter, Bezel + 3f, 0f);
|
||||
var bezel = b.Last.image;
|
||||
b.BeginPanel(ScreenW, ScreenH, TextAnchor.UpperLeft, 14f, 6f);
|
||||
var screen = b.CurrentContainer;
|
||||
var paper = b.Last.image;
|
||||
switch (_screen)
|
||||
switch (dev.Screen)
|
||||
{
|
||||
case Screen.Send: SendScreen(b); break;
|
||||
case Screen.Review: ReviewScreen(b); break;
|
||||
case Screen.Broadcast: BroadcastScreen(b); break;
|
||||
case Screen.Done: DoneScreen(b); break;
|
||||
default: HomeScreen(b); break;
|
||||
case Screen.Send: SendScreen(b, dev); break;
|
||||
case Screen.Review: ReviewScreen(b, dev); break;
|
||||
case Screen.Broadcast: BroadcastScreen(b, dev); break;
|
||||
case Screen.Done: DoneScreen(b, dev); break;
|
||||
default: HomeScreen(b, dev); break;
|
||||
}
|
||||
b.AddFlexibleSpace();
|
||||
if (!string.IsNullOrEmpty(_note)) { b.AddLabel(_note, "note"); }
|
||||
if (!string.IsNullOrEmpty(dev.Note)) b.AddLabel(dev.Note, "note");
|
||||
b.End();
|
||||
b.End();
|
||||
// 机身下沿:左 ✕,右 ✓,左右对称
|
||||
// 机身下沿:左 ×,右 ✓,左右对称
|
||||
b.BeginRow(0f, TextAnchor.MiddleCenter);
|
||||
b.AddIconButton(Items.ModSprite("ui_hw_no"), 60f, Reject);
|
||||
b.AddIconButton(Items.ModSprite("ui_hw_no"), 60f, () => Reject(dev));
|
||||
b.BeginPanel(ScreenW + Bezel * 2 - 120f, 60f, TextAnchor.MiddleCenter, 0f, 0f);
|
||||
var plate = b.Last.image;
|
||||
b.End();
|
||||
b.AddIconButton(Items.ModSprite("ui_hw_ok"), 60f, Confirm);
|
||||
b.AddIconButton(Items.ModSprite("ui_hw_ok"), 60f, () => Confirm(dev));
|
||||
var ok = b.Last.root;
|
||||
b.End();
|
||||
var win = b.Show();
|
||||
Log.Guard("硬件钱包外观", () => Skin(win, screen, bezel, paper, plate, ok));
|
||||
Log.Guard("硬件钱包外观", () => Skin(win, screen, bezel, paper, plate, ok, dev));
|
||||
}
|
||||
|
||||
/// <summary>换上硬件的样子:机身石墨色、屏幕是浅色墨水屏、屏上按钮做成黑底白字的触控键。</summary>
|
||||
private static void Skin(CustomUIWindow win, RectTransform screen, Image bezel, Image paper, Image plate, GameObject ok)
|
||||
/// <summary>换上硬件的样子:机身、屏幕边框、墨水屏是自己画的像素九宫格,屏上按钮做成黑底白字的触控键。</summary>
|
||||
private static void Skin(CustomUIWindow win, RectTransform screen, Image bezel, Image paper, Image plate, GameObject ok, Device dev)
|
||||
{
|
||||
if (win?.root == null) return;
|
||||
// 自己画的像素九宫格:圆角石墨机身、黑色屏幕边框、墨水屏;标题栏和分隔线透明,标题当作机身上的刻字
|
||||
Slice(win.root.GetComponent<Image>(), "ui_hw_shell", 10);
|
||||
Slice(bezel, "ui_hw_bezel", 5);
|
||||
Slice(paper, "ui_hw_paper", 3);
|
||||
// 标题栏和分隔线透明,标题当作机身上的刻字
|
||||
foreach (var name in new[] { "header", "rule" })
|
||||
{
|
||||
var img = win.root.transform.Find(name)?.GetComponent<Image>();
|
||||
@@ -146,7 +182,7 @@ namespace Decentralized
|
||||
foreach (var t in btn.GetComponentsInChildren<TextMeshProUGUI>(true)) t.color = KeyText;
|
||||
}
|
||||
// 签名要按住 ✓:在按键上挂一个按住计时
|
||||
if (ok != null && _screen == Screen.Review && ok.GetComponent<HoldToSign>() == null) ok.AddComponent<HoldToSign>();
|
||||
if (ok != null && dev.Screen == Screen.Review && ok.GetComponent<HoldToSign>() == null) ok.AddComponent<HoldToSign>().Device = dev.Address;
|
||||
}
|
||||
|
||||
/// <summary>把本 mod 的像素图做成九宫格贴到 Image 上:边角 border 像素不拉伸,一个像素放大成 3 个界面单位。</summary>
|
||||
@@ -166,21 +202,20 @@ namespace Decentralized
|
||||
img.color = Color.white;
|
||||
}
|
||||
|
||||
private static readonly System.Collections.Generic.Dictionary<string, Sprite> Sliced = new System.Collections.Generic.Dictionary<string, Sprite>();
|
||||
private static readonly Dictionary<string, Sprite> Sliced = new Dictionary<string, Sprite>();
|
||||
|
||||
public static string Amt(double v) => v.ToString(v >= 100 ? "0.##" : "0.####", CultureInfo.InvariantCulture);
|
||||
|
||||
/// <summary>账单金额按钱包显示的精度往上取整,照着填就够付。</summary>
|
||||
public static double RoundUp(double v) => v >= 100 ? Math.Ceiling(v * 100 - 1e-6) / 100 : Math.Ceiling(v * 10000 - 1e-6) / 10000;
|
||||
|
||||
private static Wallet W => Session.Data.Hot;
|
||||
private static string Me => Session.Data.Address("me");
|
||||
private static void Go(Device dev, Screen s) { dev.Screen = s; dev.Note = null; Open(dev); }
|
||||
|
||||
private static void HomeScreen(CustomUIBuilder b)
|
||||
private static void HomeScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
var w = W;
|
||||
b.AddHeader(L.T("hw.wallet.main"));
|
||||
b.AddLabel(Me, "addr_dim");
|
||||
var w = dev.Wallet;
|
||||
b.AddHeader(NameOf(w));
|
||||
b.AddLabel(w.Address, "addr_dim");
|
||||
b.AddSeparator();
|
||||
bool any = false;
|
||||
foreach (var kv in w.Balances.OrderBy(k => k.Key))
|
||||
@@ -195,53 +230,58 @@ namespace Decentralized
|
||||
if (Pending != null) b.AddLabel(Pending.Contract ? L.T("hw.pending.sign", Pending.Summary) : L.T("hw.pending.bill", Pending.Summary));
|
||||
b.AddSpace(6f);
|
||||
b.BeginRow();
|
||||
b.AddButton(L.T("hw.send"), () => { _screen = Screen.Send; _note = null; Open(); });
|
||||
if (Pending != null && Pending.Contract) b.AddButton(L.T("hw.review"), () => { _screen = Screen.Review; _note = null; Open(); });
|
||||
b.AddButton(L.T("hw.send"), () => Go(dev, Screen.Send));
|
||||
if (Pending != null && Pending.Contract && w == Session.Data.Hot) b.AddButton(L.T("hw.review"), () => Go(dev, Screen.Review));
|
||||
b.End();
|
||||
}
|
||||
|
||||
private static void SendScreen(CustomUIBuilder b)
|
||||
private static void SendScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
var w = dev.Wallet;
|
||||
b.AddHeader(L.T("hw.send"));
|
||||
b.AddLabel(L.T("hw.to"));
|
||||
b.AddInput("0x", _to, s => _to = s, "to", s => _to = s);
|
||||
b.AddInput("0x", dev.To, s => dev.To = s, "to", s => dev.To = s);
|
||||
b.BeginRow();
|
||||
foreach (var a in SendAssets)
|
||||
{
|
||||
var asset = a;
|
||||
b.AddButton(asset == _asset ? $"[{asset}]" : asset, () => { _asset = asset; Open(); });
|
||||
b.AddButton(asset == dev.Asset ? $"[{asset}]" : asset, () => { dev.Asset = asset; Open(dev); });
|
||||
}
|
||||
b.End();
|
||||
b.AddLabel(L.T("hw.avail", _asset, Amt(W.Avail(_asset))), "avail_dim");
|
||||
b.AddLabel(L.T("hw.avail", dev.Asset, Amt(w.Avail(dev.Asset))), "avail_dim");
|
||||
b.AddLabel(L.T("hw.amount"));
|
||||
b.AddInput("0", _amount, s => _amount = s, "amount", s => _amount = s);
|
||||
b.AddInput("0", dev.Amount, s => dev.Amount = s, "amount", s => dev.Amount = s);
|
||||
if (Pending != null && !Pending.Contract)
|
||||
{
|
||||
b.AddLabel(L.T("hw.bill", Pending.Summary, Amt(Pending.Amount), Pending.Asset), "bill_dim");
|
||||
b.AddButton(L.T("hw.fill"), () => { _asset = Pending.Asset; _amount = Amt(Pending.Amount); Open(); });
|
||||
b.AddButton(L.T("hw.fill"), () => { dev.Asset = Pending.Asset; dev.Amount = Amt(Pending.Amount); Open(dev); });
|
||||
}
|
||||
b.AddLabel(L.T("hw.next"), "hint_dim");
|
||||
}
|
||||
|
||||
private static bool ReadAmount(out double v) =>
|
||||
double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) && v > 0;
|
||||
private static bool ReadAmount(Device dev, out double v) =>
|
||||
double.TryParse((dev.Amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) && v > 0;
|
||||
|
||||
/// <summary>从发送页进核对页之前的检查:地址格式、校验位、余额、收款方是否在等这笔钱。</summary>
|
||||
private static string CheckSend()
|
||||
private static bool IsPoinbase(string to) => ChainAddress.Same(to, Session.Data.Address("poinbase"));
|
||||
|
||||
/// <summary>从发送页进核对页之前的检查:地址格式、校验位、余额、收款方收不收。</summary>
|
||||
private static string CheckSend(Device dev)
|
||||
{
|
||||
string to = ChainAddress.Normalize(_to);
|
||||
var w = dev.Wallet;
|
||||
string to = ChainAddress.Normalize(dev.To);
|
||||
if (!ChainAddress.WellFormed(to)) return L.T("hw.err.format");
|
||||
if (!ChainAddress.Valid(to)) return L.T("hw.err.checksum");
|
||||
if (ChainAddress.Same(to, Me)) return L.T("hw.err.self");
|
||||
if (!ReadAmount(out double v)) return L.T("msg.amount");
|
||||
if (W.Avail(_asset) + 1e-9 < v) return L.T(W.Frozen.ContainsKey(_asset) ? "msg.frozen" : "msg.balance");
|
||||
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");
|
||||
var bill = Match(to);
|
||||
if (bill != null)
|
||||
{
|
||||
if (bill.Asset != _asset) return L.T("hw.err.asset", bill.Asset);
|
||||
if (bill.Asset != dev.Asset) return L.T("hw.err.asset", bill.Asset);
|
||||
if (v + 1e-9 < bill.Amount) return L.T("hw.err.short", Amt(bill.Amount), bill.Asset);
|
||||
}
|
||||
else if (Known(to)) return L.T("hw.err.nobill");
|
||||
else if (IsPoinbase(to)) { if (dev.Asset != Assets.CRC && dev.Asset != Assets.PSC) return L.T("hw.err.poinbase"); }
|
||||
else if (Session.Data.Own(to) == null && Known(to)) return L.T("hw.err.nobill");
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -254,7 +294,7 @@ namespace Decentralized
|
||||
return d.Addresses.Values.Any(a => ChainAddress.Same(a, to)) || d.Black.Sellers.Any(s => ChainAddress.Same(s.Name, to));
|
||||
}
|
||||
|
||||
private static void ReviewScreen(CustomUIBuilder b)
|
||||
private static void ReviewScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
b.AddHeader(L.T("hw.review.h"));
|
||||
if (Pending != null && Pending.Contract)
|
||||
@@ -264,152 +304,172 @@ namespace Decentralized
|
||||
}
|
||||
else
|
||||
{
|
||||
ReadAmount(out double v);
|
||||
string to = ChainAddress.Normalize(_to);
|
||||
ReadAmount(dev, out double v);
|
||||
string to = ChainAddress.Normalize(dev.To);
|
||||
b.AddLabel(L.T("hw.review.to"));
|
||||
b.AddLabel("<size=26><b>" + to + "</b></size>");
|
||||
b.AddLabel(L.T("hw.review.amount", Amt(v), _asset, Fmt.Credits(v * Session.Price(_asset))));
|
||||
b.AddLabel(L.T("hw.review.amount", Amt(v), dev.Asset, Fmt.Credits(v * Session.Price(dev.Asset))));
|
||||
var bill = Match(to);
|
||||
var own = Session.Data.Own(to);
|
||||
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 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), _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");
|
||||
}
|
||||
b.AddSpace(8f);
|
||||
b.AddLabel(L.T("hw.hold"), "hold_dim");
|
||||
}
|
||||
|
||||
private static void BroadcastScreen(CustomUIBuilder b)
|
||||
private static void BroadcastScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
b.AddHeader(L.T("hw.tx.h"));
|
||||
b.AddLabel(StatusText(), "status");
|
||||
b.AddProgressBar(Progress(), "progress");
|
||||
b.AddLabel(StatusText(dev), "status");
|
||||
b.AddProgressBar(Progress(dev), "progress");
|
||||
b.AddSpace(10f);
|
||||
b.AddButton(L.T("hw.skip"), Finish);
|
||||
b.AddButton(L.T("hw.skip"), () => Finish(dev));
|
||||
}
|
||||
|
||||
private static void DoneScreen(CustomUIBuilder b)
|
||||
private static void DoneScreen(CustomUIBuilder b, Device dev)
|
||||
{
|
||||
var tx = _tx;
|
||||
var tx = dev.Tx;
|
||||
b.AddHeader(L.T("hw.done.h"));
|
||||
if (tx != null)
|
||||
{
|
||||
b.AddLabel(L.T("hw.done.block", tx.Block));
|
||||
if (tx.To != null) b.AddLabel(L.T("hw.done.sent", Amt(tx.Amount), tx.Asset, tx.To));
|
||||
var own = tx.To != null ? Session.Data.Own(tx.To) : null;
|
||||
if (tx.Bill != null) b.AddLabel(tx.Bill.Summary);
|
||||
else if (own != null) b.AddLabel(L.T("hw.review.own", NameOf(own)));
|
||||
else if (tx.To != null && IsPoinbase(tx.To)) b.AddLabel(L.T("hw.review.poinbase"));
|
||||
else if (tx.To != null) b.AddLabel(L.T("hw.done.void"), "void_note");
|
||||
}
|
||||
b.AddSpace(8f);
|
||||
b.AddButton(L.T("hw.back"), () => { _screen = Screen.Home; _note = null; _tx = null; Open(); });
|
||||
b.AddButton(L.T("hw.back"), () => { dev.Tx = null; Go(dev, Screen.Home); });
|
||||
}
|
||||
|
||||
private static float Elapsed => _tx == null ? 0 : Time.unscaledTime - _tx.Start;
|
||||
private static float Progress() => Mathf.Clamp01(Elapsed / Phases[Phases.Length - 1]);
|
||||
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 string StatusText()
|
||||
private static string StatusText(Device dev)
|
||||
{
|
||||
float t = Elapsed;
|
||||
if (_tx == null) return "";
|
||||
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", _tx.Block);
|
||||
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", _tx.Block, conf, 3);
|
||||
return L.T("hw.tx.confirm", dev.Tx.Block, conf, 3);
|
||||
}
|
||||
|
||||
/// <summary>左键 ✕:拒绝签名 / 返回上一页。</summary>
|
||||
private static void Reject()
|
||||
/// <summary>左键 ×:拒绝签名 / 返回上一页。</summary>
|
||||
private static void Reject(Device dev)
|
||||
{
|
||||
_note = null;
|
||||
switch (_screen)
|
||||
dev.Note = null;
|
||||
switch (dev.Screen)
|
||||
{
|
||||
case Screen.Review:
|
||||
if (Pending != null && Pending.Contract) { Pending = null; _screen = Screen.Home; TerminalWindow.Refresh(L.T("hw.rejected")); }
|
||||
else _screen = Screen.Send;
|
||||
if (Pending != null && Pending.Contract) { Pending = null; dev.Screen = Screen.Home; TerminalWindow.Refresh(L.T("hw.rejected")); }
|
||||
else dev.Screen = Screen.Send;
|
||||
break;
|
||||
case Screen.Send: _screen = Screen.Home; break;
|
||||
case Screen.Done: _screen = Screen.Home; _tx = null; break;
|
||||
case Screen.Send: dev.Screen = Screen.Home; break;
|
||||
case Screen.Done: dev.Screen = Screen.Home; dev.Tx = null; break;
|
||||
default: break;
|
||||
}
|
||||
Open();
|
||||
Open(dev);
|
||||
}
|
||||
|
||||
/// <summary>右键 ✓:发送页上是"下一步";核对页上要按住(见 <see cref="HoldToSign"/>),单击只提示。</summary>
|
||||
private static void Confirm()
|
||||
private static void Confirm(Device dev)
|
||||
{
|
||||
switch (_screen)
|
||||
switch (dev.Screen)
|
||||
{
|
||||
case Screen.Home: _screen = Screen.Send; _note = null; break;
|
||||
case Screen.Home: dev.Screen = Screen.Send; dev.Note = null; break;
|
||||
case Screen.Send:
|
||||
_note = CheckSend();
|
||||
if (_note == null) _screen = Screen.Review;
|
||||
dev.Note = CheckSend(dev);
|
||||
if (dev.Note == null) dev.Screen = Screen.Review;
|
||||
break;
|
||||
case Screen.Review: _note = L.T("hw.hold"); break;
|
||||
case Screen.Done: _screen = Screen.Home; _tx = null; _note = null; break;
|
||||
case Screen.Review: dev.Note = L.T("hw.hold"); break;
|
||||
case Screen.Done: dev.Screen = Screen.Home; dev.Tx = null; dev.Note = null; break;
|
||||
default: return;
|
||||
}
|
||||
Open();
|
||||
Open(dev);
|
||||
}
|
||||
|
||||
/// <summary>按住 ✓ 满时间:签名并广播。</summary>
|
||||
internal static void Sign()
|
||||
internal static void Sign(string address)
|
||||
{
|
||||
if (_screen != Screen.Review) return;
|
||||
if (!Devices.TryGetValue(address, out var dev) || dev.Screen != Screen.Review || dev.Wallet == null) return;
|
||||
var d = Session.Data;
|
||||
d.Block += 1 + d.Rng.Range(0, 3);
|
||||
if (Pending != null && Pending.Contract)
|
||||
_tx = new Tx { Bill = Pending, Block = d.Block, Start = Time.unscaledTime };
|
||||
dev.Tx = new Tx { Bill = Pending, Block = d.Block, Start = Time.unscaledTime };
|
||||
else
|
||||
{
|
||||
string err = CheckSend();
|
||||
if (err != null) { _note = err; _screen = Screen.Send; Open(); return; }
|
||||
ReadAmount(out double v);
|
||||
string to = ChainAddress.Normalize(_to);
|
||||
_tx = new Tx { To = to, Asset = _asset, Amount = v, Bill = Match(to), Block = d.Block, Start = Time.unscaledTime };
|
||||
string err = CheckSend(dev);
|
||||
if (err != null) { dev.Note = err; dev.Screen = Screen.Send; Open(dev); return; }
|
||||
ReadAmount(dev, out double v);
|
||||
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 };
|
||||
}
|
||||
if (_tx.Bill != null) Pending = null;
|
||||
_screen = Screen.Broadcast;
|
||||
_note = null;
|
||||
Open();
|
||||
if (dev.Tx.Bill != null) Pending = null;
|
||||
dev.Screen = Screen.Broadcast;
|
||||
dev.Note = null;
|
||||
Open(dev);
|
||||
}
|
||||
|
||||
/// <summary>动画走完(或点了跳过):交易落地。付账单的执行终端那边的动作;没人用的地址,币就没了。</summary>
|
||||
internal static void Finish()
|
||||
/// <summary>动画走完(或点了跳过):交易落地。付账单的执行终端那边的动作;转给自己的钱包或 Poinbase 的按比例带上污点;没人用的地址,币就没了。</summary>
|
||||
private static void Finish(Device dev)
|
||||
{
|
||||
var tx = _tx;
|
||||
var tx = dev.Tx;
|
||||
if (tx == null || tx.Finished) return;
|
||||
tx.Finished = true;
|
||||
var d = Session.Data;
|
||||
var w = dev.Wallet;
|
||||
Log.Guard("钱包交易落地", () =>
|
||||
{
|
||||
if (w == null) return;
|
||||
double amt = tx.To != null ? Math.Min(tx.Amount, w.Avail(tx.Asset)) : 0;
|
||||
if (tx.Bill != null && !tx.Bill.Contract)
|
||||
{
|
||||
// 多付的部分对方不退
|
||||
double extra = tx.Amount - tx.Bill.Amount;
|
||||
if (extra > 1e-9) Wallet.Transfer(d.Hot, new Wallet(), tx.Asset, Math.Min(extra, d.Hot.Avail(tx.Asset)), Session.Price);
|
||||
double extra = amt - tx.Bill.Amount;
|
||||
if (extra > 1e-9) Wallet.Transfer(w, new Wallet(), tx.Asset, extra, Session.Price);
|
||||
}
|
||||
if (tx.Bill != null) tx.Bill.Execute();
|
||||
else if (tx.To != null)
|
||||
{
|
||||
Wallet.Transfer(d.Hot, new Wallet(), tx.Asset, Math.Min(tx.Amount, d.Hot.Avail(tx.Asset)), Session.Price);
|
||||
Log.Info($"转进没人用的地址 {tx.To}:{tx.Amount} {tx.Asset}");
|
||||
if (tx.Bill != null) { tx.Bill.Execute(w); return; }
|
||||
if (tx.To == null) return;
|
||||
var own = d.Own(tx.To);
|
||||
Wallet target = own ?? (IsPoinbase(tx.To) ? d.Poinbase : new Wallet());
|
||||
Wallet.Transfer(w, target, tx.Asset, amt, Session.Price);
|
||||
if (own == null && target != d.Poinbase) Log.Info($"转进没人用的地址 {tx.To}:{amt} {tx.Asset}");
|
||||
TerminalWindow.Refresh(null);
|
||||
}
|
||||
if (own != null) RefreshDevice(own);
|
||||
});
|
||||
Session.Save();
|
||||
_screen = Screen.Done;
|
||||
_to = "";
|
||||
if (IsOpen) Open();
|
||||
dev.Screen = Screen.Done;
|
||||
dev.To = "";
|
||||
if (IsOpen(dev)) Open(dev);
|
||||
}
|
||||
|
||||
/// <summary>别的交易让这个钱包的余额变了:开着的话刷新主页。</summary>
|
||||
public static void RefreshDevice(Wallet w)
|
||||
{
|
||||
if (w == null || !Devices.TryGetValue(w.Address, out var dev) || !IsOpen(dev)) return;
|
||||
if (dev.Screen == Screen.Home || dev.Screen == Screen.Send) Open(dev);
|
||||
}
|
||||
|
||||
/// <summary>每帧推进出块动画:只改状态文字和进度条,不重建窗口。</summary>
|
||||
internal static void Tick()
|
||||
{
|
||||
if (_tx == null || _tx.Finished || _screen != Screen.Broadcast) return;
|
||||
if (Elapsed >= Phases[Phases.Length - 1]) { Finish(); return; }
|
||||
if (!IsOpen) return;
|
||||
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 (!IsOpen(dev)) continue;
|
||||
var m = CustomUIManager.Instance;
|
||||
m.GetElement(Id, "status")?.SetText(StatusText());
|
||||
m.GetElement(Id, "progress")?.SetProgress(Progress());
|
||||
m.GetElement(dev.Id, "status")?.SetText(StatusText(dev));
|
||||
m.GetElement(dev.Id, "progress")?.SetProgress(Progress(dev));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,6 +500,7 @@ namespace Decentralized
|
||||
{
|
||||
private const float Seconds = 1.2f;
|
||||
private float held = -1f;
|
||||
public string Device;
|
||||
|
||||
public void OnPointerDown(PointerEventData e) => held = 0f;
|
||||
public void OnPointerUp(PointerEventData e) => Stop();
|
||||
@@ -456,7 +517,7 @@ namespace Decentralized
|
||||
if (held < 0f) return;
|
||||
held += Time.unscaledDeltaTime;
|
||||
CustomUIManager.Instance?.ShowHoldRing(held / Seconds);
|
||||
if (held >= Seconds) { Stop(); Log.Guard("签名", WalletWindow.Sign); }
|
||||
if (held >= Seconds) { Stop(); Log.Guard("签名", () => WalletWindow.Sign(Device)); }
|
||||
}
|
||||
|
||||
private void OnDisable() => Stop();
|
||||
|
||||
@@ -808,6 +808,42 @@ internal static class Program
|
||||
Check(!ChainAddress.Valid("0x12345") && !ChainAddress.Valid("0xzzzzzzzz") && !ChainAddress.WellFormed("12345678ab"), "格式不对的地址不合法");
|
||||
});
|
||||
|
||||
Test("被投毒的钱包:门槛以下不转,越多越快,转空", () =>
|
||||
{
|
||||
Check(Wallet.DrainChance(Tuning.WalletDrainMin - 1) == 0, "门槛以下不会被转走");
|
||||
Check(Math.Abs(Wallet.DrainChance(Tuning.WalletDrainMin) - 1 / 7.0) < 1e-9, "刚到门槛:每晚 1/7(平均 7 天)");
|
||||
Check(Wallet.DrainChance(Tuning.WalletDrainFull) == 1 && Wallet.DrainChance(Tuning.WalletDrainFull * 3) == 1, "到满额当晚就转走");
|
||||
Check(Wallet.DrainChance(10000) > Wallet.DrainChance(5000), "钱越多越快");
|
||||
var w = new Wallet { Address = "a" };
|
||||
w.Add(Assets.PSC, 50); w.Add(Assets.CRT, 1000); w.Taint = 300; w.Basis = 900; w.Freeze(Assets.CRT, 0.5, 9);
|
||||
double v = w.Drain(a => a == Assets.PSC ? 100 : 1);
|
||||
Check(Math.Abs(v - 6000) < 1e-6 && w.Balances.Count == 0 && w.Frozen.Count == 0 && w.Taint == 0 && w.Basis == 0, $"全部转走(含冻结),污点和成本清零:{v}");
|
||||
});
|
||||
|
||||
Test("多钱包:退款、出售回款、混币提回都回到原来的钱包", () =>
|
||||
{
|
||||
Func<string, double> price = a => a == Assets.PSC ? 100 : 1;
|
||||
var a = new Wallet { Address = "0xa" }; var b = new Wallet { Address = "0xb" };
|
||||
a.Add(Assets.CRT, 5000); b.Add(Assets.CRT, 5000);
|
||||
Func<string, Wallet> pick = addr => addr == "0xa" ? a : b;
|
||||
var mx = new Mixer();
|
||||
var dep = mx.Deposit(a, Assets.CRT, 1000, 1, 1, price);
|
||||
Check(dep.Wallet == "0xa", "混币存入记下钱包地址");
|
||||
mx.Night(pick, 2, price, new Rng(3));
|
||||
Check(mx.Pending.Count == 0 ? a.Get(Assets.CRT) > 4900 && b.Get(Assets.CRT) == 5000 : true, $"混币提回 A:A {a.Get(Assets.CRT):0},B {b.Get(Assets.CRT):0}");
|
||||
var bm = new BlackMarket();
|
||||
bm.RefreshOrders(5, 80, new Rng(11), _ => 300);
|
||||
var seller = bm.Sellers.FindIndex(x => !x.Scam);
|
||||
bm.Sellers[seller].Scam = true;
|
||||
var l = bm.Catalog.First(x => x.Seller == seller);
|
||||
double bBefore = b.Get(Assets.CRT);
|
||||
var o = bm.Place(l, 1, PayMode.Escrow, b, Assets.CRT, 5, price);
|
||||
Check(o.Wallet == "0xb" && b.Get(Assets.CRT) < bBefore, "订单记下付款钱包");
|
||||
double aBefore = a.Get(Assets.CRT);
|
||||
for (int d = 6; d < 30 && o.State == OrderState.Pending; d++) bm.Night(d, false, pick, new Rng((ulong)d), 0, price);
|
||||
Check(o.State == OrderState.Refunded && Math.Abs(b.Get(Assets.CRT) - bBefore) < 1e-6 && a.Get(Assets.CRT) == aBefore, $"骗子托管退款回 B:{o.State}");
|
||||
});
|
||||
|
||||
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
|
||||
return _failed == 0 ? 0 : 1;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user