1727 lines
91 KiB
C#
1727 lines
91 KiB
C#
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();
|
||
}
|
||
}
|
||
}
|