Hardware wallet window: invoices paid from PLEDGER, checksummed addresses, DEX signing, block confirmation animation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
5b7013f240
commit
add61a6b5e
16 files changed
+768
-63
No files matched your search
@@ -2,6 +2,15 @@
|
|||||||
|
|
||||||
## 未发布
|
## 未发布
|
||||||
|
|
||||||
|
### 第五阶段:硬件钱包(进行中)
|
||||||
|
|
||||||
|
- 硬件钱包 PLEDGER:终端顶栏"钱包"打开的独立窗口。像素画的圆角石墨机身、黑色屏幕边框、浅色墨水屏,下沿两个物理按键(左 × 拒绝 / 返回,右 ✓ 下一步,签名要按住 1.2 秒,显示游戏自带的按住圆环)。屏幕页:主钱包(地址、余额、合计、污点)、发送、核对、交易、已确认。
|
||||||
|
- 链上地址改成 "0x" + 6 位 + 2 位校验位(FNV-1a 高 8 位)。输错一位几乎都被拦下("地址校验失败"),约 1/256 碰巧合法;合法但没人用的地址,核对页提示"没有交易记录",签名后币转不回来。旧存档的商家和收款地址载入时换成新格式。
|
||||||
|
- 付款改成账单:黑市现货、商家订单、上架费、改价费、批量上架、混币器、Ponero 兑换在终端开账单(收款地址、币种、金额),"打开钱包转账"在钱包里预填币种和金额,地址要手输。付给有账单的地址才执行;币种不对、金额不足在发送页拦下;多付的部分不退;对有往来但没有账单的地址拒绝发送。账单不随切换终端页面作废,可以手动作废。
|
||||||
|
- 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,当天额度用完。没有在游戏里验证:混币器账单(测试存档没解锁混币器)、商家订单、上架 / 改价费账单(代码路径和现货相同)。
|
||||||
|
|
||||||
### 第四阶段:Web3 黑市与 Ponero(进行中)
|
### 第四阶段:Web3 黑市与 Ponero(进行中)
|
||||||
|
|
||||||
- 人脉网络新增"Web3 黑市"(前置:Web3 治理社区,1500):黑市派系在链上的实体;放在终端 Web3 页的"黑市"子页里,需要黑市声望 ≥ 10,镇压期间下线。
|
- 人脉网络新增"Web3 黑市"(前置:Web3 治理社区,1500):黑市派系在链上的实体;放在终端 Web3 页的"黑市"子页里,需要黑市声望 ≥ 10,镇压期间下线。
|
||||||
|
|||||||
@@ -41,6 +41,9 @@ C = {
|
|||||||
'n': (0x1E, 0x1B, 0x25), # 近黑
|
'n': (0x1E, 0x1B, 0x25), # 近黑
|
||||||
'x': (0x4A, 0x4E, 0x58), # 石墨亮边
|
'x': (0x4A, 0x4E, 0x58), # 石墨亮边
|
||||||
'z': (0x2D, 0x2F, 0x38), # 石墨暗
|
'z': (0x2D, 0x2F, 0x38), # 石墨暗
|
||||||
|
'P': (0xD8, 0xDA, 0xD0), # 墨水屏
|
||||||
|
'K': (0x3C, 0x3F, 0x48), # 石墨机身
|
||||||
|
'E': (0x5A, 0x5E, 0x6A), # 机身受光边
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -617,7 +620,57 @@ for _tier in range(4):
|
|||||||
SPRITES['gpu_probidia_f_t%d' % _tier] = (lambda t=_tier: gpu_front('G', 'g2', tier=t))
|
SPRITES['gpu_probidia_f_t%d' % _tier] = (lambda t=_tier: gpu_front('G', 'g2', tier=t))
|
||||||
SPRITES['gpu_pmd_f_t%d' % _tier] = (lambda t=_tier: gpu_front('R', 'r2', tier=t))
|
SPRITES['gpu_pmd_f_t%d' % _tier] = (lambda t=_tier: gpu_front('R', 'r2', tier=t))
|
||||||
|
|
||||||
|
def hw_button(symbol):
|
||||||
|
"""硬件钱包的物理按键(24×24):石墨外圈、左上受光的金属环,中间凹下去的按键面,刻着 ✓ 或 ✕。"""
|
||||||
|
c = Canvas(24, 24)
|
||||||
|
for y in range(24):
|
||||||
|
for x in range(24):
|
||||||
|
d = ((x - 11.5) ** 2 + (y - 11.5) ** 2) ** 0.5
|
||||||
|
if d <= 11.5:
|
||||||
|
c.put(x, y, 'x' if x + y < 23 else 'z') # 外圈:左上亮、右下暗
|
||||||
|
if d <= 9.5:
|
||||||
|
c.put(x, y, 'k' if x + y < 23 else 'n') # 凹槽:反过来,左上暗、右下亮一点
|
||||||
|
if d <= 8.5:
|
||||||
|
c.put(x, y, 'z') # 按键面
|
||||||
|
mark = 'G' if symbol == 'ok' else 'R'
|
||||||
|
if symbol == 'ok':
|
||||||
|
pts = [(7, 12), (8, 13), (9, 14), (10, 15), (11, 14), (12, 13), (13, 12), (14, 11), (15, 10), (16, 9)]
|
||||||
|
else:
|
||||||
|
pts = [(8 + k, 8 + k) for k in range(8)] + [(15 - k, 8 + k) for k in range(8)]
|
||||||
|
for (x, y) in pts:
|
||||||
|
c.put(x, y, mark); c.put(x, y + 1, mark)
|
||||||
|
outline(c)
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
|
def rounded(size, r, fill, light=None, dark=None):
|
||||||
|
"""圆角矩形(九宫格用):圆角半径 r,外轮廓 O;可选左上一圈受光、右下一圈背光。"""
|
||||||
|
c = Canvas(size, size)
|
||||||
|
def inside(x, y, rr):
|
||||||
|
cx = min(max(x, rr), size - 1 - rr); cy = min(max(y, rr), size - 1 - rr)
|
||||||
|
return (x - cx) ** 2 + (y - cy) ** 2 <= rr * rr + rr * 0.6
|
||||||
|
for y in range(size):
|
||||||
|
for x in range(size):
|
||||||
|
if inside(x, y, r):
|
||||||
|
c.put(x, y, fill)
|
||||||
|
if light or dark:
|
||||||
|
for y in range(size):
|
||||||
|
for x in range(size):
|
||||||
|
if c.px[y][x] is None:
|
||||||
|
continue
|
||||||
|
edge = any(not (0 <= x + dx < size and 0 <= y + dy < size) or c.px[y + dy][x + dx] is None for dx, dy in ((2, 0), (-2, 0), (0, 2), (0, -2)))
|
||||||
|
if edge:
|
||||||
|
c.put(x, y, light if x + y < size - 1 else dark)
|
||||||
|
outline(c)
|
||||||
|
return c
|
||||||
|
|
||||||
|
|
||||||
UI_ICONS = {'ui_bank': ui_bank, 'ui_poinbase': ui_poinbase, 'ui_web3': ui_web3}
|
UI_ICONS = {'ui_bank': ui_bank, 'ui_poinbase': ui_poinbase, 'ui_web3': ui_web3}
|
||||||
|
UI_ICONS['ui_hw_shell'] = lambda: rounded(32, 7, 'K', 'E', 'z')
|
||||||
|
UI_ICONS['ui_hw_bezel'] = lambda: rounded(16, 4, 'n')
|
||||||
|
UI_ICONS['ui_hw_paper'] = lambda: rounded(12, 2, 'P')
|
||||||
|
UI_ICONS['ui_hw_ok'] = lambda: hw_button('ok')
|
||||||
|
UI_ICONS['ui_hw_no'] = lambda: hw_button('no')
|
||||||
|
|
||||||
# 叠加图:和底图同尺寸,位置 (0,0) 对齐,只画变化的像素,不参与判格
|
# 叠加图:和底图同尺寸,位置 (0,0) 对齐,只画变化的像素,不参与判格
|
||||||
OVERLAYS = {
|
OVERLAYS = {
|
||||||
|
|||||||
@@ -198,7 +198,7 @@ namespace Decentralized.Domain
|
|||||||
double hi = t == MerchantType.Elite ? Tuning.BmFactorEliteMax : t == MerchantType.GpuNew ? Tuning.BmFactorNewMax : t == MerchantType.GpuUsed ? Tuning.BmFactorUsedMax : t == MerchantType.Bulk ? Tuning.BmFactorBulkMax : Tuning.BmFactorMax;
|
double hi = t == MerchantType.Elite ? Tuning.BmFactorEliteMax : t == MerchantType.GpuNew ? Tuning.BmFactorNewMax : t == MerchantType.GpuUsed ? Tuning.BmFactorUsedMax : t == MerchantType.Bulk ? Tuning.BmFactorBulkMax : Tuning.BmFactorMax;
|
||||||
var s = new BmSeller
|
var s = new BmSeller
|
||||||
{
|
{
|
||||||
Name = "0x" + rng.NextUInt().ToString("x8").Substring(0, 6),
|
Name = ChainAddress.Make(rng.NextUInt()),
|
||||||
Scam = scam, Type = t, Factor = personal ? 1.0 : F(rng, lo, hi),
|
Scam = scam, Type = t, Factor = personal ? 1.0 : F(rng, lo, hi),
|
||||||
Trades = fresh ? rng.Range(0, Tuning.BmFreshTradesMax) : rng.Range(Tuning.BmOldTradesMin, Tuning.BmOldTradesMax),
|
Trades = fresh ? rng.Range(0, Tuning.BmFreshTradesMax) : rng.Range(Tuning.BmOldTradesMin, Tuning.BmOldTradesMax),
|
||||||
Rating = scam && !fresh ? rng.Range(Tuning.BmScamOldRatingMin, Tuning.BmScamOldRatingMax) : fresh ? rng.Range(Tuning.BmFreshRatingMin, Tuning.BmFreshRatingMax) : rng.Range(Tuning.BmOldRatingMin, Tuning.BmOldRatingMax),
|
Rating = scam && !fresh ? rng.Range(Tuning.BmScamOldRatingMin, Tuning.BmScamOldRatingMax) : fresh ? rng.Range(Tuning.BmFreshRatingMin, Tuning.BmFreshRatingMax) : rng.Range(Tuning.BmOldRatingMin, Tuning.BmOldRatingMax),
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
namespace Decentralized.Domain
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 链上地址:"0x" + 6 位随机十六进制 + 2 位校验位。手输地址时输错一位,校验位几乎总能发现(约 1/256 恰好也合法,币就转进了没人用的地址)。
|
||||||
|
/// </summary>
|
||||||
|
public static class ChainAddress
|
||||||
|
{
|
||||||
|
public const int BodyLength = 6, CheckLength = 2, Length = 2 + BodyLength + CheckLength;
|
||||||
|
|
||||||
|
public static string Make(uint r)
|
||||||
|
{
|
||||||
|
string body = (r & 0xFFFFFF).ToString("x6");
|
||||||
|
return "0x" + body + Check(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>校验位:地址主体的 FNV-1a 散列取高 8 位(低位对单字符改动是一一对应的,永远不会碰巧合法)。</summary>
|
||||||
|
public static string Check(string body)
|
||||||
|
{
|
||||||
|
uint h = 2166136261;
|
||||||
|
foreach (char c in body) { h ^= c; h *= 16777619; }
|
||||||
|
return (h >> 24).ToString("x2");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string Normalize(string s) => (s ?? "").Trim().ToLowerInvariant();
|
||||||
|
|
||||||
|
/// <summary>格式对(0x 开头、十六进制、长度对)。</summary>
|
||||||
|
public static bool WellFormed(string s)
|
||||||
|
{
|
||||||
|
s = Normalize(s);
|
||||||
|
if (s.Length != Length || !s.StartsWith("0x")) return false;
|
||||||
|
for (int i = 2; i < s.Length; i++)
|
||||||
|
if (!(s[i] >= '0' && s[i] <= '9' || s[i] >= 'a' && s[i] <= 'f')) return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>格式对且校验位对得上。</summary>
|
||||||
|
public static bool Valid(string s)
|
||||||
|
{
|
||||||
|
s = Normalize(s);
|
||||||
|
return WellFormed(s) && Check(s.Substring(2, BodyLength)) == s.Substring(2 + BodyLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool Same(string a, string b) => Normalize(a) == Normalize(b);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@ namespace Decentralized
|
|||||||
CoinCustomers.Unsubscribe();
|
CoinCustomers.Unsubscribe();
|
||||||
BlackDelivery.Unsubscribe();
|
BlackDelivery.Unsubscribe();
|
||||||
GpuInspectView.Remove();
|
GpuInspectView.Remove();
|
||||||
|
WalletDriver.Remove();
|
||||||
ModHook.OnGameLoadedLate -= Session.Load;
|
ModHook.OnGameLoadedLate -= Session.Load;
|
||||||
ModHook.OnNewGameStarted -= OnNewGame;
|
ModHook.OnNewGameStarted -= OnNewGame;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -332,13 +332,9 @@ merchant.Luxury,Luxury
|
|||||||
black.kind.Goods,Goods
|
black.kind.Goods,Goods
|
||||||
merchant.Elite,Elite shop
|
merchant.Elite,Elite shop
|
||||||
merchant.Personal,Private seller
|
merchant.Personal,Private seller
|
||||||
black.pay.confirm,Confirm payment
|
black.pay.confirm,Invoice
|
||||||
black.pay.item,"{0} {1} credits"
|
|
||||||
black.pay.to,"Payee address: {0}"
|
black.pay.to,"Payee address: {0}"
|
||||||
black.pay.input,Type the payee address
|
black.pay.cancel,Void
|
||||||
black.pay.go,Release payment
|
|
||||||
black.pay.cancel,Cancel
|
|
||||||
black.pay.bad,Address does not match; nothing paid.
|
|
||||||
pay.mix,"Mixer {0} {1}"
|
pay.mix,"Mixer {0} {1}"
|
||||||
pay.swap,"Ponero desk {0} {1}"
|
pay.swap,"Ponero desk {0} {1}"
|
||||||
pay.listfee," (listing fee x{0})"
|
pay.listfee," (listing fee x{0})"
|
||||||
@@ -356,3 +352,47 @@ black.platform.line,"Platform spot instant delivery payee {0}"
|
|||||||
black.orders.in,Bought
|
black.orders.in,Bought
|
||||||
black.orders.out,Selling
|
black.orders.out,Selling
|
||||||
black.desk.pair,"PONERO [anonymous coin] price {0}"
|
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"
|
||||||
|
hw.pending.bill,"Unpaid: {0}"
|
||||||
|
hw.pending.sign,"To sign: {0}"
|
||||||
|
hw.send,Send
|
||||||
|
hw.review,Signature request
|
||||||
|
hw.to,Payee address
|
||||||
|
hw.avail,"Available {1} {0}"
|
||||||
|
hw.amount,Amount
|
||||||
|
hw.bill,"Invoice: {0} {1} {2}"
|
||||||
|
hw.fill,Fill invoice amount
|
||||||
|
hw.next,✓ Next × Back
|
||||||
|
hw.err.format,Malformed address.
|
||||||
|
hw.err.checksum,Address checksum failed.
|
||||||
|
hw.err.self,That is this wallet's own address.
|
||||||
|
hw.err.asset,"The invoice asks for {0}."
|
||||||
|
hw.err.short,"Less than the invoice: {0} {1}."
|
||||||
|
hw.err.nobill,No unpaid invoice for this payee.
|
||||||
|
hw.review.h,Review
|
||||||
|
hw.contract,Contract call
|
||||||
|
hw.review.to,Payee address
|
||||||
|
hw.review.amount,"{0} {1} (about {2} credits)"
|
||||||
|
hw.review.bill,"Invoice: {0}"
|
||||||
|
hw.review.unknown,No transaction history at this address.
|
||||||
|
hw.review.over,"Overpaying {0} {1}; not refunded."
|
||||||
|
hw.hold,Hold ✓ to sign × Reject
|
||||||
|
hw.tx.h,Transaction
|
||||||
|
hw.tx.signing,Signing…
|
||||||
|
hw.tx.mempool,Broadcast; waiting in the mempool…
|
||||||
|
hw.tx.packed,"Included in block #{0}"
|
||||||
|
hw.tx.confirm,"Block #{0} confirmation {1}/{2}"
|
||||||
|
hw.skip,Skip
|
||||||
|
hw.done.h,Confirmed
|
||||||
|
hw.done.block,"Block #{0}"
|
||||||
|
hw.done.sent,"{0} {1} → {2}"
|
||||||
|
hw.done.void,Nobody uses this address. The coins are gone.
|
||||||
|
hw.back,Back
|
||||||
|
hw.rejected,Signature rejected on the wallet.
|
||||||
|
hw.wait,Confirm on the wallet.
|
||||||
|
hw.swap,"Swap {0} {1} → about {2} {3}"
|
||||||
|
@@ -331,13 +331,9 @@ merchant.Luxury,奢侈品
|
|||||||
black.kind.Goods,商品
|
black.kind.Goods,商品
|
||||||
merchant.Elite,尖端店铺
|
merchant.Elite,尖端店铺
|
||||||
merchant.Personal,个人卖家
|
merchant.Personal,个人卖家
|
||||||
black.pay.confirm,确认付款
|
black.pay.confirm,账单
|
||||||
black.pay.item,"{0} {1} 信用点等值"
|
|
||||||
black.pay.to,"收款地址:{0}"
|
black.pay.to,"收款地址:{0}"
|
||||||
black.pay.input,输入收款地址
|
black.pay.cancel,作废
|
||||||
black.pay.go,放款
|
|
||||||
black.pay.cancel,取消
|
|
||||||
black.pay.bad,地址不符,没有付款。
|
|
||||||
pay.mix,"混币器 {0} {1}"
|
pay.mix,"混币器 {0} {1}"
|
||||||
pay.swap,"Ponero 兑换处 {0} {1}"
|
pay.swap,"Ponero 兑换处 {0} {1}"
|
||||||
pay.listfee,"(上架费 ×{0})"
|
pay.listfee,"(上架费 ×{0})"
|
||||||
@@ -355,3 +351,47 @@ black.platform.line,"平台现货 立即到货 收款地址 {0}"
|
|||||||
black.orders.in,买入
|
black.orders.in,买入
|
||||||
black.orders.out,卖出
|
black.orders.out,卖出
|
||||||
black.desk.pair,"PONERO [匿名币] 价格 {0}"
|
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} 信用点"
|
||||||
|
hw.pending.bill,"待付:{0}"
|
||||||
|
hw.pending.sign,"待签名:{0}"
|
||||||
|
hw.send,发送
|
||||||
|
hw.review,签名请求
|
||||||
|
hw.to,收款地址
|
||||||
|
hw.avail,"可用 {1} {0}"
|
||||||
|
hw.amount,金额
|
||||||
|
hw.bill,"账单:{0} {1} {2}"
|
||||||
|
hw.fill,填入账单金额
|
||||||
|
hw.next,✓ 下一步 × 返回
|
||||||
|
hw.err.format,地址格式不对。
|
||||||
|
hw.err.checksum,地址校验失败。
|
||||||
|
hw.err.self,这是本钱包的地址。
|
||||||
|
hw.err.asset,"账单要的是 {0}。"
|
||||||
|
hw.err.short,"金额不足账单:{0} {1}。"
|
||||||
|
hw.err.nobill,对方没有待付账单。
|
||||||
|
hw.review.h,核对
|
||||||
|
hw.contract,合约调用
|
||||||
|
hw.review.to,收款地址
|
||||||
|
hw.review.amount,"{0} {1}(约 {2} 信用点)"
|
||||||
|
hw.review.bill,"账单:{0}"
|
||||||
|
hw.review.unknown,这个地址没有交易记录。
|
||||||
|
hw.review.over,"多付 {0} {1},不退。"
|
||||||
|
hw.hold,按住 ✓ 签名 × 拒绝
|
||||||
|
hw.tx.h,交易
|
||||||
|
hw.tx.signing,签名中…
|
||||||
|
hw.tx.mempool,已广播,内存池等待打包…
|
||||||
|
hw.tx.packed,"已打包,区块 #{0}"
|
||||||
|
hw.tx.confirm,"区块 #{0} 确认 {1}/{2}"
|
||||||
|
hw.skip,跳过
|
||||||
|
hw.done.h,已确认
|
||||||
|
hw.done.block,"区块 #{0}"
|
||||||
|
hw.done.sent,"{0} {1} → {2}"
|
||||||
|
hw.done.void,这个地址没人用,币转不回来了。
|
||||||
|
hw.back,返回
|
||||||
|
hw.rejected,已在钱包上拒绝签名。
|
||||||
|
hw.wait,请在钱包上确认。
|
||||||
|
hw.swap,"兑换 {0} {1} → 约 {2} {3}"
|
||||||
|
Binary file not shown.
|
After Width: | Height: | Size: 111 B |
Binary file not shown.
|
After Width: | Height: | Size: 244 B |
Binary file not shown.
|
After Width: | Height: | Size: 242 B |
Binary file not shown.
|
After Width: | Height: | Size: 92 B |
Binary file not shown.
|
After Width: | Height: | Size: 170 B |
@@ -35,9 +35,11 @@ namespace Decentralized
|
|||||||
|
|
||||||
public string Address(string key)
|
public string Address(string key)
|
||||||
{
|
{
|
||||||
if (!Addresses.TryGetValue(key, out var a)) { a = "0x" + Rng.NextUInt().ToString("x8").Substring(0, 6); Addresses[key] = a; }
|
if (!Addresses.TryGetValue(key, out var a) || !ChainAddress.Valid(a)) { a = ChainAddress.Make(Rng.NextUInt()); Addresses[key] = a; }
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
/// <summary>链上的区块高度:每签一笔交易往上走几块。</summary>
|
||||||
|
public long Block = 1204331;
|
||||||
/// <summary>当天用币付款的成交额(信用点),夜间清零。</summary>
|
/// <summary>当天用币付款的成交额(信用点),夜间清零。</summary>
|
||||||
public double CoinSalesToday;
|
public double CoinSalesToday;
|
||||||
}
|
}
|
||||||
@@ -64,6 +66,8 @@ namespace Decentralized
|
|||||||
catch (Exception ex) { Log.Error($"读取存档数据失败,重新开始:{ex.Message}"); }
|
catch (Exception ex) { Log.Error($"读取存档数据失败,重新开始:{ex.Message}"); }
|
||||||
if (Data == null) Data = new SaveData();
|
if (Data == null) Data = new SaveData();
|
||||||
if (Data.Market == null) Data.Market = Market.CreateDefault(StoreStation.GetDayCounter());
|
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());
|
||||||
Log.Info($"状态已载入(第 {Data.Day} 天,PSC {Data.Market.Price:0.00})");
|
Log.Info($"状态已载入(第 {Data.Day} 天,PSC {Data.Market.Price:0.00})");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,12 +16,9 @@ namespace Decentralized
|
|||||||
private enum Page { Home, Bank, Poinbase, Web3 }
|
private enum Page { Home, Bank, Poinbase, Web3 }
|
||||||
|
|
||||||
private static Page _page = Page.Home;
|
private static Page _page = Page.Home;
|
||||||
private sealed class PendingPay { public string Address, Summary; public int Total; public Action Execute; }
|
|
||||||
private static PendingPay _pay;
|
|
||||||
private sealed class EditPrice { public GameItem Item; public BmSale Sale; public string Id; public long Ref; }
|
private sealed class EditPrice { public GameItem Item; public BmSale Sale; public string Id; public long Ref; }
|
||||||
private static EditPrice _edit;
|
private static EditPrice _edit;
|
||||||
private static string _editPrice = "";
|
private static string _editPrice = "";
|
||||||
private static string _addr = "";
|
|
||||||
private static string _amount = "";
|
private static string _amount = "";
|
||||||
private static string _message;
|
private static string _message;
|
||||||
|
|
||||||
@@ -36,8 +33,9 @@ namespace Decentralized
|
|||||||
{
|
{
|
||||||
var page = p;
|
var page = p;
|
||||||
string name = L.T("tab." + p.ToString().ToLowerInvariant());
|
string name = L.T("tab." + p.ToString().ToLowerInvariant());
|
||||||
b.AddButton(page == _page ? $"[{name}]" : name, () => { _page = page; _message = null; _pay = null; _edit = null; Open(); });
|
b.AddButton(page == _page ? $"[{name}]" : name, () => { _page = page; _message = null; _edit = null; Open(); });
|
||||||
}
|
}
|
||||||
|
b.AddButton(L.T("tab.wallet"), WalletWindow.Open);
|
||||||
b.End();
|
b.End();
|
||||||
b.AddSeparator();
|
b.AddSeparator();
|
||||||
b.BeginScroll(540);
|
b.BeginScroll(540);
|
||||||
@@ -54,7 +52,14 @@ namespace Decentralized
|
|||||||
b.Show();
|
b.Show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void Go(Page p) { _page = p; _message = null; _pay = null; _edit = null; Open(); }
|
private static void Go(Page p) { _page = p; _message = null; _edit = null; Open(); }
|
||||||
|
|
||||||
|
/// <summary>钱包那边交易落地后刷新终端(开着的话),带上结果消息。</summary>
|
||||||
|
public static void Refresh(string message)
|
||||||
|
{
|
||||||
|
if (message != null) _message = message;
|
||||||
|
if (CustomUIManager.Instance != null && CustomUIManager.Instance.IsOpen(Id)) Open();
|
||||||
|
}
|
||||||
|
|
||||||
private static void Entry(CustomUIBuilder b, string icon, string name, string line1, string line2, Page target)
|
private static void Entry(CustomUIBuilder b, string icon, string name, string line1, string line2, Page target)
|
||||||
{
|
{
|
||||||
@@ -241,7 +246,7 @@ namespace Decentralized
|
|||||||
private static void Web3Page(CustomUIBuilder b)
|
private static void Web3Page(CustomUIBuilder b)
|
||||||
{
|
{
|
||||||
var w = Session.Data.Hot;
|
var w = Session.Data.Hot;
|
||||||
if (_pay != null) { PayPage(b); return; }
|
if (WalletWindow.Pending != null && !WalletWindow.Pending.Contract) { PayPage(b); return; }
|
||||||
{
|
{
|
||||||
if (_web3Tab == Web3Tab.Mixer && !Unlock.MixerUnlocked) _web3Tab = Web3Tab.Trade;
|
if (_web3Tab == Web3Tab.Mixer && !Unlock.MixerUnlocked) _web3Tab = Web3Tab.Trade;
|
||||||
if (_web3Tab == Web3Tab.Black && !Unlock.BlackUnlocked) _web3Tab = Web3Tab.Trade;
|
if (_web3Tab == Web3Tab.Black && !Unlock.BlackUnlocked) _web3Tab = Web3Tab.Trade;
|
||||||
@@ -251,7 +256,7 @@ namespace Decentralized
|
|||||||
if ((t == Web3Tab.Mixer && !Unlock.MixerUnlocked) || (t == Web3Tab.Black && !Unlock.BlackUnlocked)) continue;
|
if ((t == Web3Tab.Mixer && !Unlock.MixerUnlocked) || (t == Web3Tab.Black && !Unlock.BlackUnlocked)) continue;
|
||||||
var tab = t;
|
var tab = t;
|
||||||
string name = L.T("web3.sub." + t.ToString().ToLowerInvariant());
|
string name = L.T("web3.sub." + t.ToString().ToLowerInvariant());
|
||||||
b.AddButton(tab == _web3Tab ? $"[{name}]" : name, () => { _web3Tab = tab; _message = null; _pay = null; _edit = null; Open(); });
|
b.AddButton(tab == _web3Tab ? $"[{name}]" : name, () => { _web3Tab = tab; _message = null; _edit = null; Open(); });
|
||||||
}
|
}
|
||||||
b.End();
|
b.End();
|
||||||
if (_web3Tab == Web3Tab.Mining) { ReportPage(b); return; }
|
if (_web3Tab == Web3Tab.Mining) { ReportPage(b); return; }
|
||||||
@@ -312,7 +317,7 @@ namespace Decentralized
|
|||||||
{
|
{
|
||||||
var tab = t;
|
var tab = t;
|
||||||
string name = L.T("black.sub." + t.ToString().ToLowerInvariant());
|
string name = L.T("black.sub." + t.ToString().ToLowerInvariant());
|
||||||
b.AddButton(tab == _blackTab ? $"[{name}]" : name, () => { _blackTab = tab; _message = null; _pay = null; _edit = null; Open(); });
|
b.AddButton(tab == _blackTab ? $"[{name}]" : name, () => { _blackTab = tab; _message = null; _edit = null; Open(); });
|
||||||
}
|
}
|
||||||
b.End();
|
b.End();
|
||||||
if (d.Black.Closed(day)) { b.AddLabel(L.T("black.rugged", d.Black.ClosedUntil - day + 1)); return; }
|
if (d.Black.Closed(day)) { b.AddLabel(L.T("black.rugged", d.Black.ClosedUntil - day + 1)); return; }
|
||||||
@@ -384,7 +389,7 @@ namespace Decentralized
|
|||||||
GoodHead(c, g, L.T("black.stock", offer.Qty));
|
GoodHead(c, g, L.T("black.stock", offer.Qty));
|
||||||
c.AddLabel(L.T("black.price", offer.Price * count) + (count > 1 ? " ×" + count : ""));
|
c.AddLabel(L.T("black.price", offer.Price * count) + (count > 1 ? " ×" + count : ""));
|
||||||
c.AddFlexibleSpace();
|
c.AddFlexibleSpace();
|
||||||
c.AddButton(L.T("black.buy"), () => BeginPay(d.Address("platform"), ItemName(offer.Item) + (count > 1 ? " ×" + count : ""), offer.Price * count, () => BuyOffer(offer, count)));
|
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)); });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -404,7 +409,11 @@ namespace Decentralized
|
|||||||
}
|
}
|
||||||
b.End();
|
b.End();
|
||||||
b.AddLabel(L.T("mixer.balance", _payAsset, Fmt.Num(d.Hot.Avail(_payAsset))) + " " + L.T("black.sell.rating", d.Black.Rating) + " " + L.T("black.sell.slots", d.Black.ActiveListings, Tuning.BmMaxListings));
|
b.AddLabel(L.T("mixer.balance", _payAsset, Fmt.Num(d.Hot.Avail(_payAsset))) + " " + L.T("black.sell.rating", d.Black.Rating) + " " + L.T("black.sell.slots", d.Black.ActiveListings, Tuning.BmMaxListings));
|
||||||
b.AddButton(L.T("black.sell.all"), () => BeginPay(d.Address("platform"), L.T("black.sell.all") + L.T("pay.listfee", _sellMult.ToString("0.0", CultureInfo.InvariantCulture)), BlackDelivery.Sellable().Sum(it => BlackMarket.ListFee(BlackMarket.ListPriceOf(BlackDelivery.RefValue(it), _sellMult))), ListAll));
|
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));
|
||||||
|
});
|
||||||
b.AddSeparator();
|
b.AddSeparator();
|
||||||
var items = BlackDelivery.Sellable();
|
var items = BlackDelivery.Sellable();
|
||||||
if (items.Count == 0) { b.AddLabel(L.T("black.sell.none")); return; }
|
if (items.Count == 0) { b.AddLabel(L.T("black.sell.none")); return; }
|
||||||
@@ -428,7 +437,7 @@ namespace Decentralized
|
|||||||
|
|
||||||
private static readonly System.Collections.Generic.Dictionary<BmGood, GameItem> SellCards = new System.Collections.Generic.Dictionary<BmGood, GameItem>();
|
private static readonly System.Collections.Generic.Dictionary<BmGood, GameItem> SellCards = new System.Collections.Generic.Dictionary<BmGood, GameItem>();
|
||||||
|
|
||||||
private static void ListAll()
|
private static void ListAll(string asset, double mult)
|
||||||
{
|
{
|
||||||
var d = Session.Data;
|
var d = Session.Data;
|
||||||
int n = 0;
|
int n = 0;
|
||||||
@@ -437,7 +446,7 @@ namespace Decentralized
|
|||||||
string id = BlackDelivery.SellId(it);
|
string id = BlackDelivery.SellId(it);
|
||||||
if (id == null) continue;
|
if (id == null) continue;
|
||||||
var u = Items.IsGpu(it) ? Items.ToUnit(it) : null;
|
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), _sellMult, d.Hot, _payAsset, StoreStation.GetDayCounter(), Session.Price);
|
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price);
|
||||||
if (s == null) break;
|
if (s == null) break;
|
||||||
it.Destroy();
|
it.Destroy();
|
||||||
n++;
|
n++;
|
||||||
@@ -447,13 +456,13 @@ namespace Decentralized
|
|||||||
Open();
|
Open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ListItem(GameItem it, double mult)
|
private static void ListItem(GameItem it, double mult, string asset)
|
||||||
{
|
{
|
||||||
var d = Session.Data;
|
var d = Session.Data;
|
||||||
string id = BlackDelivery.SellId(it);
|
string id = BlackDelivery.SellId(it);
|
||||||
if (id == null) { Open(); return; }
|
if (id == null) { Open(); return; }
|
||||||
var u = Items.IsGpu(it) ? Items.ToUnit(it) : null;
|
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, _payAsset, StoreStation.GetDayCounter(), Session.Price);
|
var s = d.Black.List(id, u?.Model, u?.FakeOf, u?.Health ?? 0, BlackDelivery.RefValue(it), mult, d.Hot, 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; }
|
if (s == null) { _message = d.Black.ActiveListings >= Tuning.BmMaxListings ? L.T("black.sell.full", Tuning.BmMaxListings) : L.T("msg.balance"); Open(); return; }
|
||||||
it.Destroy();
|
it.Destroy();
|
||||||
_message = L.T("black.sell.listed", ItemName(id), s.ListPrice);
|
_message = L.T("black.sell.listed", ItemName(id), s.ListPrice);
|
||||||
@@ -483,7 +492,12 @@ namespace Decentralized
|
|||||||
c.AddLabel(L.T("black.price", BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, count), _mode)) + (count > 1 ? " ×" + count : ""));
|
c.AddLabel(L.T("black.price", BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, count), _mode)) + (count > 1 ? " ×" + count : ""));
|
||||||
c.AddFlexibleSpace();
|
c.AddFlexibleSpace();
|
||||||
bool ok = _mode == PayMode.Escrow || seller.Advance;
|
bool ok = _mode == PayMode.Escrow || seller.Advance;
|
||||||
c.AddButton(ok ? L.T("black.order") : L.T("black.noadvance"), () => { if (ok) BeginPay(seller.Name, ItemName(listing.Item) + (count > 1 ? " ×" + count : ""), BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, count), _mode), () => PlaceOrder(listing, count)); });
|
c.AddButton(ok ? L.T("black.order") : L.T("black.noadvance"), () =>
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -518,36 +532,32 @@ namespace Decentralized
|
|||||||
|
|
||||||
private static string ItemName(string id) => BlackDelivery.ItemName(id);
|
private static string ItemName(string id) => BlackDelivery.ItemName(id);
|
||||||
|
|
||||||
private static void BeginPay(string address, string summary, int total, Action execute)
|
/// <summary>开账单:收款地址、币种、金额(按信用点等值折成币),付款在硬件钱包里做。</summary>
|
||||||
|
private static void BeginPay(string address, string summary, string asset, double credits, Action execute) =>
|
||||||
|
BillFor(address, summary, asset, credits / Session.Price(asset), execute);
|
||||||
|
|
||||||
|
private static void BillFor(string address, string summary, string asset, double amount, Action execute)
|
||||||
{
|
{
|
||||||
_pay = new PendingPay { Address = address, Summary = summary, Total = total, Execute = execute };
|
WalletWindow.Pending = new WalletWindow.Bill { Address = address, Summary = summary, Asset = asset, Amount = WalletWindow.RoundUp(amount), Execute = execute };
|
||||||
_addr = "";
|
|
||||||
_message = null;
|
_message = null;
|
||||||
Open();
|
Open();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>付款确认:手输收款地址才放款,输错不付。</summary>
|
/// <summary>待付账单:收款地址和金额,去钱包里转账;也可以作废。</summary>
|
||||||
private static void PayPage(CustomUIBuilder b)
|
private static void PayPage(CustomUIBuilder b)
|
||||||
{
|
{
|
||||||
var p = _pay;
|
var p = WalletWindow.Pending;
|
||||||
b.AddHeader(L.T("black.pay.confirm"));
|
b.AddHeader(L.T("black.pay.confirm"));
|
||||||
b.AddLabel(L.T("black.pay.item", p.Summary, p.Total));
|
b.AddLabel(p.Summary);
|
||||||
b.AddLabel(L.T("black.pay.to", p.Address));
|
b.AddLabel(L.T("black.pay.to", p.Address));
|
||||||
b.BeginPanel(RightW, -1f, TextAnchor.MiddleCenter, 0f, 0f);
|
b.AddLabel(L.T("black.pay.amount", WalletWindow.Amt(p.Amount), p.Asset, Fmt.Credits(p.Amount * Session.Price(p.Asset))));
|
||||||
b.AddInput(L.T("black.pay.input"), _addr, s => _addr = s, "addr", s => _addr = s);
|
|
||||||
b.End();
|
|
||||||
b.BeginRow();
|
b.BeginRow();
|
||||||
b.AddButton(L.T("black.pay.go"), () =>
|
b.AddButton(L.T("black.pay.wallet"), () => WalletWindow.OpenForBill(p));
|
||||||
{
|
b.AddButton(L.T("black.pay.cancel"), () => { WalletWindow.Pending = null; _message = null; Open(); });
|
||||||
if (!string.Equals((_addr ?? "").Trim(), p.Address, StringComparison.OrdinalIgnoreCase)) { _message = L.T("black.pay.bad"); Open(); return; }
|
|
||||||
_pay = null;
|
|
||||||
p.Execute();
|
|
||||||
});
|
|
||||||
b.AddButton(L.T("black.pay.cancel"), () => { _pay = null; _message = null; Open(); });
|
|
||||||
b.End();
|
b.End();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void BuyOffer(BmOffer o, int count)
|
private static void BuyOffer(BmOffer o, int count, string asset)
|
||||||
{
|
{
|
||||||
var d = Session.Data;
|
var d = Session.Data;
|
||||||
string item = o.Item, model = o.Model, fake = o.FakeOf;
|
string item = o.Item, model = o.Model, fake = o.FakeOf;
|
||||||
@@ -555,7 +565,7 @@ namespace Decentralized
|
|||||||
int bought = 0;
|
int bought = 0;
|
||||||
for (int i = 0; i < count; i++)
|
for (int i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
if (!d.Black.Buy(o, d.Hot, _payAsset, Session.Price)) break;
|
if (!d.Black.Buy(o, d.Hot, asset, Session.Price)) break;
|
||||||
Items.Give(BlackDelivery.Make(item, model, fake, health));
|
Items.Give(BlackDelivery.Make(item, model, fake, health));
|
||||||
bought++;
|
bought++;
|
||||||
}
|
}
|
||||||
@@ -565,10 +575,10 @@ namespace Decentralized
|
|||||||
Open();
|
Open();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void PlaceOrder(BmListing l, int count)
|
private static void PlaceOrder(BmListing l, int count, string asset, PayMode mode)
|
||||||
{
|
{
|
||||||
var d = Session.Data;
|
var d = Session.Data;
|
||||||
var o = d.Black.Place(l, count, _mode, d.Hot, _payAsset, StoreStation.GetDayCounter(), Session.Price);
|
var o = d.Black.Place(l, count, mode, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price);
|
||||||
if (o == null) { _message = L.T("msg.balance"); Open(); return; }
|
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);
|
_message = L.T("black.ordered", ItemName(o.Item) + (o.Count > 1 ? " ×" + o.Count : ""), o.DueDay);
|
||||||
Session.Save();
|
Session.Save();
|
||||||
@@ -613,17 +623,18 @@ namespace Decentralized
|
|||||||
var d = Session.Data;
|
var d = Session.Data;
|
||||||
int fee = e.Sale != null ? BlackMarket.RepriceFee(price) : BlackMarket.ListFee(price);
|
int fee = e.Sale != null ? BlackMarket.RepriceFee(price) : BlackMarket.ListFee(price);
|
||||||
_edit = null;
|
_edit = null;
|
||||||
BeginPay(d.Address("platform"), ItemName(e.Id) + L.T(e.Sale != null ? "pay.repricefee" : "pay.listfee", mult.ToString("0.00", CultureInfo.InvariantCulture)), fee,
|
string asset = _payAsset;
|
||||||
() => { if (e.Sale != null) RepriceSale(e.Sale, mult); else ListItem(e.Item, mult); });
|
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); });
|
||||||
});
|
});
|
||||||
b.AddButton(L.T("black.edit.cancel"), () => { _edit = null; _message = null; Open(); });
|
b.AddButton(L.T("black.edit.cancel"), () => { _edit = null; _message = null; Open(); });
|
||||||
b.End();
|
b.End();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void RepriceSale(BmSale s, double mult)
|
private static void RepriceSale(BmSale s, double mult, string asset)
|
||||||
{
|
{
|
||||||
var d = Session.Data;
|
var d = Session.Data;
|
||||||
if (!d.Black.Reprice(s, mult, d.Hot, _payAsset, StoreStation.GetDayCounter(), Session.Price)) { _message = L.T("msg.balance"); Open(); return; }
|
if (!d.Black.Reprice(s, mult, d.Hot, asset, StoreStation.GetDayCounter(), Session.Price)) { _message = L.T("msg.balance"); Open(); return; }
|
||||||
_message = L.T("black.sell.repriced", ItemName(s.Item), s.ListPrice);
|
_message = L.T("black.sell.repriced", ItemName(s.Item), s.ListPrice);
|
||||||
Session.Save();
|
Session.Save();
|
||||||
Open();
|
Open();
|
||||||
@@ -687,15 +698,17 @@ namespace Decentralized
|
|||||||
void Swap()
|
void Swap()
|
||||||
{
|
{
|
||||||
if (!ReadAmount(out double dv)) return;
|
if (!ReadAmount(out double dv)) return;
|
||||||
string asset = _deskIn ? _deskAsset : Assets.Ponero;
|
bool into = _deskIn; string other = _deskAsset;
|
||||||
BeginPay(d.Address("desk"), L.T("pay.swap", Fmt.Num(dv), asset), (int)Math.Round(dv * Session.Price(asset)), DeskSwap);
|
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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void DeskSwap()
|
private static void DeskSwap(double v, bool into, string other)
|
||||||
{
|
{
|
||||||
if (!ReadAmount(out double v)) return;
|
|
||||||
var d = Session.Data;
|
var d = Session.Data;
|
||||||
|
_deskIn = into; _deskAsset = other;
|
||||||
int day = StoreStation.GetDayCounter();
|
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(d.Hot, _deskAsset, v, day, Session.Price) : d.Black.FromPonero(d.Hot, _deskAsset, v, day, Session.Price);
|
||||||
if (got < 0) { _message = L.T("black.desk.fail"); Open(); return; }
|
if (got < 0) { _message = L.T("black.desk.fail"); Open(); return; }
|
||||||
@@ -757,18 +770,22 @@ namespace Decentralized
|
|||||||
double rate = Mixer.RoundRate(v * Session.Price(_mixAsset));
|
double rate = Mixer.RoundRate(v * Session.Price(_mixAsset));
|
||||||
r.AddLabel(L.T("mixer.preview", Fmt.Num(v * Math.Pow(1 - Tuning.MixerFee, _mixRounds)), _mixAsset, (int)Math.Round((1 - Math.Pow(1 - rate, _mixRounds)) * 100), day + _mixRounds));
|
r.AddLabel(L.T("mixer.preview", Fmt.Num(v * Math.Pow(1 - Tuning.MixerFee, _mixRounds)), _mixAsset, (int)Math.Round((1 - Math.Pow(1 - rate, _mixRounds)) * 100), day + _mixRounds));
|
||||||
}
|
}
|
||||||
r.AddButton(L.T("mixer.go"), () => { if (!ReadAmount(out double mv)) return; BeginPay(d.Address("mixer"), L.T("pay.mix", Fmt.Num(mv), _mixAsset), (int)Math.Round(mv * Session.Price(_mixAsset)), MixDeposit); });
|
r.AddButton(L.T("mixer.go"), () =>
|
||||||
|
{
|
||||||
|
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));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void MixDeposit()
|
private static void MixDeposit(double v, string asset, int rounds)
|
||||||
{
|
{
|
||||||
if (!ReadAmount(out double v)) return;
|
|
||||||
var d = Session.Data;
|
var d = Session.Data;
|
||||||
int day = StoreStation.GetDayCounter();
|
int day = StoreStation.GetDayCounter();
|
||||||
if (d.Hot.Avail(_mixAsset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
if (d.Hot.Avail(asset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
||||||
var dep = d.Mixer.Deposit(d.Hot, _mixAsset, v, _mixRounds, day, Session.Price);
|
var dep = d.Mixer.Deposit(d.Hot, asset, v, rounds, day, Session.Price);
|
||||||
_message = dep == null ? L.T("mixer.min", Tuning.MixerMin) : L.T("mixer.done", Fmt.Num(v), _mixAsset, dep.ReadyDay);
|
_message = dep == null ? L.T("mixer.min", Tuning.MixerMin) : L.T("mixer.done", Fmt.Num(v), asset, dep.ReadyDay);
|
||||||
Session.Save();
|
Session.Save();
|
||||||
Open();
|
Open();
|
||||||
}
|
}
|
||||||
@@ -836,9 +853,22 @@ namespace Decentralized
|
|||||||
Open();
|
Open();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>交易:Poinbase 是托管账户,直接成交;DEX 是合约调用,要在硬件钱包上签名。</summary>
|
||||||
private static void Trade(Pool p, bool buy)
|
private static void Trade(Pool p, bool buy)
|
||||||
{
|
{
|
||||||
if (!ReadAmount(out double v)) return;
|
if (!ReadAmount(out double v)) return;
|
||||||
|
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; }
|
||||||
|
if (p.Venue != Venue.Dex) { DoTrade(p, buy, v); 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) });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DoTrade(Pool p, bool buy, double v)
|
||||||
|
{
|
||||||
var w = Session.Data.Hot;
|
var w = Session.Data.Hot;
|
||||||
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
|
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 (w.Avail(pay) + 1e-9 < v) { _message = L.T(frozenPay(w, pay) ? "msg.frozen" : "msg.balance"); Open(); return; }
|
||||||
|
|||||||
@@ -0,0 +1,464 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using System.Linq;
|
||||||
|
using Decentralized.Domain;
|
||||||
|
using TMPro;
|
||||||
|
using UnityEngine;
|
||||||
|
using UnityEngine.EventSystems;
|
||||||
|
using UnityEngine.UI;
|
||||||
|
|
||||||
|
namespace Decentralized
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 硬件钱包:从终端打开的独立窗口,石墨机身、墨水屏、下面两个物理按键(左 ✕ 拒绝,右 ✓ 确认,签名要按住)。
|
||||||
|
/// 链上的转账都从这里发:终端只开账单(收款地址、币种、金额),玩家在钱包里手输地址、核对、按住 ✓ 签名,
|
||||||
|
/// 然后看着交易进内存池、打包、确认。地址带校验位,输错一位几乎都会被拦下;碰巧合法的错地址没人用,币就没了。
|
||||||
|
/// DEX 兑换是合约调用,没有收款地址,只要签名。
|
||||||
|
/// </summary>
|
||||||
|
public static class WalletWindow
|
||||||
|
{
|
||||||
|
public const string Id = "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>
|
||||||
|
public sealed class Bill
|
||||||
|
{
|
||||||
|
public string Address, Asset, Summary;
|
||||||
|
public double Amount;
|
||||||
|
public bool Contract;
|
||||||
|
public Action Execute;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>当前待付的账单(同一时间一张,新的覆盖旧的)。</summary>
|
||||||
|
public static Bill Pending;
|
||||||
|
|
||||||
|
private sealed class Tx
|
||||||
|
{
|
||||||
|
public string To, Asset;
|
||||||
|
public double Amount;
|
||||||
|
public Bill Bill;
|
||||||
|
public long Block;
|
||||||
|
public float Start;
|
||||||
|
public bool Finished;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Screen _screen = Screen.Home;
|
||||||
|
private static string _to = "", _amount = "", _asset = Assets.PSC, _note;
|
||||||
|
private static Tx _tx;
|
||||||
|
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),
|
||||||
|
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);
|
||||||
|
|
||||||
|
/// <summary>终端"打开钱包转账":账单的币种和金额填好,地址留给玩家手输。</summary>
|
||||||
|
public static void OpenForBill(Bill bill)
|
||||||
|
{
|
||||||
|
Pending = bill;
|
||||||
|
if (_tx != null && !_tx.Finished) { Open(); return; }
|
||||||
|
_note = null;
|
||||||
|
if (bill.Contract) _screen = Screen.Review;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_screen = Screen.Send;
|
||||||
|
_to = "";
|
||||||
|
_asset = bill.Asset;
|
||||||
|
_amount = Amt(bill.Amount);
|
||||||
|
}
|
||||||
|
Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Open()
|
||||||
|
{
|
||||||
|
if (Session.Data == null) return;
|
||||||
|
WalletDriver.Ensure();
|
||||||
|
var b = CustomUIManager.Window(Id, "PLEDGER");
|
||||||
|
if (b == null) return;
|
||||||
|
b.SetSize(WinW, WinH).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
|
||||||
|
b.SetPosition(new Vector2(600f, 0f));
|
||||||
|
// 屏幕外面一圈黑色边框,里面是墨水屏
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
b.AddFlexibleSpace();
|
||||||
|
if (!string.IsNullOrEmpty(_note)) { b.AddLabel(_note, "note"); }
|
||||||
|
b.End();
|
||||||
|
b.End();
|
||||||
|
// 机身下沿:左 ✕,右 ✓,左右对称
|
||||||
|
b.BeginRow(0f, TextAnchor.MiddleCenter);
|
||||||
|
b.AddIconButton(Items.ModSprite("ui_hw_no"), 60f, Reject);
|
||||||
|
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);
|
||||||
|
var ok = b.Last.root;
|
||||||
|
b.End();
|
||||||
|
var win = b.Show();
|
||||||
|
Log.Guard("硬件钱包外观", () => Skin(win, screen, bezel, paper, plate, ok));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>换上硬件的样子:机身石墨色、屏幕是浅色墨水屏、屏上按钮做成黑底白字的触控键。</summary>
|
||||||
|
private static void Skin(CustomUIWindow win, RectTransform screen, Image bezel, Image paper, Image plate, GameObject ok)
|
||||||
|
{
|
||||||
|
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>();
|
||||||
|
if (img != null) img.color = new Color(0, 0, 0, 0);
|
||||||
|
}
|
||||||
|
if (win.frame?.titleText != null)
|
||||||
|
{
|
||||||
|
var t = win.frame.titleText;
|
||||||
|
t.color = new Color(0.62f, 0.64f, 0.70f); t.fontStyle = FontStyles.Bold; t.characterSpacing = 18f; t.fontSize = 16f; t.horizontalAlignment = HorizontalAlignmentOptions.Center;
|
||||||
|
}
|
||||||
|
var content = win.frame?.contentRoot?.GetComponent<VerticalLayoutGroup>();
|
||||||
|
if (content != null) content.childAlignment = TextAnchor.UpperCenter;
|
||||||
|
if (plate != null) plate.color = new Color(0, 0, 0, 0);
|
||||||
|
foreach (var t in screen.GetComponentsInChildren<TextMeshProUGUI>(true))
|
||||||
|
{
|
||||||
|
if (t.GetComponentInParent<Button>() != null || t.GetComponentInParent<TMP_InputField>() != null) continue;
|
||||||
|
t.color = t.gameObject.name.Contains("_note") ? Alert : t.gameObject.name.Contains("_dim") ? Dim : Ink;
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
// 签名要按住 ✓:在按键上挂一个按住计时
|
||||||
|
if (ok != null && _screen == Screen.Review && ok.GetComponent<HoldToSign>() == null) ok.AddComponent<HoldToSign>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>把本 mod 的像素图做成九宫格贴到 Image 上:边角 border 像素不拉伸,一个像素放大成 3 个界面单位。</summary>
|
||||||
|
private static void Slice(Image img, string key, int border)
|
||||||
|
{
|
||||||
|
var src = Items.ModSprite(key);
|
||||||
|
if (img == null || src == null) return;
|
||||||
|
if (!Sliced.TryGetValue(key, out var sp))
|
||||||
|
{
|
||||||
|
src.texture.filterMode = FilterMode.Point;
|
||||||
|
sp = Sprite.Create(src.texture, src.rect, new Vector2(0.5f, 0.5f), src.pixelsPerUnit, 0, SpriteMeshType.FullRect, new Vector4(border, border, border, border));
|
||||||
|
Sliced[key] = sp;
|
||||||
|
}
|
||||||
|
img.sprite = sp;
|
||||||
|
img.type = Image.Type.Sliced;
|
||||||
|
img.pixelsPerUnitMultiplier = 100f / (3f * sp.pixelsPerUnit);
|
||||||
|
img.color = Color.white;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static readonly System.Collections.Generic.Dictionary<string, Sprite> Sliced = new System.Collections.Generic.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 HomeScreen(CustomUIBuilder b)
|
||||||
|
{
|
||||||
|
var w = W;
|
||||||
|
b.AddHeader(L.T("hw.wallet.main"));
|
||||||
|
b.AddLabel(Me, "addr_dim");
|
||||||
|
b.AddSeparator();
|
||||||
|
bool any = false;
|
||||||
|
foreach (var kv in w.Balances.OrderBy(k => k.Key))
|
||||||
|
{
|
||||||
|
any = true;
|
||||||
|
b.AddLabel(L.T("hw.balance", kv.Key, Amt(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key))));
|
||||||
|
}
|
||||||
|
if (!any) b.AddLabel(L.T("wallet.empty"));
|
||||||
|
b.AddSeparator();
|
||||||
|
b.AddLabel(L.T("hw.total", Fmt.Credits(w.Value(Session.Price))));
|
||||||
|
if (w.Taint > 0.5) b.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
|
||||||
|
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.End();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SendScreen(CustomUIBuilder b)
|
||||||
|
{
|
||||||
|
b.AddHeader(L.T("hw.send"));
|
||||||
|
b.AddLabel(L.T("hw.to"));
|
||||||
|
b.AddInput("0x", _to, s => _to = s, "to", s => _to = s);
|
||||||
|
b.BeginRow();
|
||||||
|
foreach (var a in SendAssets)
|
||||||
|
{
|
||||||
|
var asset = a;
|
||||||
|
b.AddButton(asset == _asset ? $"[{asset}]" : asset, () => { _asset = asset; Open(); });
|
||||||
|
}
|
||||||
|
b.End();
|
||||||
|
b.AddLabel(L.T("hw.avail", _asset, Amt(W.Avail(_asset))), "avail_dim");
|
||||||
|
b.AddLabel(L.T("hw.amount"));
|
||||||
|
b.AddInput("0", _amount, s => _amount = s, "amount", s => _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.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;
|
||||||
|
|
||||||
|
/// <summary>从发送页进核对页之前的检查:地址格式、校验位、余额、收款方是否在等这笔钱。</summary>
|
||||||
|
private static string CheckSend()
|
||||||
|
{
|
||||||
|
string to = ChainAddress.Normalize(_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");
|
||||||
|
var bill = Match(to);
|
||||||
|
if (bill != null)
|
||||||
|
{
|
||||||
|
if (bill.Asset != _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");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bill Match(string to) => Pending != null && !Pending.Contract && ChainAddress.Same(Pending.Address, to) ? Pending : null;
|
||||||
|
|
||||||
|
/// <summary>有过往来的地址(平台、商家、混币器、兑换处):没有账单时对方不收。</summary>
|
||||||
|
private static bool Known(string to)
|
||||||
|
{
|
||||||
|
var d = Session.Data;
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
b.AddHeader(L.T("hw.review.h"));
|
||||||
|
if (Pending != null && Pending.Contract)
|
||||||
|
{
|
||||||
|
b.AddLabel(L.T("hw.contract"));
|
||||||
|
b.AddLabel(Pending.Summary);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ReadAmount(out double v);
|
||||||
|
string to = ChainAddress.Normalize(_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))));
|
||||||
|
var bill = Match(to);
|
||||||
|
if (bill != null) b.AddLabel(L.T("hw.review.bill", bill.Summary));
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
b.AddSpace(8f);
|
||||||
|
b.AddLabel(L.T("hw.hold"), "hold_dim");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void BroadcastScreen(CustomUIBuilder b)
|
||||||
|
{
|
||||||
|
b.AddHeader(L.T("hw.tx.h"));
|
||||||
|
b.AddLabel(StatusText(), "status");
|
||||||
|
b.AddProgressBar(Progress(), "progress");
|
||||||
|
b.AddSpace(10f);
|
||||||
|
b.AddButton(L.T("hw.skip"), Finish);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DoneScreen(CustomUIBuilder b)
|
||||||
|
{
|
||||||
|
var tx = _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));
|
||||||
|
if (tx.Bill != null) b.AddLabel(tx.Bill.Summary);
|
||||||
|
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(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static float Elapsed => _tx == null ? 0 : Time.unscaledTime - _tx.Start;
|
||||||
|
private static float Progress() => Mathf.Clamp01(Elapsed / Phases[Phases.Length - 1]);
|
||||||
|
|
||||||
|
private static string StatusText()
|
||||||
|
{
|
||||||
|
float t = Elapsed;
|
||||||
|
if (_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);
|
||||||
|
int conf = t < Phases[3] ? 1 : t < Phases[4] ? 2 : 3;
|
||||||
|
return L.T("hw.tx.confirm", _tx.Block, conf, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>左键 ✕:拒绝签名 / 返回上一页。</summary>
|
||||||
|
private static void Reject()
|
||||||
|
{
|
||||||
|
_note = null;
|
||||||
|
switch (_screen)
|
||||||
|
{
|
||||||
|
case Screen.Review:
|
||||||
|
if (Pending != null && Pending.Contract) { Pending = null; _screen = Screen.Home; TerminalWindow.Refresh(L.T("hw.rejected")); }
|
||||||
|
else _screen = Screen.Send;
|
||||||
|
break;
|
||||||
|
case Screen.Send: _screen = Screen.Home; break;
|
||||||
|
case Screen.Done: _screen = Screen.Home; _tx = null; break;
|
||||||
|
default: break;
|
||||||
|
}
|
||||||
|
Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>右键 ✓:发送页上是"下一步";核对页上要按住(见 <see cref="HoldToSign"/>),单击只提示。</summary>
|
||||||
|
private static void Confirm()
|
||||||
|
{
|
||||||
|
switch (_screen)
|
||||||
|
{
|
||||||
|
case Screen.Home: _screen = Screen.Send; _note = null; break;
|
||||||
|
case Screen.Send:
|
||||||
|
_note = CheckSend();
|
||||||
|
if (_note == null) _screen = Screen.Review;
|
||||||
|
break;
|
||||||
|
case Screen.Review: _note = L.T("hw.hold"); break;
|
||||||
|
case Screen.Done: _screen = Screen.Home; _tx = null; _note = null; break;
|
||||||
|
default: return;
|
||||||
|
}
|
||||||
|
Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>按住 ✓ 满时间:签名并广播。</summary>
|
||||||
|
internal static void Sign()
|
||||||
|
{
|
||||||
|
if (_screen != Screen.Review) 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 };
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
if (_tx.Bill != null) Pending = null;
|
||||||
|
_screen = Screen.Broadcast;
|
||||||
|
_note = null;
|
||||||
|
Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>动画走完(或点了跳过):交易落地。付账单的执行终端那边的动作;没人用的地址,币就没了。</summary>
|
||||||
|
internal static void Finish()
|
||||||
|
{
|
||||||
|
var tx = _tx;
|
||||||
|
if (tx == null || tx.Finished) return;
|
||||||
|
tx.Finished = true;
|
||||||
|
var d = Session.Data;
|
||||||
|
Log.Guard("钱包交易落地", () =>
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
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}");
|
||||||
|
TerminalWindow.Refresh(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Session.Save();
|
||||||
|
_screen = Screen.Done;
|
||||||
|
_to = "";
|
||||||
|
if (IsOpen) Open();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <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;
|
||||||
|
var m = CustomUIManager.Instance;
|
||||||
|
m.GetElement(Id, "status")?.SetText(StatusText());
|
||||||
|
m.GetElement(Id, "progress")?.SetProgress(Progress());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>常驻的驱动:每帧推进钱包的出块动画(窗口关着也照样走完)。</summary>
|
||||||
|
public sealed class WalletDriver : MonoBehaviour
|
||||||
|
{
|
||||||
|
private static WalletDriver instance;
|
||||||
|
|
||||||
|
public static void Ensure()
|
||||||
|
{
|
||||||
|
if (instance != null) return;
|
||||||
|
var go = new GameObject("pd_wallet_driver");
|
||||||
|
DontDestroyOnLoad(go);
|
||||||
|
instance = go.AddComponent<WalletDriver>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Remove()
|
||||||
|
{
|
||||||
|
if (instance != null) Destroy(instance.gameObject);
|
||||||
|
instance = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update() => Log.Guard("钱包动画", WalletWindow.Tick);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>✓ 键上的按住计时:按满 1.2 秒签名,鼠标挪开或松手就重来。进度用游戏自带的按住圆环显示。</summary>
|
||||||
|
public sealed class HoldToSign : MonoBehaviour, IPointerDownHandler, IPointerUpHandler, IPointerExitHandler
|
||||||
|
{
|
||||||
|
private const float Seconds = 1.2f;
|
||||||
|
private float held = -1f;
|
||||||
|
|
||||||
|
public void OnPointerDown(PointerEventData e) => held = 0f;
|
||||||
|
public void OnPointerUp(PointerEventData e) => Stop();
|
||||||
|
public void OnPointerExit(PointerEventData e) => Stop();
|
||||||
|
|
||||||
|
private void Stop()
|
||||||
|
{
|
||||||
|
if (held >= 0f) CustomUIManager.Instance?.HideHoldRing();
|
||||||
|
held = -1f;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void Update()
|
||||||
|
{
|
||||||
|
if (held < 0f) return;
|
||||||
|
held += Time.unscaledDeltaTime;
|
||||||
|
CustomUIManager.Instance?.ShowHoldRing(held / Seconds);
|
||||||
|
if (held >= Seconds) { Stop(); Log.Guard("签名", WalletWindow.Sign); }
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDisable() => Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -789,6 +789,25 @@ internal static class Program
|
|||||||
Check(personalListings > 100 && lo < 0.65 && hi > 1.05, $"个人卖家每件价格系数分散:{lo:0.00}–{hi:0.00}");
|
Check(personalListings > 100 && lo < 0.65 && hi > 1.05, $"个人卖家每件价格系数分散:{lo:0.00}–{hi:0.00}");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Test("链上地址校验位", () =>
|
||||||
|
{
|
||||||
|
var rng = new Rng(5);
|
||||||
|
int caught = 0, typos = 0;
|
||||||
|
for (int i = 0; i < 2000; i++)
|
||||||
|
{
|
||||||
|
string a = ChainAddress.Make(rng.NextUInt());
|
||||||
|
Check(ChainAddress.Valid(a) && ChainAddress.Valid(a.ToUpperInvariant().Replace("0X", "0x")) && ChainAddress.Valid(" " + a + " "), "生成的地址合法,大小写、空格不影响:" + a);
|
||||||
|
int pos = 2 + rng.Range(0, ChainAddress.BodyLength + ChainAddress.CheckLength - 1);
|
||||||
|
char c = a[pos];
|
||||||
|
char t = "0123456789abcdef"[(Convert.ToInt32(c.ToString(), 16) + 1 + rng.Range(0, 14)) % 16];
|
||||||
|
string typo = a.Substring(0, pos) + t + a.Substring(pos + 1);
|
||||||
|
typos++;
|
||||||
|
if (!ChainAddress.Valid(typo)) caught++;
|
||||||
|
}
|
||||||
|
Check(caught > typos * 0.98 && caught < typos, $"输错一位几乎都被校验位拦下,极少数碰巧合法:{caught}/{typos}");
|
||||||
|
Check(!ChainAddress.Valid("0x12345") && !ChainAddress.Valid("0xzzzzzzzz") && !ChainAddress.WellFormed("12345678ab"), "格式不对的地址不合法");
|
||||||
|
});
|
||||||
|
|
||||||
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
|
Console.WriteLine($"\n{_passed} 通过,{_failed} 失败");
|
||||||
return _failed == 0 ? 0 : 1;
|
return _failed == 0 ? 0 : 1;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user