Files
probably-decentralized/src/Decentralized/TerminalWindow.cs
T
2026-11-04 08:47:34 +08:00

1727 lines
91 KiB
C#
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Globalization;
using System.Linq;
using UnityEngine;
using Decentralized.Domain;
namespace Decentralized
{
/// <summary>
/// 交易终端窗口:首页三个入口(银行、Poinbase、Web3)加报告。银行是真实的信用点(存取、房租);
/// Poinbase 是合规交易所;Web3 是去中心化交易和链上钱包。每次操作后按当前页重建窗口。
/// </summary>
public static class TerminalWindow
{
public const string Id = "pd_terminal";
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; }
private static EditPrice _edit;
private static string _editPrice = "";
private static string _amount = "";
private static string _message;
public static void Open()
{
if (Session.Data == null) return;
var b = CustomUIManager.Window(Id, L.T("term.title"));
if (b == null) return;
b.SetSize(780, 660).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
b.BeginRow();
foreach (Page p in Enum.GetValues(typeof(Page)))
{
var page = p;
string name = L.T("tab." + p.ToString().ToLowerInvariant());
b.AddButton(page == _page ? $"[{name}]" : name, () => { _page = page; _message = null; _edit = null; Open(); });
}
b.End();
b.AddSeparator();
b.BeginScroll(540);
switch (_page)
{
case Page.Home: HomePage(b); break;
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();
if (!string.IsNullOrEmpty(_message)) b.AddLabel(_message);
b.Show();
}
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)
{
b.BeginPanel(220f, 250f, TextAnchor.UpperCenter, 10f, 6f);
b.AddImage(ModInfo.Id + ":" + icon, 96f, 96f);
b.AddHeader(name);
b.AddLabel(line1);
if (line2 != null) b.AddLabel(line2);
b.AddFlexibleSpace();
b.AddButton(L.T("home.enter"), () => Go(target));
b.End();
}
private static void HomePage(CustomUIBuilder b)
{
var d = Session.Data;
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);
Entry(b, "ui_bank", L.T("entry.bank"), L.T("entry.bank.l1"), L.T("entry.bank.l2", PlayerStore.Instance.playerCash), Page.Bank);
Entry(b, "ui_poinbase", L.T("entry.poinbase"), L.T("entry.poinbase.l1"), null, Page.Poinbase);
Entry(b, "ui_web3", L.T("entry.web3"), L.T("entry.web3.l1"), L.T("entry.web3.l2", Fmt.Credits(chain)), Page.Web3);
b.End();
b.AddSpace(8f);
b.AddSeparator();
var r = d.LastNight;
b.AddLabel(r == null ? L.T("report.none") : L.T("home.lastnight", r.Day, Fmt.Num(r.Mined), r.PoweredRigs, r.Rigs));
}
private const float LeftW = 430f, RightW = 300f;
private static void Cell(CustomUIBuilder b, float w, string text, bool header = false)
{
b.BeginPanel(w, 30f, TextAnchor.MiddleLeft, 6f, 0f);
b.AddLabel(header ? "<b>" + text + "</b>" : text);
b.End();
}
private static void TableRow(CustomUIBuilder b, float[] widths, params string[] cells)
{
b.BeginRow(2f);
for (int i = 0; i < widths.Length; i++) Cell(b, widths[i], i < cells.Length ? cells[i] : "");
b.End();
}
private static void TableHeader(CustomUIBuilder b, float[] widths, params string[] cells)
{
b.BeginRow(2f);
for (int i = 0; i < widths.Length; i++) Cell(b, widths[i], cells[i], true);
b.End();
}
/// <summary>左右分栏:左边是账户与持仓表格,右边是操作区。</summary>
private static void Columns(CustomUIBuilder b, Action<CustomUIBuilder> left, Action<CustomUIBuilder> right)
{
b.BeginRow(10f);
b.BeginPanel(LeftW, -1f, TextAnchor.UpperLeft, 8f, 4f);
left(b);
b.End();
b.BeginPanel(RightW, -1f, TextAnchor.UpperLeft, 8f, 6f);
right(b);
b.End();
b.End();
}
private static void AmountRow(CustomUIBuilder b)
{
b.AddLabel(L.T("trade.amount"));
b.BeginPanel(RightW - 30f, -1f, TextAnchor.MiddleCenter, 0f, 0f); // 给输入框一个固定宽度,容得下大额数字
b.AddInput("0", _amount, s => _amount = s, "amount", s => _amount = s);
b.End();
}
/// <summary>银行:真实的信用点。现金、房租,以及信用点与 Poinbase 账户(CRC)之间的存取。</summary>
private static void BankPage(CustomUIBuilder b)
{
var ps = PlayerStore.Instance;
var w = new[] { 200f, 220f };
Columns(b, l =>
{
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.Poinbase.Get(Assets.CRC)));
}, r =>
{
r.AddHeader(L.T("bank.ops"));
AmountRow(r);
AmlPreview(r);
r.AddButton(L.T("trade.deposit"), Deposit);
r.AddButton(L.T("trade.withdraw"), Withdraw);
});
}
/// <summary>提取前的合规审查提示:本周已提取的累计、污点比例,以及按当前数量估算的审查概率。</summary>
private static void AmlPreview(CustomUIBuilder b)
{
var d = Session.Data;
int day = StoreStation.GetDayCounter();
double rent = Session.Rent();
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)));
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, 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)));
}
}
private static readonly float[] StableW = { 80f, 100f, 220f };
private static string StableStatus(Stablecoins st, string asset, int day)
{
double p = st.Price(asset);
if (asset == Assets.CRA && st.CraWarning(day)) return L.T("stable.warn");
if (asset == Assets.CRA && p < 0.97) return L.T("stable.spiral");
return p < 0.99 ? L.T("stable.depeg") : L.T("stable.ok");
}
/// <summary>稳定币行情:价格与状态(脱锚、预兆)。</summary>
private static void StableTable(CustomUIBuilder b)
{
var st = Session.Data.Stable;
int day = StoreStation.GetDayCounter();
b.AddSeparator();
TableHeader(b, StableW, L.T("col.asset"), L.T("col.peg"), L.T("col.status"));
foreach (var a in new[] { Assets.CRC, Assets.CRT, Assets.CRA })
TableRow(b, StableW, a, Fmt.Num(st.Price(a)), StableStatus(st, a, day));
}
private static void TradeRows(CustomUIBuilder b, Venue venue)
{
foreach (var pool in Session.Data.Market.Pools.Where(x => x.Venue == venue))
{
var p = pool;
b.AddLabel(L.T("trade.pair", p.Base, p.Quote, L.T("venue." + p.Venue)) + " " + L.T("trade.price", Fmt.Num(p.Price)));
b.BeginRow();
b.AddButton(L.T("trade.buy"), () => Trade(p, buy: true));
b.AddButton(L.T("trade.sell"), () => Trade(p, buy: false));
b.End();
}
}
private static readonly float[] HoldW = { 80f, 100f, 110f, 110f };
private static void Holdings(CustomUIBuilder b, Wallet w, Func<string, bool> show)
{
TableHeader(b, HoldW, L.T("col.asset"), L.T("col.amount"), L.T("col.value"), L.T("col.avg"));
bool any = false;
foreach (var kv in w.Balances.OrderBy(k => k.Key).Where(k => show(k.Key)))
{
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(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));
}
/// <summary>Poinbase:合规交易所。左边账户持仓,右边交易;信用点的存取在银行。</summary>
private enum PoinTab { Spot, Perp }
private static PoinTab _poinTab = PoinTab.Spot;
private static void ExchangePage(CustomUIBuilder b, Venue venue)
{
var d = Session.Data;
var w = d.Poinbase;
b.BeginRow();
foreach (PoinTab t in Enum.GetValues(typeof(PoinTab)))
{
var tab = t;
string name = L.T("poin.sub." + t.ToString().ToLowerInvariant());
b.AddButton(tab == _poinTab ? $"[{name}]" : name, () => { _poinTab = tab; _message = null; Open(); });
}
b.End();
if (_poinTab == PoinTab.Perp) { PerpPage(b); return; }
Columns(b, l =>
{
CandleChart.Add(l, "poin", d.Candles.Of(Assets.PSC));
l.AddSeparator();
l.AddHeader(L.T("poin.account.h"));
Holdings(l, w, a => true);
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
l.AddSeparator();
l.BeginRow(8f, TextAnchor.MiddleLeft);
l.AddLabel(L.T("poin.deposit", d.Address("poinbase")));
l.AddButton(L.T("hw.copy"), () => { Clipboard.Copy(d.Address("poinbase")); _message = L.T("hw.copied.addr", d.Address("poinbase")); Open(); });
l.End();
}, r =>
{
r.AddHeader(L.T("trade.h"));
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;
private static string _perpProduct = "PSC";
private static bool _perpLong = true;
private static int _perpLev = 3;
private static readonly float[] PerpW = { 118f, 60f, 104f, 60f, 58f };
private static double PerpSpot(string p) => p == "SPARK" ? Session.Data.Stable.Spark : Session.Data.Market.Price;
/// <summary>永续页:保证金账户、仓位、资金费与保险基金;开仓、追加保证金、平仓。Poinbase 是托管账户,不用签名。</summary>
private static void PerpPage(CustomUIBuilder b)
{
var d = Session.Data;
var bk = d.Perps;
var acct = d.Poinbase;
int day = StoreStation.GetDayCounter();
if (_perpLev > PerpBook.MaxLeverage(_perpProduct)) _perpLev = PerpBook.MaxLeverage(_perpProduct);
Columns(b, l =>
{
l.AddHeader(L.T("perp.h"));
l.AddLabel(L.T("perp.acct", Fmt.Num(acct.Avail(Assets.CRC))));
if (bk.Debt > 0) l.AddLabel(L.T("perp.debt", Fmt.Credits(bk.Debt), Math.Max(0, Tuning.PerpDebtDays - (day - bk.DebtSince))));
if (bk.Banned(day)) l.AddLabel(L.T("perp.banned", bk.BannedUntil - day + 1));
l.AddSeparator();
if (bk.Positions.Count == 0) l.AddLabel(L.T("perp.none"));
else
{
TableHeader(l, PerpW, L.T("perp.col.pos"), L.T("perp.col.notional"), L.T("perp.col.price"), L.T("perp.col.pnl"), L.T("perp.col.liq"));
foreach (var p in bk.Positions)
{
double spot = PerpSpot(p.Product);
TableRow(l, PerpW, p.Product + " " + L.T(p.Long ? "perp.long" : "perp.short") + " " + Math.Round(p.Leverage, 1) + "×", Fmt.Credits(p.Notional(spot)),
Fmt.Num(p.Entry) + " → " + Fmt.Num(spot), Fmt.Credits(p.Pnl(spot)), Fmt.Num(p.LiqPrice));
}
}
l.AddSeparator();
foreach (var p in PerpBook.Products)
l.AddLabel(L.T("perp.rate", p, Fmt.Num(PerpSpot(p)), (bk.Rate(p) * 100).ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture)));
l.AddLabel(L.T("perp.fund", Fmt.Credits(bk.InsuranceFund)));
l.AddSeparator();
CandleChart.Add(l, "perp", d.Candles.Of(Assets.PSC));
}, r =>
{
r.AddHeader(L.T("perp.open"));
r.BeginRow();
foreach (var p in PerpBook.Products)
{
var product = p;
r.AddButton(product == _perpProduct ? $"[{product}]" : product, () => { _perpProduct = product; Open(); });
}
r.End();
r.BeginRow();
r.AddButton(_perpLong ? "[" + L.T("perp.long") + "]" : L.T("perp.long"), () => { _perpLong = true; Open(); });
r.AddButton(!_perpLong ? "[" + L.T("perp.short") + "]" : L.T("perp.short"), () => { _perpLong = false; Open(); });
r.End();
r.BeginRow();
foreach (int lev in new[] { 1, 2, 3, 5 })
{
if (lev > PerpBook.MaxLeverage(_perpProduct)) continue;
int lv = lev;
r.AddButton(lv == _perpLev ? $"[{lv}×]" : lv + "×", () => { _perpLev = lv; Open(); });
}
r.End();
r.AddLabel(L.T("perp.margin"));
AmountRow(r);
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double m) && m > 0)
{
double notional = m * _perpLev, spot = PerpSpot(_perpProduct), exec = bk.Exec(spot, notional, _perpLong, day);
var ghost = new PerpPosition { Long = _perpLong, Entry = exec, Size = notional / exec, Margin = m };
r.AddLabel(L.T("perp.preview", Fmt.Credits(notional), Fmt.Num(exec)));
r.AddLabel(L.T("perp.preview2", Fmt.Num(notional * Tuning.PerpFee), Fmt.Num(ghost.LiqPrice)));
}
r.BeginRow();
r.AddButton(L.T("perp.do.open"), PerpOpen);
r.AddButton(L.T("perp.do.add"), PerpAdd);
r.AddButton(L.T("perp.do.close"), PerpClose);
r.End();
});
}
private static readonly float[] PoolW = { 128f, 50f, 64f, 64f, 50f, 60f };
/// <summary>第三方收益池:日息、储备、时间锁、审计、我的存款,存入 / 取出(合约调用,签名)。是不是骗局看不见。</summary>
private static void YieldPools(CustomUIBuilder l, Wallet w, int day)
{
var sc = Session.Data.Scams;
var pools = sc.Pools.Where(p => !p.Closed || p.Pending.Any(q => ChainAddress.Same(q.Wallet, w.Address))).ToList();
l.AddSeparator();
l.AddHeader(L.T("pool.h"));
if (pools.Count == 0) { l.AddLabel(L.T("pool.none")); return; }
TableHeader(l, PoolW, L.T("pool.col.name"), L.T("pool.col.rate"), L.T("pool.col.reserve"), L.T("pool.col.lock"), L.T("pool.col.audit"), L.T("pool.col.mine"));
foreach (var p in pools)
{
var pp = p;
string mine = Fmt.Num(p.Of(w.Address)) + (p.Pending.Any(q => ChainAddress.Same(q.Wallet, w.Address)) ? "+" + L.T("pool.pending") : "");
TableRow(l, PoolW, p.Name + (p.Closed ? " " + L.T("pool.closed") : ""), (p.Rate * 100).ToString("0", CultureInfo.InvariantCulture) + "%", Fmt.Num(p.Reserve), L.T(p.Timelock ? "pool.yes" : "pool.no"), L.T(p.Audited ? "pool.yes" : "pool.no"), mine);
if (p.Closed) continue;
l.BeginRow(4f);
l.AddButton(L.T("pool.deposit"), () => PoolCall(pp, true));
if (p.Of(w.Address) > 0) l.AddButton(L.T(p.Timelock ? "pool.withdraw.lock" : "pool.withdraw", Tuning.YieldLockDays), () => PoolCall(pp, false));
l.End();
}
}
private static void PoolCall(YieldPool p, bool deposit)
{
var d = Session.Data;
var w = d.Hot;
double v = 0;
if (deposit)
{
if (!ReadAmount(out v)) return;
if (w.Avail(Assets.CRC) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
if (p.Total + v > p.Cap + 1e-9) { _message = L.T("pool.err.cap", Fmt.Num(Math.Max(0, p.Cap - p.Total))); Open(); return; }
}
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = Assets.CRC, Amount = v, Spend = deposit ? v : 0, FeeAsset = deposit ? null : BestFeeAsset(w),
Summary = L.T(deposit ? "pool.sum.in" : "pool.sum.out", p.Name, Fmt.Num(deposit ? v : p.Of(w.Address))),
Execute = pw =>
{
int day = StoreStation.GetDayCounter();
if (deposit) _message = d.Scams.Deposit(p, pw, v, Session.Price) ? L.T("pool.done.in", Fmt.Num(v), p.Name) : L.T("msg.balance");
else
{
double amt = p.Of(pw.Address), got = d.Scams.Withdraw(p, pw, day);
_message = amt <= 0 ? L.T("pool.err.none") : got > 0 ? L.T("pool.done.out", Fmt.Num(got)) : L.T("pool.done.lock", Fmt.Num(amt), day + Tuning.YieldLockDays);
}
Session.Save();
Open();
},
});
}
/// <summary>空投页:可领取的空投(名字、奖励、要求的授权)和当前钱包的授权(可撤销)。领取和撤销都是合约调用,签名时 PLEDGER 上会写明授权范围。</summary>
private static void AirdropPage(CustomUIBuilder b)
{
var d = Session.Data;
var w = d.Hot;
int day = StoreStation.GetDayCounter();
Columns(b, l =>
{
l.AddHeader(L.T("air.h"));
var list = d.Scams.Airdrops.Where(a => !a.Claimed && a.Expire >= day).ToList();
if (list.Count == 0) l.AddLabel(L.T("air.none"));
foreach (var a in list)
{
var ad = a;
l.AddLabel(L.T("air.row", a.Name, Fmt.Num(a.Reward), ApproveText(a.Approve, a.Asset), a.Expire));
l.AddButton(L.T("air.claim"), () => ClaimAirdrop(ad));
}
}, r =>
{
r.AddHeader(L.T("air.approvals", WalletWindow.NameOf(w)));
var aps = d.Scams.Approvals.Where(x => ChainAddress.Same(x.Wallet, w.Address)).ToList();
if (aps.Count == 0) r.AddLabel(L.T("air.approvals.none"));
foreach (var ap in aps)
{
int id = ap.Id;
r.AddLabel(L.T("air.approval", ap.Spender, ApproveText(ap.Limit, ap.Asset)));
r.AddButton(L.T("air.revoke"), () => RevokeApproval(id));
}
});
}
private static string ApproveText(double limit, string asset) =>
limit <= 0 ? L.T("air.noapprove") : double.IsPositiveInfinity(limit) ? L.T("air.unlimited", asset) : L.T("air.limited", asset, Fmt.Num(limit));
private static void ClaimAirdrop(Airdrop a)
{
var d = Session.Data;
_message = L.T("hw.wait");
Open();
var w = d.Hot;
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = Assets.CRC, Amount = 0, Spend = 0, FeeAsset = BestFeeAsset(w),
Summary = L.T("air.sum", a.Name, ApproveText(a.Approve, a.Asset)),
Execute = pw =>
{
var ap = d.Scams.Claim(a, pw, StoreStation.GetDayCounter(), d.Rng, out double got);
_message = L.T("air.claimed", Fmt.Num(got));
Log.Info($"领取空投 {a.Name}:得 {got},授权 {(ap == null ? "无" : ap.Asset + " " + ap.Limit)},骗局 {a.Scam}");
Session.Save();
Open();
},
});
}
private static void RevokeApproval(int id)
{
var d = Session.Data;
var ap = d.Scams.Approvals.Find(x => x.Id == id);
if (ap == null) return;
var w = d.Own(ap.Wallet) ?? d.Hot;
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = Assets.CRC, Amount = 0, Spend = 0, FeeAsset = BestFeeAsset(w),
Summary = L.T("air.revoke.sum", ap.Spender, ap.Asset),
Execute = pw =>
{
_message = ChainAddress.Same(pw.Address, ap.Wallet) && d.Scams.Revoke(id) ? L.T("air.revoked") : L.T("air.err.wallet");
Session.Save();
Open();
},
});
}
private static bool PonziVisible => Session.Data != null && (Session.Data.Ponzi.Open || Session.Data.Ponzi.Done || StoreStation.GetDayCounter() >= Tuning.PonziMinDay);
private static double _ponziRate = 0.04;
private static bool _ponziConfirm;
private static int LowerRep() => StoreReputation.GetStoreReputation(StoreReputation.FACTION_LOWER_LEVEL)?.GetReputation() ?? 0;
/// <summary>理财页:开一个高息理财产品,下层的人往里存钱;池子随时可以卷走,但每晚都有被举报的风险。开张和卷走是链上合约调用,用当前钱包,要签名;之后每晚垫付的利息从发行人钱包出。</summary>
private static void PonziPage(CustomUIBuilder b)
{
var d = Session.Data;
var pz = d.Ponzi;
int day = StoreStation.GetDayCounter();
Columns(b, l =>
{
l.AddHeader(L.T("ponzi.h"));
var issuer = pz.Open ? d.Own(pz.Wallet) ?? d.Hot : d.Hot;
l.AddLabel(L.T("ponzi.acct", WalletWindow.NameOf(issuer), Fmt.Num(issuer.Avail(Assets.CRC))));
l.AddSeparator();
if (pz.Open)
{
l.AddLabel(L.T("ponzi.rate", (pz.Rate * 100).ToString("0", CultureInfo.InvariantCulture)));
l.AddLabel(L.T("ponzi.pool", Fmt.Num(pz.Pool), Fmt.Num(Ponzi.Cap(day))));
l.AddLabel(L.T("ponzi.inflow", Fmt.Num(pz.Inflow)));
l.AddLabel(L.T("ponzi.interest", Fmt.Num(pz.Pool * pz.Rate)));
l.AddLabel(L.T("ponzi.own", Fmt.Num(pz.OwnPaid)));
double risk = Ponzi.ReportChance(pz.Rate, pz.Pool, day);
l.AddLabel(L.T("ponzi.wind", L.T(risk < 0.05 ? "ponzi.wind.low" : risk < 0.10 ? "ponzi.wind.mid" : "ponzi.wind.high")));
}
else l.AddLabel(L.T(pz.Done ? "ponzi.done" : "ponzi.idle", Tuning.PonziMinDay));
}, r =>
{
if (pz.Open)
{
r.AddHeader(L.T("ponzi.ops"));
r.AddButton(_ponziConfirm ? L.T("ponzi.rug.confirm", Fmt.Num(pz.Pool)) : L.T("ponzi.rug", Fmt.Num(pz.Pool)), () =>
{
if (!_ponziConfirm) { _ponziConfirm = true; Open(); return; }
_ponziConfirm = false;
var iw = d.Own(pz.Wallet) ?? d.Hot;
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = Assets.CRC, Amount = 0, Spend = 0, FeeAsset = BestFeeAsset(iw),
Summary = L.T("ponzi.rug.sum", Fmt.Num(pz.Pool)),
Execute = pw =>
{
if (!pz.Open || !ChainAddress.Same(pw.Address, pz.Wallet)) { _message = L.T("ponzi.err.wallet"); Open(); return; }
double take = pz.Rug(pw);
PlayerStore.Instance.secData.CommitCrime("FRAUD", (int)Tuning.PonziCrime);
StoreReputation.GetStoreReputation(StoreReputation.FACTION_LOWER_LEVEL)?.SetReputation(0);
News.Show("pd_news_ponzi_rug_" + day, L.T("news.ponzi.rug.title"), L.T("news.ponzi.rug.desc"), 4);
Log.Info($"卷走理财池 {take:0},自己垫的利息 {pz.OwnPaid:0},钱包 {pw.Address}");
_message = L.T("ponzi.rugged", Fmt.Num(take));
Session.Save();
Open();
},
});
});
return;
}
if (pz.Done) return;
r.AddHeader(L.T("ponzi.open"));
r.BeginRow();
foreach (double rate in Tuning.PonziRates)
{
double rt = rate;
string t = (rate * 100).ToString("0", CultureInfo.InvariantCulture) + "%";
r.AddButton(Math.Abs(rt - _ponziRate) < 1e-9 ? "[" + t + "]" : t, () => { _ponziRate = rt; Open(); });
}
r.End();
r.AddLabel(L.T("ponzi.ad", Fmt.Num(Tuning.PonziAdCost)));
r.AddButton(L.T("ponzi.start"), () =>
{
if (d.Hot.Avail(Assets.CRC) + 1e-9 < Tuning.PonziAdCost) { _message = L.T("ponzi.err"); Open(); return; }
double rate = _ponziRate;
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = Assets.CRC, Amount = Tuning.PonziAdCost,
Summary = L.T("ponzi.start.sum", (rate * 100).ToString("0", CultureInfo.InvariantCulture), Fmt.Num(Tuning.PonziAdCost)),
Execute = pw =>
{
_message = pz.Start(pw, rate, StoreStation.GetDayCounter()) ? L.T("ponzi.started") : L.T("ponzi.err");
Session.Save();
Open();
},
});
});
});
}
private static int _predPick;
private enum PredTab { Markets, Intel }
private static PredTab _predTab = PredTab.Markets;
private static string P2(double x) => x.ToString("0.00", CultureInfo.InvariantCulture);
/// <summary>预测页:盘口(左边列表与最近结果,右边选中盘口的价格、线索、买卖)和情报(情报贩子)。下注和卖出是链上合约调用,用当前钱包的 CRC,要在钱包上签名;持仓记在签名的钱包名下。</summary>
private static void PredictPage(CustomUIBuilder b)
{
if (_predTab == PredTab.Intel && !Unlock.BlackUnlocked) _predTab = PredTab.Markets;
if (Unlock.BlackUnlocked)
{
b.BeginRow();
foreach (PredTab t in Enum.GetValues(typeof(PredTab)))
{
var tab = t;
string name = L.T("predict.tab." + t.ToString().ToLowerInvariant());
b.AddButton(tab == _predTab ? $"[{name}]" : name, () => { _predTab = tab; _message = null; Open(); });
}
b.End();
}
if (_predTab == PredTab.Intel) { IntelPage(b); return; }
var d = Session.Data;
var bk = d.Predict;
var w = d.Hot;
int day = StoreStation.GetDayCounter();
var open = bk.Markets.Where(m => m.Outcome < 0).OrderBy(m => m.IsOpen(day) ? 0 : 1).ThenBy(m => m.FromDay).ThenBy(m => m.Id).ToList();
var sel = open.Find(m => m.Id == _predPick) ?? open.FirstOrDefault();
_predPick = sel?.Id ?? 0;
Columns(b, l =>
{
l.AddHeader(L.T("predict.h"));
l.AddLabel(L.T("predict.acct", WalletWindow.NameOf(w), Fmt.Num(w.Avail(Assets.CRC))));
l.AddSeparator();
// 选中 PSC 价格盘时,在列表上方画日 K 和阈值线
if (sel != null && sel.Kind == PredictKind.Price) { CandleChart.Add(l, "predict", d.Candles.Of(Assets.PSC), sel.Param); l.AddSeparator(); }
if (open.Count == 0) l.AddLabel(L.T("predict.none"));
foreach (var m in open)
{
var mk = m;
var all = bk.PosAll(m.Id);
string q = PredictGame.Question(m);
l.BeginRow(4f);
l.AddButton(m.Id == _predPick ? "[" + q + "]" : q, () => { _predPick = mk.Id; _message = null; Open(); });
l.End();
string state = m.IsOpen(day) ? "" : " " + L.T("predict.closed");
string move = m.PrevP >= 0 && Math.Abs(m.PYes - m.PrevP) >= 0.02 ? L.T("predict.row.move", P2(m.PrevP), m.PYes > m.PrevP ? "↑" : "↓") : "";
l.AddLabel(L.T("predict.row", P2(m.PYes), P2(1 - m.PYes)) + move + (all.Count > 0 ? " " + L.T("predict.held", L.T(all[0].Yes ? "predict.yes" : "predict.no"), Fmt.Num(all.Sum(x => x.Shares)), Fmt.Num(all.Sum(x => x.Cost))) : "") + state);
}
if (bk.Stats.Count > 0)
{
l.AddSeparator();
l.AddLabel(L.T("predict.stats.h"));
foreach (var kv in bk.Stats.OrderByDescending(k => k.Value.Count))
l.AddLabel(L.T("predict.stats.row", L.T("predict.kind." + kv.Key), kv.Value.Count, P2(kv.Value.AvgP0), (kv.Value.HitRate * 100).ToString("0", CultureInfo.InvariantCulture)));
}
if (bk.Recent.Count > 0)
{
l.AddSeparator();
foreach (var r in Enumerable.Reverse(bk.Recent))
l.AddLabel(L.T("predict.result", r.Day, r.Title, r.Outcome == 2 ? L.T("predict.void") : L.T(r.Outcome == 1 ? "predict.yes" : "predict.no"), r.Held ? (r.Pnl >= 0 ? "+" : "") + Fmt.Num(r.Pnl) : "—"));
}
}, r =>
{
if (sel == null) { r.AddHeader(L.T("predict.h")); return; }
r.AddHeader(L.T("predict.pick"));
r.AddLabel(PredictGame.Question(sel));
r.AddLabel(L.T("predict.price", P2(sel.PYes), P2(1 - sel.PYes)));
foreach (var clue in PredictGame.Clues(sel)) r.AddLabel("· " + clue);
foreach (var pos in bk.PosAll(sel.Id))
{
var ow = d.Own(pos.Wallet);
r.AddLabel(L.T("predict.pos", ow != null ? WalletWindow.NameOf(ow) : pos.Wallet, L.T(pos.Yes ? "predict.yes" : "predict.no"), Fmt.Num(pos.Shares)));
if (pos.Wallet == w.Address)
{
r.AddLabel(L.T("predict.pos3", P2(pos.Cost / pos.Shares), P2(sel.PriceOf(pos.Yes))));
r.AddLabel(L.T("predict.pos2", Fmt.Num(pos.Cost), Fmt.Num(pos.Shares)));
r.AddLabel(L.T("predict.pos4", Fmt.Num(sel.SellValue(pos.Yes, pos.Shares) * (1 - Tuning.PredictFee))));
}
}
if (!sel.IsOpen(day)) { r.AddLabel(L.T("predict.closed.note")); return; }
AmountRow(r);
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double v) && v > 0 && v < 1e7)
{
foreach (bool yes in new[] { true, false })
{
double shares = sel.SharesFor(yes, v / (1 + Tuning.PredictFee));
r.AddLabel(L.T("predict.preview", L.T(yes ? "predict.yes" : "predict.no"), Fmt.Num(shares), P2(v / shares), Fmt.Num(shares - v)));
}
}
r.BeginRow();
r.AddButton(L.T("predict.buy.yes"), () => PredictBuy(sel, true));
r.AddButton(L.T("predict.buy.no"), () => PredictBuy(sel, false));
r.End();
r.AddButton(L.T("predict.sell"), () => PredictSell(sel));
});
}
/// <summary>情报页:局势简报,以及对派系行动、矿机时代盘口的内幕消息。付款是 CRT 的合约调用(销毁),要签名。</summary>
private static void IntelPage(CustomUIBuilder b)
{
var d = Session.Data;
var w = d.Hot;
int day = StoreStation.GetDayCounter();
Columns(b, l =>
{
l.AddHeader(L.T("intel.h"));
if (!PredictGame.BrokerOpen) { l.AddLabel(L.T("intel.locked", Tuning.IntelRep)); return; }
l.AddLabel(L.T("predict.acct.crt", WalletWindow.NameOf(w), Fmt.Num(w.Avail(Assets.CRT))));
l.AddButton(L.T("intel.brief.buy", Fmt.Num(Tuning.IntelBriefPrice)), IntelBuyBrief);
l.AddSeparator();
if (d.Brief.Count == 0) l.AddLabel(L.T("intel.brief.none"));
else
{
l.AddLabel(L.T("intel.brief.day", d.BriefDay));
foreach (var line in d.Brief) l.AddLabel(line);
}
}, r =>
{
r.AddHeader(L.T("intel.tips.h"));
if (!PredictGame.BrokerOpen) return;
var list = d.Predict.Markets.Where(m => PredictGame.Tippable(m, day)).OrderBy(m => m.FromDay).ToList();
if (list.Count == 0) r.AddLabel(L.T("intel.tips.none"));
foreach (var m in list)
{
var mk = m;
r.AddLabel(PredictGame.Question(m));
var tip = d.Predict.TipFor(m.Id);
if (tip != null) { r.AddLabel(L.T("predict.clue.tip", L.T("predict.tier." + tip.Tier), L.T(tip.Claim ? "predict.yes" : "predict.no"))); continue; }
for (int t = 0; t < 3; t++)
{
int tier = t;
r.AddButton(L.T("intel.tip.buy", (int)Math.Round(Tuning.IntelTipAcc[t] * 100), Fmt.Num(Tuning.IntelTipPrice[t] * m.MaxLoss)), () => IntelBuyTip(mk, tier));
}
}
});
}
private static void IntelBuyBrief()
{
var d = Session.Data;
double price = Tuning.IntelBriefPrice;
if (d.Hot.Avail(Assets.CRT) + 1e-9 < price) { _message = L.T("intel.err.funds"); Open(); return; }
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = Assets.CRT, Amount = price, Summary = L.T("intel.brief.sum", Fmt.Num(price)),
Execute = pw =>
{
if (pw.Avail(Assets.CRT) + 1e-9 < price) { _message = L.T("intel.err.funds"); Open(); return; }
pw.Spend(Assets.CRT, price, Session.Price);
int day = StoreStation.GetDayCounter();
bool fake = d.Rng.Chance(Tuning.IntelFake);
d.Brief = PredictGame.MakeBrief(fake, day); d.BriefDay = day;
Log.Info($"局势简报:{(fake ? "假情报" : "真")},{string.Join(" | ", d.Brief)}");
_message = L.T("intel.brief.done");
Session.Save();
Open();
},
});
}
private static void IntelBuyTip(PredictMarket m, int tier)
{
var d = Session.Data;
double price = Tuning.IntelTipPrice[tier] * m.MaxLoss;
if (d.Hot.Avail(Assets.CRT) + 1e-9 < price) { _message = L.T("intel.err.funds"); Open(); return; }
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = Assets.CRT, Amount = price, Summary = L.T("intel.tip.sum", Fmt.Num(price), PredictGame.Question(m)),
Execute = pw =>
{
int day = StoreStation.GetDayCounter();
var truth = PredictGame.Truth(m);
if (pw.Avail(Assets.CRT) + 1e-9 < price || truth == null || !m.IsOpen(day) || d.Predict.TipFor(m.Id) != null) { _message = L.T("intel.err.funds"); Open(); return; }
pw.Spend(Assets.CRT, price, Session.Price);
var tip = PredictBook.MakeTip(m, truth.Value, tier, d.Rng);
d.Predict.Tips.Add(tip);
Log.Info($"内幕消息 #{m.Id} 第 {tier + 1} 档:说{(tip.Claim ? "是" : "否")},真相{(truth.Value ? "是" : "否")}");
_message = L.T("intel.tip.done", L.T(tip.Claim ? "predict.yes" : "predict.no"));
Session.Save();
Open();
},
});
}
private static void PredictBuy(PredictMarket m, bool yes)
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
var w = d.Hot;
if (w.Avail(Assets.CRC) + 1e-9 < v) { _message = L.T("predict.err.Funds"); Open(); return; }
if (d.Predict.PosAll(m.Id).Any(x => x.Yes != yes)) { _message = L.T("predict.err.Opposite"); Open(); return; }
string side = L.T(yes ? "predict.yes" : "predict.no");
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = Assets.CRC, Amount = v,
Summary = L.T("predict.sum.buy", PredictGame.Question(m), side, Fmt.Num(v)),
Execute = pw =>
{
int day = StoreStation.GetDayCounter();
bool insider = PredictGame.Known(m, yes);
var res = d.Predict.Buy(pw, m, yes, v, day, Session.Price, insider, out double shares);
_message = res == PredictResult.Ok ? L.T("predict.bought", side, Fmt.Num(shares), Fmt.Num(v)) : L.T("predict.err." + res);
if (res == PredictResult.Ok) { Log.Info($"预测下注 #{m.Id} {(yes ? "是" : "否")} {shares:0.0} 份,花 {v:0.##},钱包 {pw.Address},内幕 {insider}"); Session.Save(); }
Open();
},
});
}
private static void PredictSell(PredictMarket m)
{
var d = Session.Data;
var w = d.Hot;
if (d.Predict.PosOf(m.Id, w.Address) == null) { _message = L.T("predict.err.NoPosition"); Open(); return; }
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = Assets.CRC, Amount = 0, Spend = 0, FeeAsset = BestFeeAsset(w),
Summary = L.T("predict.sum.sell", PredictGame.Question(m)),
Execute = pw =>
{
var res = d.Predict.SellAll(pw, m, StoreStation.GetDayCounter(), d.Rng, out double got, out double fraud);
_message = res == PredictResult.Ok ? L.T("predict.sold", Fmt.Num(got)) : L.T("predict.err." + res);
if (fraud > 0)
{
PlayerStore.Instance.secData.CommitCrime("FRAUD", (int)Math.Round(fraud));
_message = L.T("predict.sold.caught", Fmt.Num(got));
Log.Info($"内幕卖出被查:只退本金 {got:0},罪证 {fraud:0}");
}
if (res == PredictResult.Ok) Session.Save();
Open();
},
});
}
private static string PerpErr(PerpResult res) => L.T("perp.err." + res);
private static void PerpOpen()
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
var res = d.Perps.Open(d.Poinbase, _perpProduct, _perpLong, v, _perpLev, PerpSpot(_perpProduct), StoreStation.GetDayCounter(), Session.Price);
_message = res == PerpResult.Ok ? L.T("perp.opened", _perpProduct, L.T(_perpLong ? "perp.long" : "perp.short"), Fmt.Num(v), _perpLev) : PerpErr(res);
if (res == PerpResult.Ok) Session.Save();
Open();
}
private static void PerpAdd()
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
var res = d.Perps.AddMargin(d.Poinbase, _perpProduct, v, Session.Price);
_message = res == PerpResult.Ok ? L.T("perp.added", Fmt.Num(v)) : PerpErr(res);
if (res == PerpResult.Ok) Session.Save();
Open();
}
private static void PerpClose()
{
var d = Session.Data;
var pos = d.Perps.Of(_perpProduct);
if (pos == null) { _message = PerpErr(PerpResult.NoPosition); Open(); return; }
double margin = pos.Margin;
double paid = d.Perps.Close(d.Poinbase, _perpProduct, PerpSpot(_perpProduct), StoreStation.GetDayCounter(), out double deficit);
// 操纵:平仓盈利,而且盈利方向和玩家自己推价的方向一致,按推价幅度被查:盈利没收,记诈骗罪证,永续账户冻结几天
double push = d.Market.PlayerPush, profit = paid - margin;
bool aligned = _perpProduct == "PSC" && profit > 0 && (pos.Long ? push > 0 : push < 0);
if (aligned && d.Rng.Chance(Market.ManipChance(push)))
{
int day = StoreStation.GetDayCounter();
d.Poinbase.Add(Assets.CRC, -Math.Min(profit, d.Poinbase.Avail(Assets.CRC)));
d.Perps.BannedUntil = day + Tuning.ManipFreezeDays;
PlayerStore.Instance.secData.CommitCrime("FRAUD", (int)Math.Round(Math.Min(Tuning.PredictFraudCap, profit * Tuning.PredictFraudShare)));
News.Show("pd_news_manip_" + day, L.T("news.manip.title"), L.T("news.manip.desc"), 3);
_message = L.T("perp.manip", Fmt.Credits(profit), Tuning.ManipFreezeDays);
Log.Info($"永续操纵被查:盈利 {profit:0} 没收,推价 {push:0.000}");
Session.Save();
Open();
return;
}
_message = L.T("perp.closed", Fmt.Num(paid), (paid - margin >= 0 ? "+" : "") + Fmt.Num(paid - margin)) + (deficit > 0 ? "\n" + L.T("perp.deficit", Fmt.Credits(deficit)) : "");
Session.Save();
Open();
}
/// <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[] { 100f, 110f, 85f, 70f, 375f };
TableHeader(b, cols, L.T("wallets.col.name"), L.T("wallets.col.addr"), L.T("col.value"), L.T("wallets.col.taint"), "");
for (int i = 0; i < d.Wallets.Count; i++)
{
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));
b.AddButton(L.T("hw.copy"), () => { Clipboard.Copy(w.Address); _message = L.T("hw.copied.addr", w.Address); Open(); });
b.AddButton(L.T("wallets.rename"), () => { _renamePick = w.Address; _renameText = w.Label ?? ""; _message = null; Open(); });
if (idx != d.Current) b.AddButton(L.T("wallets.use"), () => { d.Current = idx; Session.Save(); Open(); });
else b.AddLabel(L.T("wallets.current"));
if (idx > 0) b.AddButton(_destroyPick == w.Address ? L.T("wallets.destroy.sure") : L.T("wallets.destroy"), () => DestroyWallet(w));
b.End();
b.End();
b.End();
}
if (_renamePick != null) RenameRow(b);
}
private static string _destroyPick, _renamePick, _renameText = "";
/// <summary>改名:表格下方一行输入框;留空恢复默认名。</summary>
private static void RenameRow(CustomUIBuilder b)
{
var w = Session.Data.Own(_renamePick);
if (w == null) { _renamePick = null; return; }
b.AddSeparator();
b.BeginRow(8f, TextAnchor.MiddleLeft);
b.AddLabel(L.T("wallets.rename.h", w.Address));
b.BeginPanel(220f, -1f, TextAnchor.MiddleLeft, 0f, 0f);
b.AddInput(WalletWindow.NameOf(w), _renameText, s => _renameText = s, "rename", s => _renameText = s);
b.End();
b.AddButton(L.T("wallets.rename.ok"), () =>
{
string name = (_renameText ?? "").Trim();
w.Label = name.Length == 0 ? null : name.Length > 16 ? name.Substring(0, 16) : name;
_renamePick = null;
Session.Save();
WalletWindow.RefreshDevice(w, true);
Open();
});
b.AddButton(L.T("black.edit.cancel"), () => { _renamePick = null; Open(); });
b.End();
}
/// <summary>销毁钱包(主钱包不能销毁):要先把币转走;点两次确认。之后回到这个地址的退款、回款、混币提回改进当前钱包。</summary>
private static void DestroyWallet(Wallet w)
{
var d = Session.Data;
int idx = d.Wallets.IndexOf(w);
if (idx <= 0) return;
if (w.Balances.Values.Any(v => v > 1e-9) || w.Frozen.Values.Any(v => v > 1e-9)) { _destroyPick = null; _message = L.T("wallets.destroy.notempty", WalletWindow.NameOf(w)); Open(); return; }
if (_destroyPick != w.Address) { _destroyPick = w.Address; _message = null; Open(); return; }
_destroyPick = null;
string name = WalletWindow.NameOf(w);
WalletWindow.Close(w);
var current = d.Hot;
d.Wallets.RemoveAt(idx);
d.Current = Math.Max(0, d.Wallets.IndexOf(current));
Log.Info($"销毁硬件钱包 {w.Address}{(w.Poisoned ? "(被投毒)" : "")}");
Session.Save();
_message = L.T("wallets.destroyed", name, w.Address);
Open();
}
/// <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)
{
var w = Session.Data.Hot;
if (WalletWindow.Pending != null && !WalletWindow.Pending.Contract) { PayPage(b); return; }
{
if (_web3Tab == Web3Tab.Mixer && !Unlock.MixerUnlocked) _web3Tab = Web3Tab.Trade;
if (_web3Tab == Web3Tab.Black && !Unlock.BlackUnlocked) _web3Tab = Web3Tab.Trade;
if (_web3Tab == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter())) _web3Tab = Web3Tab.Trade;
if (_web3Tab == Web3Tab.Ponzi && !PonziVisible) _web3Tab = Web3Tab.Trade;
b.BeginRow();
foreach (Web3Tab t in Enum.GetValues(typeof(Web3Tab)))
{
if ((t == Web3Tab.Mixer && !Unlock.MixerUnlocked) || (t == Web3Tab.Black && !Unlock.BlackUnlocked) || (t == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter())) || (t == Web3Tab.Ponzi && !PonziVisible)) continue;
var tab = t;
string name = L.T("web3.sub." + t.ToString().ToLowerInvariant());
b.AddButton(tab == _web3Tab ? $"[{name}]" : name, () => { _web3Tab = tab; _message = null; _edit = null; Open(); });
}
b.End();
if (_web3Tab == Web3Tab.Stake) { StakePage(b); return; }
if (_web3Tab == Web3Tab.Predict) { PredictPage(b); return; }
if (_web3Tab == Web3Tab.Ponzi) { PonziPage(b); return; }
if (_web3Tab == Web3Tab.Airdrop) { AirdropPage(b); return; }
if (_web3Tab == Web3Tab.Mining) { ReportPage(b); return; }
if (_web3Tab == Web3Tab.Mixer) { MixerPage(b); return; }
if (_web3Tab == Web3Tab.Black) { BlackPage(b); return; }
}
Columns(b, l =>
{
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))));
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
StableTable(l);
}, r =>
{
r.AddHeader(L.T("trade.h"));
AmountRow(r);
TradeRows(r, Venue.Dex);
if (Unlock.BlackUnlocked && BlackMarket.DeskOpen(StoreReputation.GetBMReputation())) PoneroBlock(r);
});
}
private enum BlackTab { Buy, Sell, Orders }
private static BlackTab _blackTab = BlackTab.Buy;
private static PayMode _mode = PayMode.Escrow;
private static string _deskAsset = Assets.PSC;
private static bool _deskIn = true;
private static string _payAsset = Assets.CRT;
private static readonly System.Collections.Generic.Dictionary<string, Sprite> IconCache = new System.Collections.Generic.Dictionary<string, Sprite>();
/// <summary>货架上的商品图:游戏物品自己的贴图(本 mod 的显卡用本 mod 的贴图)。</summary>
private static Sprite Icon(string id)
{
if (IconCache.TryGetValue(id, out var cached)) return cached;
Sprite sprite = null;
var it = ModHelper.SpawnItem(BlackDelivery.GameId(id));
if (it != null)
{
try { sprite = it.useSpriteFromMod ? Items.ModSprite(it.spritePath) : RenderHandler.LoadFromAtlas(it.spriteAtlasPath, it.spritePath); }
catch (Exception ex) { Log.Info($"商品图读取失败 {id}:{ex.Message}"); }
it.Destroy();
}
IconCache[id] = sprite;
return sprite;
}
/// <summary>Web3 页里的黑市:黑市派系的链上实体。货架式商城(现货、订单)、我的订单与情报、Ponero 兑换处。</summary>
private static void BlackPage(CustomUIBuilder b)
{
int rep = StoreReputation.GetBMReputation();
int day = StoreStation.GetDayCounter();
var d = Session.Data;
if (Night.ActiveEvents().Contains(MarketEvent.Crackdown)) { b.AddLabel(L.T("black.offline")); return; }
if (!BlackMarket.Open(rep)) { b.AddLabel(L.T("black.closed", Tuning.BmOpenRep, rep)); return; }
b.BeginRow();
foreach (BlackTab t in Enum.GetValues(typeof(BlackTab)))
{
var tab = t;
string name = L.T("black.sub." + t.ToString().ToLowerInvariant());
b.AddButton(tab == _blackTab ? $"[{name}]" : name, () => { _blackTab = tab; _message = null; _edit = null; Open(); });
}
b.End();
if (d.Black.Closed(day)) { b.AddLabel(L.T("black.rugged", d.Black.ClosedUntil - day + 1)); return; }
if (_edit != null) { EditPage(b); return; }
if (_blackTab == BlackTab.Orders) { OrdersPage(b, day); return; }
if (_blackTab == BlackTab.Sell) { SellPage(b, day); return; }
BuyPage(b, d, rep);
}
/// <summary>买货:选一家卖家(第一个是平台现货,立即到货但贵,其余是商家,便宜但要等),选件数,下单。论坛情报在最下面。</summary>
private static void BuyPage(CustomUIBuilder b, SaveData d, int rep)
{
var w = d.Hot;
var sellers = d.Black.Sellers;
if (_merchant >= sellers.Count) _merchant = -1;
bool platform = _merchant < 0;
b.AddLabel(L.T("black.rep", rep, BlackMarket.MaxLevel(rep)) + " " + L.T("mixer.balance", _payAsset, Fmt.Num(w.Avail(_payAsset))) + (w.Taint > 0.5 ? " " + L.T("wallet.taint", Fmt.Credits(w.Taint)) : ""));
b.BeginRow();
foreach (var a in new[] { Assets.PSC, Assets.CRT, Assets.CRA, Assets.Ponero })
{
var asset = a;
b.AddButton(asset == _payAsset ? $"[{asset}]" : asset, () => { _payAsset = asset; Open(); });
}
if (!platform)
foreach (PayMode m in Enum.GetValues(typeof(PayMode)))
{
var mode = m;
string n = L.T("black.mode." + m);
b.AddButton(mode == _mode ? $"[{n}]" : n, () => { _mode = mode; Open(); });
}
b.End();
b.BeginRow();
foreach (var m in Qtys)
{
int q = m;
b.AddButton(q == _qty ? $"[×{q}]" : "×" + q, () => { _qty = q; Open(); });
}
b.End();
var all = new System.Collections.Generic.List<System.Collections.Generic.KeyValuePair<int, string>> { new System.Collections.Generic.KeyValuePair<int, string>(-1, "[" + L.T("merchant.Platform") + "] " + d.Address("platform")) };
for (int k = 0; k < sellers.Count; k++) all.Add(new System.Collections.Generic.KeyValuePair<int, string>(k, MerchantLabel(sellers[k])));
for (int i = 0; i < all.Count; i += 3)
{
b.BeginRow();
for (int k = i; k < Math.Min(all.Count, i + 3); k++)
{
int idx = all[k].Key; string n = all[k].Value;
b.AddButton(idx == _merchant ? "> " + n : n, () => { _merchant = idx; Open(); });
}
b.End();
}
b.AddSeparator();
if (platform) PlatformShelf(b, d);
else MerchantShelf(b, d);
b.AddSeparator();
b.AddHeader(L.T("black.forum.h"));
if (d.Black.Forum.Count == 0) b.AddLabel(L.T("black.forum.quiet"));
foreach (var f in d.Black.Forum)
b.AddLabel(f.StartsWith("phish:") ? L.T("black.forum.phish", f.Substring(6)) : f.StartsWith("scam:") ? L.T("black.forum.scam", BlackDelivery.DisplayName(f.Substring(5))) : f.StartsWith("busted:") ? L.T("black.forum.busted", f.Substring(7)) : L.T("black.forum." + f));
}
private static void PlatformShelf(CustomUIBuilder b, SaveData d)
{
b.AddLabel(L.T("black.platform.line", d.Address("platform")));
if (d.Black.Offers.Count == 0) { b.AddLabel(L.T("black.empty")); return; }
Shelf(b, d.Black.Offers.Cast<BmGood>().ToList(), (c, g) =>
{
var offer = (BmOffer)g;
int count = Math.Min(_qty, offer.Qty);
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, pw => BuyOffer(offer, count, asset, pw)); });
});
}
private static double _sellMult = 1.0;
private static readonly double[] SellMults = { 0.6, 0.8, 0.9, 1.0, 1.2, 1.5, 1.8 };
/// <summary>出售:储藏区里能上架的货做成货架,选标价倍率后上架。</summary>
private static void SellPage(CustomUIBuilder b, int day)
{
var d = Session.Data;
b.BeginRow();
foreach (var m in SellMults)
{
double mult = m;
string n = m.ToString("0.0", CultureInfo.InvariantCulture);
b.AddButton(Math.Abs(mult - _sellMult) < 1e-9 ? $"[×{n}]" : "×" + n, () => { _sellMult = mult; Open(); });
}
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.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))), pw => ListAll(asset, mult, pw));
});
b.AddSeparator();
var items = BlackDelivery.Sellable();
if (items.Count == 0) { b.AddLabel(L.T("black.sell.none")); return; }
Shelf(b, items.Select(it =>
{
string id = BlackDelivery.SellId(it);
var kind = BlackDelivery.KindOf(id);
var g = new BmGood { Item = id, Kind = kind, Level = kind == GoodKind.Gpu ? 2 : Merchants.Info(id).Level };
if (kind == GoodKind.Gpu) { var uu = Items.ToUnit(it); g.Health = uu.Health; g.Model = uu.Model; }
g.Price = BlackMarket.ListPriceOf(BlackDelivery.RefValue(it), _sellMult);
return new { g, it };
}).Select(x => { SellCards[x.g] = x.it; return x.g; }).ToList(), (c, g) =>
{
var it = SellCards[g];
GoodHead(c, g, "");
c.AddLabel(L.T("black.price", g.Price));
c.AddFlexibleSpace();
c.AddButton(L.T("black.sell.list"), () => BeginEdit(new EditPrice { Item = it, Id = g.Item, Ref = BlackDelivery.RefValue(it) }, g.Price));
});
}
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, Wallet pw)
{
var d = Session.Data;
int n = 0;
foreach (var it in BlackDelivery.Sellable())
{
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, pw, asset, StoreStation.GetDayCounter(), Session.Price);
if (s == null) break;
it.Destroy();
n++;
}
_message = L.T("black.sell.listedall", n);
Session.Save();
Open();
}
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, 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);
Session.Save();
Open();
}
private static string MerchantLabel(BmSeller s) => "[" + L.T("merchant." + s.Type) + "] " + s.Name;
private static int _merchant = -1;
private static int _qty = 1;
private static readonly int[] Qtys = { 1, 5, 10 };
/// <summary>商家货架:选一家商家,看它这周的货,选件数下单。</summary>
private static void MerchantShelf(CustomUIBuilder b, SaveData d)
{
var sellers = d.Black.Sellers;
var seller = sellers[_merchant];
b.AddLabel(L.T("black.seller", MerchantLabel(seller), seller.Trades, seller.Rating) + " " + L.T("black.days", seller.Days));
var goods = d.Black.Catalog.Where(l => l.Seller == _merchant).Cast<BmGood>().ToList();
if (goods.Count == 0) { b.AddLabel(L.T("black.empty")); return; }
Shelf(b, goods, (c, g) =>
{
var listing = (BmListing)g;
int count = Math.Min(_qty, listing.Qty);
GoodHead(c, g, L.T("black.stock", listing.Qty));
c.AddLabel(L.T("black.price", BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, count), _mode)) + (count > 1 ? " ×" + count : ""));
c.AddFlexibleSpace();
bool ok = _mode == PayMode.Escrow || seller.Advance;
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), pw => PlaceOrder(listing, count, asset, mode, pw));
});
});
}
private const int ShelfCols = 4;
private const float CardW = 172f, CardH = 268f;
/// <summary>货架:一行 4 张商品卡。</summary>
private static void Shelf(CustomUIBuilder b, System.Collections.Generic.List<BmGood> goods, Action<CustomUIBuilder, BmGood> card)
{
for (int i = 0; i < goods.Count; i += ShelfCols)
{
b.BeginRow(6f);
for (int k = i; k < Math.Min(goods.Count, i + ShelfCols); k++)
{
var g = goods[k];
b.BeginPanel(CardW, CardH, TextAnchor.UpperCenter, 6f, 3f);
card(b, g);
b.End();
}
b.End();
}
}
private static void GoodHead(CustomUIBuilder c, BmGood g, string line2)
{
var sprite = Icon(g.Item);
if (sprite != null) c.AddImage(sprite, 64f, 64f); else c.AddSpace(64f);
c.AddLabel("<b>" + ItemName(g.Item) + "</b>");
bool asic = g.Kind == GoodKind.Gpu && g.Model != null && Hardware.Specs.TryGetValue(g.Model, out var gs) && gs.Asic;
c.AddLabel(asic ? (g.Health >= 100 ? L.T("black.kind.asicnew") : L.T("black.kind.asic", (int)Math.Round(g.Health))) : g.Kind == GoodKind.Gpu ? (g.Health >= 100 ? L.T("black.kind.gpunew") : L.T("black.kind.gpu", (int)Math.Round(g.Health))) : L.T("black.kind." + g.Kind + (g.Kind == GoodKind.Contraband ? "." + g.Level : "")));
if (!string.IsNullOrEmpty(line2)) c.AddLabel(line2);
}
private static string ItemName(string id) => BlackDelivery.ItemName(id);
/// <summary>开账单:收款地址、币种、金额(按信用点等值折成币),付款在硬件钱包里做。</summary>
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<Wallet> execute)
{
WalletWindow.Pending = new WalletWindow.Bill { Address = address, Summary = summary, Asset = asset, Amount = WalletWindow.RoundUp(amount), Execute = execute };
_message = null;
Open();
}
/// <summary>待付账单:收款地址和金额,去钱包里转账;也可以作废。</summary>
private static void PayPage(CustomUIBuilder b)
{
var p = WalletWindow.Pending;
b.AddHeader(L.T("black.pay.confirm"));
b.AddLabel(p.Summary);
b.BeginRow(8f, TextAnchor.MiddleLeft);
b.AddLabel(L.T("black.pay.to", p.Address));
b.AddButton(L.T("hw.copy"), () => { Clipboard.Copy(p.Address); _message = L.T("hw.copied.addr", p.Address); Open(); });
b.End();
b.AddLabel(L.T("black.pay.amount", WalletWindow.Amt(p.Amount), p.Asset, Fmt.Credits(p.Amount * Session.Price(p.Asset))));
b.BeginRow();
b.AddButton(L.T("black.pay.wallet"), () => WalletWindow.OpenForBill(p));
b.AddButton(L.T("black.pay.cancel"), () => { WalletWindow.Pending = null; _message = null; Open(); });
b.End();
}
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;
double health = o.Health;
int bought = 0;
for (int i = 0; i < count; i++)
{
if (!d.Black.Buy(o, pw, asset, Session.Price)) break;
Items.Give(BlackDelivery.Make(item, model, fake, health));
bought++;
}
if (bought == 0) { _message = L.T("msg.balance"); Open(); return; }
_message = L.T("black.bought", ItemName(item) + (bought > 1 ? " ×" + bought : ""));
Session.Save();
Open();
}
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, 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();
Open();
}
private static void BeginEdit(EditPrice e, int initial)
{
_edit = e;
_editPrice = initial.ToString(CultureInfo.InvariantCulture);
_message = null;
Open();
}
private static int MinPrice(long refValue) => (int)Math.Ceiling(Tuning.BmSellMin * refValue - 1e-9);
private static int MaxPrice(long refValue) => (int)Math.Floor(Tuning.BmSellMax * refValue + 1e-9);
/// <summary>设定标价:显示参考价,档位按钮只是帮忙填价格,价格也可以手输;按 标价 / 参考价 的比例算成交和污点。</summary>
private static void EditPage(CustomUIBuilder b)
{
var e = _edit;
b.AddHeader(ItemName(e.Id));
b.AddLabel(L.T("black.edit.ref", e.Ref));
b.BeginPanel(RightW, -1f, TextAnchor.MiddleCenter, 0f, 0f);
b.AddInput(L.T("black.edit.input"), _editPrice, s => _editPrice = s, "price", s => _editPrice = s);
b.End();
b.BeginRow();
foreach (var m in SellMults)
{
double mult = m;
b.AddButton("×" + m.ToString("0.0", CultureInfo.InvariantCulture), () => { _editPrice = BlackMarket.ListPriceOf(e.Ref, mult).ToString(CultureInfo.InvariantCulture); Open(); });
}
b.End();
if (int.TryParse((_editPrice ?? "").Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int p) && p > 0)
b.AddLabel(L.T("black.edit.price", p, (p / (double)e.Ref).ToString("0.00", CultureInfo.InvariantCulture)));
b.BeginRow();
b.AddButton(L.T("black.edit.ok"), () =>
{
if (!int.TryParse((_editPrice ?? "").Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out int price) || price < MinPrice(e.Ref) || price > MaxPrice(e.Ref))
{ _message = L.T("black.edit.range", MinPrice(e.Ref), MaxPrice(e.Ref)); Open(); return; }
double mult = price / (double)e.Ref;
var d = Session.Data;
int fee = e.Sale != null ? BlackMarket.RepriceFee(price) : BlackMarket.ListFee(price);
_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,
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, Wallet pw)
{
var d = Session.Data;
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();
}
private static void Delist(BmSale s)
{
var d = Session.Data;
d.Black.Delist(s);
Items.Give(BlackDelivery.Make(s.Item, s.Model, s.FakeOf, s.Health));
Session.Save();
Open();
}
/// <summary>我的订单与论坛情报。</summary>
private static void OrdersPage(CustomUIBuilder b, int day)
{
var d = Session.Data;
b.AddHeader(L.T("black.orders.in"));
if (d.Black.Orders.Count == 0) b.AddLabel(L.T("black.mine.none"));
foreach (var o in d.Black.Orders.OrderByDescending(x => x.PlacedDay))
b.AddLabel(L.T("black.mine.line", ItemName(o.Item) + (o.Count > 1 ? " ×" + o.Count : ""), BlackDelivery.DisplayName(o.Seller), L.T("black.mode." + o.Mode), L.T("black.state." + o.State + (o.Why != null && o.State != OrderState.Done ? "." + o.Why : "")), o.DueDay));
b.AddSeparator();
b.AddHeader(L.T("black.orders.out"));
if (d.Black.Sales.Count == 0) b.AddLabel(L.T("black.mine.none"));
foreach (var sl in d.Black.Sales.Where(x => x.State != SaleState.Delisted || x.Why == "fake").OrderByDescending(x => x.ListedDay))
{
var sale = sl;
string st = sale.State == SaleState.Sold ? L.T("black.sale.Sold", sale.PayDay) : L.T("black.sale." + sale.State + (sale.State == SaleState.Delisted && sale.Why != null ? "." + sale.Why : ""));
b.AddLabel(L.T("black.mine.sale", ItemName(sale.Item), sale.Ref, sale.ListPrice, sale.Mult.ToString("0.00", CultureInfo.InvariantCulture), st));
if (sale.State == SaleState.Listed)
{
b.BeginRow();
b.AddButton(L.T("black.sell.reprice"), () => BeginEdit(new EditPrice { Sale = sale, Id = sale.Item, Ref = sale.Ref }, sale.ListPrice));
b.AddButton(L.T("black.sell.delist"), () => Delist(sale));
b.End();
}
}
}
/// <summary>匿名币兑换:Web3 交易页里的一个通用兑换块,带"匿名币"标签。要黑市声望,有每日额度;兑入时这部分价值的污点被清除。</summary>
private static void PoneroBlock(CustomUIBuilder r)
{
var d = Session.Data;
var w = d.Hot;
int day = StoreStation.GetDayCounter();
r.AddSeparator();
r.AddLabel(L.T("black.desk.pair", Fmt.Num(Session.Price(Assets.Ponero))));
r.AddLabel(L.T("black.desk.left", Fmt.Credits(d.Black.DeskLeft(day)), Fmt.Credits(Tuning.PoneroDailyCap)));
r.BeginRow();
foreach (var a in new[] { Assets.PSC, Assets.CRT, Assets.CRA })
{
var asset = a;
r.AddButton(asset == _deskAsset ? $"[{asset}]" : asset, () => { _deskAsset = asset; Open(); });
}
r.End();
r.BeginRow();
r.AddButton(L.T("black.desk.in"), () => { _deskIn = true; Swap(); });
r.AddButton(L.T("black.desk.out"), () => { _deskIn = false; Swap(); });
r.End();
void Swap()
{
if (!ReadAmount(out double dv)) return;
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, pw => DeskSwap(dv, into, other, pw));
}
}
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(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;
_message = _deskIn ? L.T("msg.trade", Fmt.Num(v), _deskAsset, Fmt.Num(got), Assets.Ponero) : L.T("msg.trade", Fmt.Num(v), Assets.Ponero, Fmt.Num(got), _deskAsset);
Session.Save();
Open();
}
private enum Web3Tab { Trade, Stake, Airdrop, Predict, Ponzi, Mining, Mixer, Black }
private static Web3Tab _web3Tab = Web3Tab.Trade;
private static string _mixAsset = Assets.PSC;
private static int _mixRounds = 2;
private static readonly float[] MixW = { 70f, 100f, 100f, 80f };
/// <summary>混币器:把币存进去,几轮之后提回同一个地址,污点降低,每轮扣手续费。</summary>
private static void MixerPage(CustomUIBuilder b)
{
var d = Session.Data;
var w = d.Hot;
int day = StoreStation.GetDayCounter();
Columns(b, l =>
{
l.AddHeader(L.T("mixer.h"));
l.AddLabel(d.Mixer.Sanctioned(day) ? L.T("mixer.sanctioned", d.Mixer.SanctionedUntil - day + 1) : L.T("mixer.ok"));
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)));
l.AddSeparator();
l.AddHeader(L.T("mixer.pending"));
if (d.Mixer.Pending.Count == 0) l.AddLabel(L.T("mixer.none"));
else
{
TableHeader(l, MixW, L.T("col.asset"), L.T("col.amount"), L.T("mixer.col.taint"), L.T("mixer.col.ready"));
foreach (var p in d.Mixer.Pending)
TableRow(l, MixW, p.Asset, Fmt.Num(p.Amount), Fmt.Credits(p.Taint), L.T("mixer.day", p.ReadyDay));
}
}, r =>
{
r.AddHeader(L.T("mixer.deposit"));
r.BeginRow();
foreach (var a in new[] { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA })
{
var asset = a;
r.AddButton(asset == _mixAsset ? $"[{asset}]" : asset, () => { _mixAsset = asset; Open(); });
}
r.End();
r.AddLabel(L.T("mixer.balance", _mixAsset, Fmt.Num(w.Avail(_mixAsset))));
AmountRow(r);
r.AddLabel(L.T("mixer.rounds"));
r.BeginRow();
for (int n = 1; n <= Tuning.MixerMaxRounds; n++)
{
int rounds = n;
r.AddButton(rounds == _mixRounds ? $"[{rounds}]" : rounds.ToString(), () => { _mixRounds = rounds; Open(); });
}
r.End();
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double v) && v > 0)
{
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.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, pw => MixDeposit(mv, asset, rounds, pw));
});
});
}
private static void MixDeposit(double v, string asset, int rounds, Wallet pw)
{
var d = Session.Data;
int day = StoreStation.GetDayCounter();
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();
}
private static readonly float[] StakeW = { 110f, 90f, 200f };
/// <summary>质押页:革命军 CRA 质押池(日息、解锁排队)和平衡代币 SPARK 的铸销。都是合约调用,要在钱包上签名。</summary>
private static void StakePage(CustomUIBuilder b)
{
var d = Session.Data;
var st = d.Stable;
var w = d.Hot;
int day = StoreStation.GetDayCounter();
Columns(b, l =>
{
l.AddHeader(L.T("stake.h"));
l.AddLabel(L.T("stake.rate", (st.CraRate * 100).ToString("0.0", CultureInfo.InvariantCulture), Tuning.CraUnlockDays) + (st.CraSubsidyLow ? " " + L.T("stake.low") : ""));
l.AddLabel(L.T("stake.cra", Fmt.Num(st.Cra), StableStatus(st, Assets.CRA, day)));
l.AddSeparator();
if (d.Staking.Lots.Count == 0) l.AddLabel(L.T("stake.none"));
else
{
TableHeader(l, StakeW, L.T("stake.col.wallet"), L.T("stake.col.amount"), L.T("stake.col.queue"));
foreach (var lot in d.Staking.Lots)
{
var ow = d.Own(lot.Wallet);
string queue = lot.Unlocking.Count == 0 ? "—" : string.Join(" ", lot.Unlocking.Select(u => Fmt.Num(u.Value) + L.T("stake.day", u.Key)));
TableRow(l, StakeW, ow != null ? WalletWindow.NameOf(ow) : lot.Wallet, Fmt.Num(lot.Amount), queue);
}
}
l.AddSeparator();
l.AddLabel(L.T("spark.price", Fmt.Num(st.Spark)));
YieldPools(l, w, day);
}, r =>
{
r.AddHeader(L.T("stake.ops", WalletWindow.NameOf(w)));
r.AddLabel(L.T("stake.avail", Fmt.Num(w.Avail(Assets.CRA)), Fmt.Num(d.Staking.Of(w.Address)?.Amount ?? 0)));
r.AddLabel(L.T("stake.avail2", Fmt.Num(w.Avail(Assets.SPARK))));
AmountRow(r);
r.BeginRow();
r.AddButton(L.T("stake.do"), () => StakeCall(true));
r.AddButton(L.T("stake.undo"), () => StakeCall(false));
r.End();
r.AddSeparator();
r.AddHeader(L.T("spark.h"));
r.BeginRow();
r.AddButton(L.T("spark.mint"), () => SparkCall(true));
r.AddButton(L.T("spark.burn"), () => SparkCall(false));
r.End();
});
}
/// <summary>手续费用钱包里价值最高的那种币付。</summary>
private static string BestFeeAsset(Wallet w) =>
w.Balances.Where(k => k.Value > 1e-9 && Session.Price(k.Key) > 0).OrderByDescending(k => k.Value * Session.Price(k.Key)).Select(k => k.Key).FirstOrDefault() ?? Assets.PSC;
private static void StakeCall(bool into)
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
var w = d.Hot;
if (into ? w.Avail(Assets.CRA) + 1e-9 < v : (d.Staking.Of(w.Address)?.Amount ?? 0) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = Assets.CRA, Amount = v, Spend = into ? v : 0, FeeAsset = into ? null : BestFeeAsset(w),
Summary = L.T(into ? "stake.sum.in" : "stake.sum.out", Fmt.Num(v), Tuning.CraUnlockDays),
Execute = pw =>
{
int day = StoreStation.GetDayCounter();
bool ok = into ? d.Staking.Stake(pw, v, Session.Price) : d.Staking.RequestUnstake(pw.Address, v, day);
_message = ok ? L.T(into ? "stake.done.in" : "stake.done.out", Fmt.Num(v), day + Tuning.CraUnlockDays) : L.T("msg.balance");
Session.Save();
Open();
},
});
}
private static void SparkCall(bool mint)
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
var w = d.Hot;
string pay = mint ? Assets.CRA : Assets.SPARK, get = mint ? Assets.SPARK : Assets.CRA;
if (w.Avail(pay) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
double quote = mint ? v * d.Stable.Cra / d.Stable.Spark * (1 - Tuning.SparkFee) : v * d.Stable.Spark / d.Stable.Cra * (1 - Tuning.SparkFee);
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill
{
Contract = true, Asset = pay, Amount = v, Summary = L.T("hw.swap", Fmt.Num(v), pay, Fmt.Num(quote), get),
Execute = pw =>
{
double got = mint ? d.Stable.Mint(pw, v) : d.Stable.Burn(pw, v);
_message = got < 0 ? L.T("msg.balance") : L.T("msg.trade", Fmt.Num(v), pay, Fmt.Num(got), get);
Session.Save();
Open();
},
});
}
private static bool frozenPay(Wallet w, string asset) => w.Frozen.ContainsKey(asset);
private static string AvgText(Wallet w, string asset) => w.AvgBuy(asset) is double a ? Fmt.Num(a) : L.T("avg.none");
private static void ReportPage(CustomUIBuilder b)
{
var r = Session.Data.LastNight;
if (r == null) { b.AddLabel(L.T("report.none")); return; }
b.AddHeader(L.T("report.day", r.Day));
b.AddLabel(L.T("report.rigs", r.PoweredRigs, r.Rigs));
b.AddLabel(L.T("report.hash", Fmt.Num(r.Hash * 10), Fmt.Num(r.NetworkHash * 10)));
b.AddLabel(L.T("report.mined", Fmt.Num(r.Mined), Fmt.Credits(r.Mined * r.Price)));
b.AddLabel(L.T("report.energy", Fmt.Num(r.Kj)));
b.AddLabel(L.T("report.burnt", r.Burnt.Count));
foreach (var n in r.Burnt) b.AddLabel(" " + n);
b.AddLabel(L.T("report.price", Fmt.Num(r.Price)));
}
private static bool ReadAmount(out double v)
{
if (!double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) || v <= 0)
{
_message = L.T("msg.amount");
Open();
return false;
}
return true;
}
private static void Deposit()
{
if (!ReadAmount(out double v)) return;
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.Poinbase;
w.Add(Assets.CRC, credits / Session.Data.Stable.Crc);
w.RecordBuy(Assets.CRC, credits / Session.Data.Stable.Crc, credits);
w.Basis += credits;
_message = L.T("msg.deposit", credits);
Session.Save();
Open();
}
private static void Withdraw()
{
if (!ReadAmount(out double v)) return;
var d = Session.Data;
if (d.Perps.Debt > 0) { _message = L.T("perp.debt.block"); Open(); return; }
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));
if (r.Outcome == AmlOutcome.Light) _message += "\n" + L.T("msg.aml_light", Fmt.Credits(r.Confiscated));
if (r.Outcome == AmlOutcome.Heavy)
{
_message += "\n" + L.T("msg.aml_heavy", Fmt.Credits(r.Confiscated));
PlayerStore.Instance.secData.CommitCrime("FENCING", (int)Math.Round(r.Crime));
}
Session.Save();
Open();
}
/// <summary>交易:Poinbase 是托管账户,直接成交;DEX 是合约调用,要在硬件钱包上签名。</summary>
private static void Trade(Pool p, bool buy)
{
if (!ReadAmount(out double v)) return;
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, w); return; }
double quote = buy ? v / p.Price : v * p.Price;
_message = L.T("hw.wait");
Open();
WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Asset = pay, Amount = v, Summary = L.T("hw.swap", Fmt.Num(v), pay, Fmt.Num(quote), get), Execute = pw => DoTrade(p, buy, v, pw) });
}
private static void DoTrade(Pool p, bool buy, double v, Wallet w)
{
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 before = p.Price;
double got = buy ? p.BuyBase(v) : p.SellBase(v);
if (p == Session.Data.Market.Main) { Session.Data.Market.PlayerPush += Math.Log(p.Price / before); Session.Data.Candles.Tick(Assets.PSC, StoreStation.GetDayCounter(), p.Price, v * Session.Price(pay), true); Log.Info($"现货推价:累计 {Session.Data.Market.PlayerPush:0.000}"); }
double spent = v * Session.Price(pay);
w.Add(pay, -v);
w.Add(get, got);
w.RecordBuy(get, got, spent);
_message = L.T("msg.trade", Fmt.Num(v), pay, Fmt.Num(got), get);
Session.Save();
Open();
}
}
}