2026-11-03 09:11:46 +08:00
|
|
|
|
using System;
|
2026-11-04 09:59:03 +08:00
|
|
|
|
using System.Collections.Generic;
|
2026-11-03 09:11:46 +08:00
|
|
|
|
using System.Globalization;
|
|
|
|
|
|
using System.Linq;
|
2026-11-03 15:15:19 +08:00
|
|
|
|
using UnityEngine;
|
2026-11-04 09:59:03 +08:00
|
|
|
|
using UnityEngine.UI;
|
2026-11-04 11:18:45 +08:00
|
|
|
|
using TMPro;
|
2026-11-03 09:11:46 +08:00
|
|
|
|
using Decentralized.Domain;
|
|
|
|
|
|
|
|
|
|
|
|
namespace Decentralized
|
|
|
|
|
|
{
|
2026-11-03 15:23:15 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 交易终端窗口:首页三个入口(银行、Poinbase、Web3)加报告。银行是真实的信用点(存取、房租);
|
|
|
|
|
|
/// Poinbase 是合规交易所;Web3 是去中心化交易和链上钱包。每次操作后按当前页重建窗口。
|
|
|
|
|
|
/// </summary>
|
2026-11-03 09:11:46 +08:00
|
|
|
|
public static class TerminalWindow
|
|
|
|
|
|
{
|
|
|
|
|
|
public const string Id = "pd_terminal";
|
2026-11-04 15:34:38 +08:00
|
|
|
|
private const float Bezel = 6f;
|
|
|
|
|
|
// 窗口尺寸取自 mod 设置(828×680 是基准,各页布局按多出来的宽高等比放宽)
|
|
|
|
|
|
private static float WinW => Settings.TerminalW;
|
|
|
|
|
|
private static float WinH => Settings.TerminalH;
|
|
|
|
|
|
private static float ScreenW => WinW - 60f;
|
|
|
|
|
|
private static float ScreenH => WinH - 102f;
|
|
|
|
|
|
private static float Ext => ScreenW - 768f;
|
|
|
|
|
|
private static float LeftW => 420f + Ext * 0.6f;
|
|
|
|
|
|
private static float RightW => 310f + Ext * 0.4f;
|
|
|
|
|
|
/// <summary>大窗口里文字放大(行高、卡片同步放大):基准窗口 1,1200 宽约 1.24,上限 1.3。</summary>
|
|
|
|
|
|
private static float F => Mathf.Clamp(1f + Ext * 0.00065f, 1f, 1.3f);
|
|
|
|
|
|
public static float ChartScale => LeftW >= 634f ? 3f : 2f;
|
|
|
|
|
|
private static float[] Sc(float[] b) { float k = LeftW / 420f; return b.Select(x => x * k).ToArray(); }
|
2026-11-05 16:31:42 +08:00
|
|
|
|
private enum Page { Home, Bank, Poinbase, Web3, Mining, Gray, Wallets }
|
2026-11-03 09:11:46 +08:00
|
|
|
|
|
2026-11-03 15:23:15 +08:00
|
|
|
|
private static Page _page = Page.Home;
|
2026-11-03 09:11:46 +08:00
|
|
|
|
private static string _amount = "";
|
|
|
|
|
|
private static string _message;
|
|
|
|
|
|
|
2026-11-04 09:59:03 +08:00
|
|
|
|
private static readonly List<GameObject> _picked = new List<GameObject>();
|
|
|
|
|
|
private static readonly List<Image> _clear = new List<Image>();
|
|
|
|
|
|
private static readonly List<PledgerSkin.RowDef> _rows = new List<PledgerSkin.RowDef>();
|
|
|
|
|
|
|
2026-11-05 21:57:15 +08:00
|
|
|
|
// 窗口建好之后保留:之后的刷新只重建页签栏、滚动内容和消息行,不重建窗口框架,滚动位置也不丢。
|
|
|
|
|
|
private static CustomUIWindow _win;
|
|
|
|
|
|
private static RectTransform _screen, _topRoot, _bodyRoot, _noteRoot;
|
|
|
|
|
|
private static ScrollRect _scroll;
|
|
|
|
|
|
private static float _builtW, _builtH;
|
|
|
|
|
|
private static string _scrollKey;
|
|
|
|
|
|
private static bool _dirty;
|
|
|
|
|
|
|
|
|
|
|
|
private static bool Alive() =>
|
|
|
|
|
|
CustomUIManager.Instance != null && CustomUIManager.Instance.IsOpen(Id) && _win != null && _win.root != null
|
|
|
|
|
|
&& _screen != null && _topRoot != null && _bodyRoot != null && _noteRoot != null
|
|
|
|
|
|
&& Math.Abs(_builtW - WinW) < 0.5f && Math.Abs(_builtH - WinH) < 0.5f;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>打开或刷新终端。窗口还没开(或窗口大小变了)时马上完整构建;已经开着就只标记要刷新,同一帧里的多次刷新合并成一次(见 <see cref="Flush"/>)。</summary>
|
2026-11-03 09:11:46 +08:00
|
|
|
|
public static void Open()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (Session.Data == null) return;
|
2026-11-05 21:57:15 +08:00
|
|
|
|
if (!Alive()) { OpenFull(); return; }
|
|
|
|
|
|
_dirty = true;
|
|
|
|
|
|
WalletDriver.Ensure();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>每帧由常驻驱动调用:有待刷新的就重建一次内容区。</summary>
|
|
|
|
|
|
public static void Flush()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!_dirty) return;
|
|
|
|
|
|
_dirty = false;
|
|
|
|
|
|
if (Session.Data == null || CustomUIManager.Instance == null || !CustomUIManager.Instance.IsOpen(Id)) return;
|
|
|
|
|
|
if (!Alive()) { OpenFull(); return; }
|
|
|
|
|
|
Rebuild();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string ScrollKey() => string.Join("|", _page, _web3Tab, _grayTab, _blackTab, _sellView, _walletTab, _poinTab);
|
|
|
|
|
|
|
|
|
|
|
|
private static void BuildTop(CustomUIBuilder b)
|
|
|
|
|
|
{
|
2026-11-05 16:31:42 +08:00
|
|
|
|
if (_page == Page.Gray && !GrayAvailable) _page = Page.Web3;
|
|
|
|
|
|
TabRows(b, Enum.GetValues(typeof(Page)).Cast<Page>().Where(p => p != Page.Gray || GrayAvailable).Select(p =>
|
2026-11-03 09:11:46 +08:00
|
|
|
|
{
|
|
|
|
|
|
var page = p;
|
2026-11-05 21:32:30 +08:00
|
|
|
|
return new TabDef { Name = L.T("tab." + p.ToString().ToLowerInvariant()), On = page == _page, Click = () => { _page = page; _message = null; Open(); } };
|
2026-11-05 16:31:42 +08:00
|
|
|
|
}).ToList(), true);
|
|
|
|
|
|
ConnectPicker(b);
|
2026-11-05 21:57:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void BuildBody(CustomUIBuilder b)
|
|
|
|
|
|
{
|
2026-11-05 16:31:42 +08:00
|
|
|
|
if ((_page == Page.Web3 || _page == Page.Gray) && WalletWindow.Pending != null && !WalletWindow.Pending.Contract) PayPage(b);
|
|
|
|
|
|
else switch (_page)
|
2026-11-03 09:11:46 +08:00
|
|
|
|
{
|
2026-11-03 15:23:15 +08:00
|
|
|
|
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;
|
2026-11-05 16:31:42 +08:00
|
|
|
|
case Page.Mining: ReportPage(b); break;
|
|
|
|
|
|
case Page.Gray: GrayPage(b); break;
|
2026-11-03 22:58:24 +08:00
|
|
|
|
case Page.Wallets: WalletsPage(b); break;
|
2026-11-03 20:13:18 +08:00
|
|
|
|
default: break;
|
2026-11-03 09:11:46 +08:00
|
|
|
|
}
|
2026-11-05 21:57:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void BuildNote(CustomUIBuilder b)
|
|
|
|
|
|
{
|
2026-11-04 09:59:03 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(_message)) b.AddLabel(_message, "term_note");
|
2026-11-05 21:57:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>完整构建:窗口、黑边、屏幕、页签栏、滚动区、消息行。</summary>
|
|
|
|
|
|
private static void OpenFull()
|
|
|
|
|
|
{
|
|
|
|
|
|
var b = CustomUIManager.Window(Id, L.T("term.title"));
|
|
|
|
|
|
if (b == null) return;
|
2026-11-05 22:41:00 +08:00
|
|
|
|
_picked.Clear(); _clear.Clear(); _rows.Clear(); _live.Clear();
|
2026-11-05 21:57:15 +08:00
|
|
|
|
b.SetSize(WinW, WinH).SetDraggable(true).SetCloseOnEscape(true).UseMachineryStyle();
|
|
|
|
|
|
// 黑色外边框包裹墨水屏
|
|
|
|
|
|
b.BeginPanel(ScreenW + Bezel * 2, ScreenH + Bezel * 2, TextAnchor.MiddleCenter, Bezel + 3f, 0f);
|
|
|
|
|
|
var bezel = b.Last.image;
|
|
|
|
|
|
b.BeginPanel(ScreenW, ScreenH, TextAnchor.UpperLeft, 12f, 4f);
|
|
|
|
|
|
var screen = b.CurrentContainer;
|
|
|
|
|
|
var paper = b.Last.image;
|
|
|
|
|
|
b.BeginPanel(ScreenW - 24f, -1f, TextAnchor.UpperLeft, 0f, 4f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
|
|
|
|
|
var top = b.CurrentContainer;
|
|
|
|
|
|
BuildTop(b);
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
b.AddSeparator();
|
|
|
|
|
|
b.BeginScroll(ScreenH - 52f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
|
|
|
|
|
var body = b.CurrentContainer;
|
|
|
|
|
|
BuildBody(b);
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
b.BeginPanel(ScreenW - 24f, -1f, TextAnchor.UpperLeft, 0f, 0f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
|
|
|
|
|
var note = b.CurrentContainer;
|
|
|
|
|
|
BuildNote(b);
|
|
|
|
|
|
b.End();
|
2026-11-04 09:59:03 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
var win = b.Show();
|
2026-11-05 21:57:15 +08:00
|
|
|
|
_win = win; _screen = screen; _topRoot = top; _bodyRoot = body; _noteRoot = note;
|
|
|
|
|
|
_scroll = body != null ? body.GetComponentInParent<ScrollRect>() : null;
|
|
|
|
|
|
_builtW = WinW; _builtH = WinH; _scrollKey = ScrollKey(); _dirty = false;
|
2026-11-04 15:34:38 +08:00
|
|
|
|
Log.Guard("交易终端外观", () => PledgerSkin.Apply(win, screen, bezel, paper, _picked, _clear, _rows, F));
|
2026-11-04 11:18:45 +08:00
|
|
|
|
Log.Guard("购物车按钮", () => CartFab(win));
|
2026-11-03 09:11:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 21:57:15 +08:00
|
|
|
|
/// <summary>只重建页签栏、滚动内容和消息行;窗口框架和滚动视图保留(滚动位置自然不变,换页或换子页签时回到顶部)。</summary>
|
|
|
|
|
|
private static void Rebuild()
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = CustomUIManager.Instance;
|
2026-11-05 22:41:00 +08:00
|
|
|
|
_picked.Clear(); _clear.Clear(); _rows.Clear(); _live.Clear();
|
2026-11-05 21:57:15 +08:00
|
|
|
|
CustomUIBuilder.ClearContainer(_topRoot);
|
|
|
|
|
|
CustomUIBuilder.ClearContainer(_bodyRoot);
|
|
|
|
|
|
CustomUIBuilder.ClearContainer(_noteRoot);
|
|
|
|
|
|
BuildTop(new CustomUIBuilder(m, _win, _topRoot));
|
|
|
|
|
|
BuildBody(new CustomUIBuilder(m, _win, _bodyRoot));
|
|
|
|
|
|
BuildNote(new CustomUIBuilder(m, _win, _noteRoot));
|
|
|
|
|
|
string key = ScrollKey();
|
|
|
|
|
|
if (key != _scrollKey)
|
|
|
|
|
|
{
|
|
|
|
|
|
_scrollKey = key;
|
|
|
|
|
|
if (_scroll != null && _scroll.content != null) { var cp = _scroll.content.anchoredPosition; _scroll.content.anchoredPosition = new Vector2(cp.x, 0f); }
|
|
|
|
|
|
}
|
|
|
|
|
|
CustomUIBuilder.RebuildLayout(_topRoot); CustomUIBuilder.RebuildLayout(_bodyRoot); CustomUIBuilder.RebuildLayout(_noteRoot);
|
|
|
|
|
|
Log.Guard("交易终端外观", () => PledgerSkin.Apply(_win, _screen, null, null, _picked, _clear, _rows, F, false));
|
|
|
|
|
|
Log.Guard("购物车按钮", () => CartFab(_win));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 21:32:30 +08:00
|
|
|
|
private static void Go(Page p) { _page = p; _message = null; Open(); }
|
2026-11-03 22:29:42 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>钱包那边交易落地后刷新终端(开着的话),带上结果消息。</summary>
|
|
|
|
|
|
public static void Refresh(string message)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (message != null) _message = message;
|
|
|
|
|
|
if (CustomUIManager.Instance != null && CustomUIManager.Instance.IsOpen(Id)) Open();
|
|
|
|
|
|
}
|
2026-11-03 15:23:15 +08:00
|
|
|
|
|
2026-11-05 16:50:40 +08:00
|
|
|
|
private static void Entry(CustomUIBuilder b, string icon, string name, string line1, string line2, Page target, bool compact, int perRow) =>
|
|
|
|
|
|
Entry(b, icon, name, line1, line2, L.T("home.enter"), () => Go(target), compact, perRow);
|
2026-11-05 01:21:35 +08:00
|
|
|
|
|
2026-11-05 16:50:40 +08:00
|
|
|
|
private static void Entry(CustomUIBuilder b, string icon, string name, string line1, string line2, string button, Action onEnter, bool compact, int perRow)
|
2026-11-03 09:11:46 +08:00
|
|
|
|
{
|
2026-11-05 16:50:40 +08:00
|
|
|
|
float h = compact ? Mathf.Clamp((ScreenH - 190f) / 2f, 180f, 270f) : 250f + (ScreenH - 578f) * 0.25f, pic = compact ? 64f : 96f;
|
|
|
|
|
|
b.BeginPanel((ScreenW - 80f) / perRow, h, TextAnchor.UpperCenter, 10f, compact ? 4f : 6f);
|
2026-11-04 09:59:03 +08:00
|
|
|
|
var cardImg = b.Last.image;
|
|
|
|
|
|
cardImg.sprite = MachineryStyle.White;
|
|
|
|
|
|
cardImg.color = PledgerSkin.RowNormal;
|
2026-11-05 16:50:40 +08:00
|
|
|
|
b.AddImage(ModInfo.Id + ":" + icon, pic, pic);
|
2026-11-03 15:23:15 +08:00
|
|
|
|
b.AddHeader(name);
|
|
|
|
|
|
b.AddLabel(line1);
|
2026-11-04 09:59:03 +08:00
|
|
|
|
if (line2 != null) b.AddLabel(line2, "entry_l2_dim");
|
2026-11-03 15:23:15 +08:00
|
|
|
|
b.AddFlexibleSpace();
|
2026-11-05 01:21:35 +08:00
|
|
|
|
b.AddButton(button, onEnter);
|
2026-11-03 15:23:15 +08:00
|
|
|
|
b.End();
|
2026-11-03 09:11:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 15:23:15 +08:00
|
|
|
|
private static void HomePage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-03 22:58:24 +08:00
|
|
|
|
double chain = d.ChainValue(Session.Price);
|
2026-11-03 15:23:15 +08:00
|
|
|
|
b.AddLabel(L.T("home.summary", PlayerStore.Instance.playerCash, Fmt.Credits(chain)));
|
|
|
|
|
|
b.AddSpace(6f);
|
2026-11-05 16:50:40 +08:00
|
|
|
|
var cards = new List<Action<bool, int>>
|
|
|
|
|
|
{
|
|
|
|
|
|
(two, n) => Entry(b, "ui_bank", L.T("entry.bank"), L.T("entry.bank.l1"), L.T("entry.bank.l2", PlayerStore.Instance.playerCash), Page.Bank, two, n),
|
|
|
|
|
|
(two, n) => Entry(b, "ui_poinbase", L.T("entry.poinbase"), L.T("entry.poinbase.l1"), null, Page.Poinbase, two, n),
|
|
|
|
|
|
(two, n) => Entry(b, "ui_web3", L.T("entry.web3"), L.T("entry.web3.l1"), L.T("entry.web3.l2", Fmt.Credits(chain)), Page.Web3, two, n),
|
|
|
|
|
|
(two, n) => Entry(b, "ui_mining", L.T("tab.mining"), L.T("entry.mining.l1"), null, Page.Mining, two, n),
|
|
|
|
|
|
};
|
|
|
|
|
|
if (GrayAvailable) cards.Add((two, n) => Entry(b, "ui_gray", L.T("tab.gray"), L.T("entry.gray.l1"), null, Page.Gray, two, n));
|
|
|
|
|
|
cards.Add((two, n) => Entry(b, "ui_wallets", L.T("tab.wallets"), L.T("entry.wallets.l1"), L.T("entry.wallets.l2", d.Wallets.Count, Tuning.WalletMax), Page.Wallets, two, n));
|
|
|
|
|
|
cards.Add((two, n) => Entry(b, "ui_help", L.T("entry.help"), L.T("entry.help.l1"), null, L.T("home.open"), HandbookWindow.Open, two, n));
|
|
|
|
|
|
// 一行四张;多于四张时分两行(每行尽量均分),卡片和图标相应缩小
|
|
|
|
|
|
bool twoRows = cards.Count > 4;
|
|
|
|
|
|
int first = twoRows ? (cards.Count + 1) / 2 : cards.Count;
|
|
|
|
|
|
for (int i = 0; i < cards.Count; i += first)
|
|
|
|
|
|
{
|
|
|
|
|
|
b.BeginRow(8f, TextAnchor.UpperCenter);
|
|
|
|
|
|
for (int j = i; j < Math.Min(cards.Count, i + first); j++) cards[j](twoRows, first);
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
if (twoRows && i == 0) b.AddSpace(8f);
|
|
|
|
|
|
}
|
2026-11-03 15:23:15 +08:00
|
|
|
|
b.AddSpace(8f);
|
|
|
|
|
|
b.AddSeparator();
|
|
|
|
|
|
var r = d.LastNight;
|
2026-11-04 09:59:03 +08:00
|
|
|
|
b.AddLabel(r == null ? L.T("report.none") : L.T("home.lastnight", r.Day, Fmt.Num(r.Mined), r.PoweredRigs, r.Rigs), "report_dim");
|
2026-11-03 15:23:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 22:25:48 +08:00
|
|
|
|
private static void Cell(CustomUIBuilder b, float w, string text, bool header = false, TextAnchor align = TextAnchor.MiddleLeft, string tip = null)
|
2026-11-03 15:41:42 +08:00
|
|
|
|
{
|
2026-11-04 15:34:38 +08:00
|
|
|
|
b.BeginPanel(w, 26f * F, align, 0f, 0f);
|
2026-11-04 09:59:03 +08:00
|
|
|
|
_clear.Add(b.Last.image);
|
2026-11-05 22:25:48 +08:00
|
|
|
|
if (!string.IsNullOrEmpty(tip)) b.WithTooltip(tip);
|
2026-11-03 15:41:42 +08:00
|
|
|
|
b.AddLabel(header ? "<b>" + text + "</b>" : text);
|
2026-11-04 14:55:11 +08:00
|
|
|
|
PledgerSkin.FitCurrent(b);
|
2026-11-03 15:41:42 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 22:25:48 +08:00
|
|
|
|
private static void TableRow(CustomUIBuilder b, float totalW, float[] widths, TextAnchor[] aligns, string[] cells, bool selected = false, Action onClick = null, string[] tips = null)
|
2026-11-03 15:41:42 +08:00
|
|
|
|
{
|
2026-11-04 15:34:38 +08:00
|
|
|
|
b.BeginPanel(totalW, 28f * F, TextAnchor.MiddleLeft, 4f, 0f);
|
2026-11-04 09:59:03 +08:00
|
|
|
|
_rows.Add(new PledgerSkin.RowDef { Root = b.Last.root, Image = b.Last.image, Selected = selected, OnClick = onClick });
|
|
|
|
|
|
b.BeginRow(0f, TextAnchor.MiddleLeft);
|
|
|
|
|
|
for (int i = 0; i < widths.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var align = (aligns != null && i < aligns.Length) ? aligns[i] : (i == 0 ? TextAnchor.MiddleLeft : TextAnchor.MiddleRight);
|
2026-11-05 22:25:48 +08:00
|
|
|
|
Cell(b, widths[i], i < cells.Length ? cells[i] : "", false, align, tips != null && i < tips.Length ? tips[i] : null);
|
2026-11-04 09:59:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
b.End();
|
2026-11-03 15:41:42 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 09:59:03 +08:00
|
|
|
|
private static void TableRow(CustomUIBuilder b, float[] widths, params string[] cells) =>
|
|
|
|
|
|
TableRow(b, widths.Sum() + 8f, widths, null, cells, false, null);
|
|
|
|
|
|
|
|
|
|
|
|
private static void TableHeader(CustomUIBuilder b, float totalW, float[] widths, TextAnchor[] aligns, params string[] cells)
|
2026-11-03 15:41:42 +08:00
|
|
|
|
{
|
2026-11-04 15:34:38 +08:00
|
|
|
|
b.BeginPanel(totalW, 26f * F, TextAnchor.MiddleLeft, 4f, 0f);
|
2026-11-04 09:59:03 +08:00
|
|
|
|
_clear.Add(b.Last.image);
|
|
|
|
|
|
b.BeginRow(0f, TextAnchor.MiddleLeft);
|
|
|
|
|
|
for (int i = 0; i < widths.Length; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var align = (aligns != null && i < aligns.Length) ? aligns[i] : (i == 0 ? TextAnchor.MiddleLeft : TextAnchor.MiddleRight);
|
|
|
|
|
|
Cell(b, widths[i], i < cells.Length ? cells[i] : "", true, align);
|
|
|
|
|
|
}
|
|
|
|
|
|
b.End();
|
2026-11-03 15:41:42 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 09:59:03 +08:00
|
|
|
|
private static void TableHeader(CustomUIBuilder b, float[] widths, params string[] cells) =>
|
|
|
|
|
|
TableHeader(b, widths.Sum() + 8f, widths, null, cells);
|
|
|
|
|
|
|
2026-11-03 15:41:42 +08:00
|
|
|
|
/// <summary>左右分栏:左边是账户与持仓表格,右边是操作区。</summary>
|
|
|
|
|
|
private static void Columns(CustomUIBuilder b, Action<CustomUIBuilder> left, Action<CustomUIBuilder> right)
|
|
|
|
|
|
{
|
|
|
|
|
|
b.BeginRow(10f);
|
2026-11-04 09:59:03 +08:00
|
|
|
|
b.BeginPanel(LeftW, -1f, TextAnchor.UpperLeft, 4f, 4f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
2026-11-03 15:41:42 +08:00
|
|
|
|
left(b);
|
|
|
|
|
|
b.End();
|
2026-11-04 09:59:03 +08:00
|
|
|
|
b.BeginPanel(RightW, -1f, TextAnchor.UpperLeft, 4f, 6f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
2026-11-03 15:41:42 +08:00
|
|
|
|
right(b);
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 22:41:00 +08:00
|
|
|
|
// 输入时实时更新的预览文字:输入框每次输入只改这些文字,不重建页面(重建会让输入框失焦)。
|
|
|
|
|
|
private static readonly List<KeyValuePair<CustomUIElement, Func<string>>> _live = new List<KeyValuePair<CustomUIElement, Func<string>>>();
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>加一行实时预览:文字由 text() 算,输入框内容变化时刷新;没有内容时是一个空行占位。</summary>
|
|
|
|
|
|
private static void LiveLabel(CustomUIBuilder b, Func<string> text, string style)
|
|
|
|
|
|
{
|
|
|
|
|
|
string t = text();
|
|
|
|
|
|
b.AddLabel(string.IsNullOrEmpty(t) ? " " : t, style);
|
|
|
|
|
|
if (b.Last != null && b.Last.text != null) _live.Add(new KeyValuePair<CustomUIElement, Func<string>>(b.Last, text));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void RefreshLive()
|
|
|
|
|
|
{
|
|
|
|
|
|
foreach (var kv in _live)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (kv.Key == null || kv.Key.text == null) continue;
|
|
|
|
|
|
string t = kv.Value();
|
|
|
|
|
|
kv.Key.text.text = string.IsNullOrEmpty(t) ? " " : t;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool ParseAmount(out double v) =>
|
|
|
|
|
|
double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out v) && v > 0;
|
|
|
|
|
|
|
2026-11-07 22:24:36 +08:00
|
|
|
|
private static void AmountRow(CustomUIBuilder b, bool label = true, string unit = null)
|
2026-11-03 09:11:46 +08:00
|
|
|
|
{
|
2026-11-07 22:24:36 +08:00
|
|
|
|
if (label) b.AddLabel(unit == null ? L.T("trade.amount") : L.T("trade.amount.unit", unit));
|
2026-11-04 09:59:03 +08:00
|
|
|
|
b.BeginPanel(RightW - 30f, -1f, TextAnchor.MiddleCenter, 0f, 0f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
2026-11-05 22:41:00 +08:00
|
|
|
|
b.AddInput("0", _amount, s => { _amount = s; RefreshLive(); }, "amount", s => { _amount = s; RefreshLive(); });
|
2026-11-03 09:11:46 +08:00
|
|
|
|
b.End();
|
2026-11-03 15:23:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>银行:真实的信用点。现金、房租,以及信用点与 Poinbase 账户(CRC)之间的存取。</summary>
|
|
|
|
|
|
private static void BankPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var ps = PlayerStore.Instance;
|
2026-11-04 09:59:45 +08:00
|
|
|
|
var w = new[] { 180f, 220f };
|
2026-11-05 07:19:38 +08:00
|
|
|
|
b.AddHeader(L.T("entry.bank"));
|
|
|
|
|
|
TableRow(b, w, L.T("bank.cash.k"), ps.playerCash.ToString());
|
|
|
|
|
|
TableRow(b, w, L.T("bank.rent.k"), L.T("bank.rent.v", Session.Rent(), ps.GetRentDayCounter()));
|
|
|
|
|
|
TableRow(b, w, L.T("bank.crc.k"), Fmt.Num(Session.Data.Poinbase.Get(Assets.CRC)));
|
2026-11-03 15:23:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 10:34:06 +08:00
|
|
|
|
/// <summary>提取前的合规审查提示:仅保留账户污点比例(额度与命中概率按设计不公开)。</summary>
|
2026-11-03 15:44:49 +08:00
|
|
|
|
private static void AmlPreview(CustomUIBuilder b)
|
|
|
|
|
|
{
|
2026-11-04 10:34:06 +08:00
|
|
|
|
var pb = Session.Data.Poinbase;
|
2026-11-03 22:58:24 +08:00
|
|
|
|
double total = pb.Value(Session.Price);
|
|
|
|
|
|
double taintRatio = total > 0 ? pb.Taint / total : 0;
|
2026-11-04 10:34:06 +08:00
|
|
|
|
b.AddSeparator();
|
2026-11-04 09:59:45 +08:00
|
|
|
|
b.AddLabel(L.T("aml.taint", (int)Math.Round(taintRatio * 100)), taintRatio > 0.05 ? "aml_note" : "aml_dim");
|
2026-11-03 15:44:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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");
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 07:09:05 +08:00
|
|
|
|
private static readonly float[] PriceWBase = new[] { 100f, 160f };
|
|
|
|
|
|
private static float[] PriceW => Sc(PriceWBase);
|
|
|
|
|
|
private static readonly string[] SwapCoins = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA, Assets.SPARK };
|
|
|
|
|
|
private static string _swapFrom = Assets.CRC, _swapTo = Assets.PSC;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>币价表:各币在 DEX 池里以 CRC 计的价格。</summary>
|
|
|
|
|
|
private static void PriceTable(CustomUIBuilder b)
|
2026-11-03 15:44:49 +08:00
|
|
|
|
{
|
2026-11-05 07:09:05 +08:00
|
|
|
|
var m = Session.Data.Market;
|
2026-11-03 15:44:49 +08:00
|
|
|
|
b.AddSeparator();
|
2026-11-05 07:09:05 +08:00
|
|
|
|
TableHeader(b, PriceW, L.T("col.asset"), L.T("col.crcprice"));
|
|
|
|
|
|
foreach (var a in SwapCoins.Where(x => x != Assets.CRC))
|
|
|
|
|
|
TableRow(b, PriceW, a, Fmt.Num(m.DexPool(a).Price));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>互换:任意币对任意币,实际经 CRC 路由;预览显示得到数量、经过几个池、损耗。</summary>
|
|
|
|
|
|
private static void SwapBlock(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var m = Session.Data.Market;
|
|
|
|
|
|
Action<string, string, bool> picker = (label, cur, from) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
b.AddLabel(label);
|
|
|
|
|
|
b.BeginRow(4f);
|
|
|
|
|
|
foreach (var c in SwapCoins)
|
|
|
|
|
|
{
|
|
|
|
|
|
var coin = c;
|
|
|
|
|
|
PledgerSkin.Pick(b, coin, coin == cur, () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (from) { _swapFrom = coin; if (_swapTo == coin) _swapTo = SwapCoins.First(x => x != coin); }
|
|
|
|
|
|
else { _swapTo = coin; if (_swapFrom == coin) _swapFrom = SwapCoins.First(x => x != coin); }
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
};
|
|
|
|
|
|
AmountRow(b);
|
|
|
|
|
|
picker(L.T("swap.pay"), _swapFrom, true);
|
|
|
|
|
|
picker(L.T("swap.get"), _swapTo, false);
|
2026-11-05 22:41:00 +08:00
|
|
|
|
LiveLabel(b, () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var q = ParseAmount(out double v) ? m.Quote(_swapFrom, _swapTo, v) : null;
|
2026-11-07 23:28:41 +08:00
|
|
|
|
return q == null ? "" : L.T("swap.preview", Fmt.Num(q.Out), _swapTo, q.Hops, (q.Impact * 100).ToString("0.00", CultureInfo.InvariantCulture));
|
2026-11-05 22:41:00 +08:00
|
|
|
|
}, "swap_preview_dim");
|
2026-11-05 07:09:05 +08:00
|
|
|
|
b.AddButton(L.T("swap.go"), () => Swap());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>互换:DEX 是合约调用,要在硬件钱包上签名。</summary>
|
|
|
|
|
|
private static void Swap()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!ReadAmount(out double v)) return;
|
|
|
|
|
|
string from = _swapFrom, to = _swapTo;
|
|
|
|
|
|
var q = Session.Data.Market.Quote(from, to, v);
|
|
|
|
|
|
if (q == null) { _message = L.T("swap.same"); Open(); return; }
|
|
|
|
|
|
var w = Session.Data.Hot;
|
|
|
|
|
|
if (w.Avail(from) + 1e-9 < v) { _message = L.T(frozenPay(w, from) ? "msg.frozen" : "msg.balance"); Open(); return; }
|
|
|
|
|
|
_message = L.T("hw.wait");
|
|
|
|
|
|
Open();
|
2026-11-07 22:39:18 +08:00
|
|
|
|
WalletWindow.OpenForBill(new WalletWindow.Bill { Contract = true, Party = Session.Data.Address("dex"), Asset = from, Amount = v, Summary = L.T("hw.swap", Fmt.Num(v), from, Fmt.Num(q.Out), to), Execute = pw => DoSwap(from, to, v, pw) });
|
2026-11-05 07:09:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void DoSwap(string from, string to, double v, Wallet w)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (w.Avail(from) + 1e-9 < v) { _message = L.T(frozenPay(w, from) ? "msg.frozen" : "msg.balance"); Open(); return; }
|
|
|
|
|
|
double got = Session.Data.Market.Swap(from, to, v);
|
|
|
|
|
|
if (got <= 0) { _message = L.T("swap.same"); Open(); return; }
|
|
|
|
|
|
double spent = v * Session.Price(from);
|
|
|
|
|
|
CandleChart.Trade(from, spent); CandleChart.Trade(to, spent);
|
|
|
|
|
|
w.Add(from, -v);
|
|
|
|
|
|
w.Add(to, got);
|
|
|
|
|
|
w.RecordBuy(to, got, spent);
|
|
|
|
|
|
_message = L.T("msg.trade", Fmt.Num(v), from, Fmt.Num(got), to);
|
|
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
2026-11-03 15:44:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-07 23:03:59 +08:00
|
|
|
|
private static void TradeRows(CustomUIBuilder b, Pool pool)
|
2026-11-03 15:23:15 +08:00
|
|
|
|
{
|
2026-11-03 09:11:46 +08:00
|
|
|
|
{
|
|
|
|
|
|
var p = pool;
|
2026-11-07 23:28:41 +08:00
|
|
|
|
// 在 Poinbase 页里,交易所名不用再写一遍(行太长会被截断)
|
|
|
|
|
|
b.AddLabel(p.Base + "/" + p.Quote + " " + L.T("trade.price", Fmt.Num(p.Price)) + " " + L.T("trade.fee", (p.Fee * 100).ToString("0.0#", CultureInfo.InvariantCulture)));
|
2026-11-05 22:41:00 +08:00
|
|
|
|
LiveLabel(b, () => SpotPreview(p, true), "trade_preview_dim");
|
|
|
|
|
|
LiveLabel(b, () => SpotPreview(p, false), "trade_preview_dim");
|
2026-11-04 10:01:41 +08:00
|
|
|
|
b.BeginRow(4f);
|
2026-11-03 09:11:46 +08:00
|
|
|
|
b.AddButton(L.T("trade.buy"), () => Trade(p, buy: true));
|
|
|
|
|
|
b.AddButton(L.T("trade.sell"), () => Trade(p, buy: false));
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-07 22:24:36 +08:00
|
|
|
|
/// <summary>现货模拟(不改池子):输入的数量是标的币;买入算要付多少计价币,卖出算能得到多少。均价含手续费,滑点不含手续费。</summary>
|
2026-11-05 22:41:00 +08:00
|
|
|
|
private static string SpotPreview(Pool p, bool buy)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!ParseAmount(out double v)) return "";
|
2026-11-07 22:24:36 +08:00
|
|
|
|
double quote = buy ? p.QuoteIn(v) : p.QuoteOut(v);
|
|
|
|
|
|
if (quote <= 0 || double.IsInfinity(quote)) return buy ? L.T("trade.sim.thin") : "";
|
|
|
|
|
|
double avg = quote / v, net = buy ? avg * (1 - p.Fee) : avg / (1 - p.Fee);
|
|
|
|
|
|
double slip = buy ? net / p.Price - 1 : 1 - net / p.Price;
|
|
|
|
|
|
return L.T(buy ? "trade.sim.buy" : "trade.sim.sell", Fmt.Num(quote), p.Quote, Fmt.Num(avg), (Math.Max(0, slip) * 100).ToString("0.00", CultureInfo.InvariantCulture));
|
2026-11-05 22:41:00 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:34:38 +08:00
|
|
|
|
private static readonly float[] HoldWBase = new[] { 80f, 100f, 110f, 110f };
|
|
|
|
|
|
private static float[] HoldW => Sc(HoldWBase);
|
2026-11-04 10:01:41 +08:00
|
|
|
|
private static readonly TextAnchor[] HoldAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight };
|
2026-11-03 15:41:42 +08:00
|
|
|
|
|
|
|
|
|
|
private static void Holdings(CustomUIBuilder b, Wallet w, Func<string, bool> show)
|
|
|
|
|
|
{
|
2026-11-04 10:01:41 +08:00
|
|
|
|
TableHeader(b, HoldW.Sum() + 8f, HoldW, HoldAligns, L.T("col.asset"), L.T("col.amount"), L.T("col.value"), L.T("col.avg"));
|
2026-11-03 15:41:42 +08:00
|
|
|
|
bool any = false;
|
|
|
|
|
|
foreach (var kv in w.Balances.OrderBy(k => k.Key).Where(k => show(k.Key)))
|
|
|
|
|
|
{
|
|
|
|
|
|
any = true;
|
2026-11-04 10:01:41 +08:00
|
|
|
|
string asset = kv.Key;
|
2026-11-05 07:19:38 +08:00
|
|
|
|
bool selected = (_page == Page.Poinbase && _poinTab == PoinTab.Funds && asset == _fundsAsset);
|
2026-11-04 10:01:41 +08:00
|
|
|
|
TableRow(b, HoldW.Sum() + 8f, HoldW, HoldAligns,
|
|
|
|
|
|
new[] { kv.Key, Fmt.Num(kv.Value), Fmt.Credits(kv.Value * Session.Price(kv.Key)), AvgText(w, kv.Key) },
|
|
|
|
|
|
selected,
|
2026-11-05 07:19:38 +08:00
|
|
|
|
() => { if (_page == Page.Poinbase && _poinTab == PoinTab.Funds) { _fundsAsset = asset; if (asset != Assets.CRC) _fundsBank = false; Open(); } });
|
2026-11-03 15:41:42 +08:00
|
|
|
|
}
|
2026-11-04 10:01:41 +08:00
|
|
|
|
if (!any) b.AddLabel(L.T(w == Session.Data.Poinbase ? "poin.empty" : "wallet.empty"), "hold_empty_dim");
|
2026-11-03 16:51:12 +08:00
|
|
|
|
foreach (var kv in w.Frozen.OrderBy(k => k.Key).Where(k => show(k.Key)))
|
2026-11-04 10:01:41 +08:00
|
|
|
|
b.AddLabel(L.T("wallet.frozen", kv.Key, Fmt.Num(kv.Value), w.FrozenUntil.TryGetValue(kv.Key, out var u) ? u : 0), "hold_frozen_note");
|
2026-11-03 15:41:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Poinbase:合规交易所。左边账户持仓,右边交易;信用点的存取在银行。</summary>
|
2026-11-05 07:19:38 +08:00
|
|
|
|
private enum PoinTab { Funds, Spot, Perp }
|
|
|
|
|
|
private static PoinTab _poinTab = PoinTab.Funds;
|
2026-11-04 00:26:50 +08:00
|
|
|
|
|
2026-11-03 15:23:15 +08:00
|
|
|
|
private static void ExchangePage(CustomUIBuilder b, Venue venue)
|
|
|
|
|
|
{
|
2026-11-03 22:58:24 +08:00
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
var w = d.Poinbase;
|
2026-11-04 10:01:41 +08:00
|
|
|
|
b.BeginRow(4f);
|
2026-11-04 00:26:50 +08:00
|
|
|
|
foreach (PoinTab t in Enum.GetValues(typeof(PoinTab)))
|
|
|
|
|
|
{
|
|
|
|
|
|
var tab = t;
|
|
|
|
|
|
string name = L.T("poin.sub." + t.ToString().ToLowerInvariant());
|
2026-11-04 10:01:41 +08:00
|
|
|
|
PledgerSkin.Pick(b, name, tab == _poinTab, () => { _poinTab = tab; _message = null; Open(); }, _picked);
|
2026-11-04 00:26:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
if (_poinTab == PoinTab.Perp) { PerpPage(b); return; }
|
2026-11-05 07:19:38 +08:00
|
|
|
|
if (_poinTab == PoinTab.Funds) { FundsPage(b); return; }
|
2026-11-03 15:41:42 +08:00
|
|
|
|
Columns(b, l =>
|
|
|
|
|
|
{
|
2026-11-07 23:03:59 +08:00
|
|
|
|
CandleChart.AddPicker(l, "poin", Assets.PSC, Open, _picked, Exchange.Listed);
|
2026-11-04 08:47:34 +08:00
|
|
|
|
l.AddSeparator();
|
2026-11-03 15:41:42 +08:00
|
|
|
|
l.AddHeader(L.T("poin.account.h"));
|
2026-11-03 22:58:24 +08:00
|
|
|
|
Holdings(l, w, a => true);
|
2026-11-04 10:01:41 +08:00
|
|
|
|
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)), "taint_note");
|
2026-11-03 15:41:42 +08:00
|
|
|
|
}, r =>
|
|
|
|
|
|
{
|
2026-11-07 23:03:59 +08:00
|
|
|
|
// 交易对跟着行情图选中的币(CRC 本身没有交易对,显示 PSC/CRC)
|
|
|
|
|
|
string sel = CandleChart.Selected("poin", Assets.PSC, Exchange.Listed);
|
|
|
|
|
|
var pair = d.Market.Find(Venue.Exchange, sel, Assets.CRC) ?? d.Market.Main;
|
2026-11-03 15:41:42 +08:00
|
|
|
|
r.AddHeader(L.T("trade.h"));
|
2026-11-07 23:03:59 +08:00
|
|
|
|
AmountRow(r, unit: pair.Base);
|
|
|
|
|
|
TradeRows(r, pair);
|
2026-11-03 15:41:42 +08:00
|
|
|
|
});
|
2026-11-03 15:23:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 07:19:38 +08:00
|
|
|
|
private enum FundsMode { Receive, Send }
|
|
|
|
|
|
private static FundsMode _fundsMode = FundsMode.Receive;
|
|
|
|
|
|
private static string _fundsAsset = Assets.CRC, _fundsTo = "";
|
|
|
|
|
|
private static bool _fundsBank = true;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>资金页:入金和出金,先选币种。CRC 可以走银行账户或钱包,其他币只能走钱包。</summary>
|
|
|
|
|
|
private static void FundsPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
var w = d.Poinbase;
|
|
|
|
|
|
if (_fundsAsset != Assets.CRC) _fundsBank = false;
|
|
|
|
|
|
Columns(b, l =>
|
|
|
|
|
|
{
|
|
|
|
|
|
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)), "taint_note");
|
|
|
|
|
|
}, r =>
|
|
|
|
|
|
{
|
|
|
|
|
|
r.BeginRow(4f);
|
|
|
|
|
|
foreach (FundsMode m in Enum.GetValues(typeof(FundsMode)))
|
|
|
|
|
|
{
|
|
|
|
|
|
var mode = m;
|
|
|
|
|
|
PledgerSkin.Pick(r, L.T("funds." + m.ToString().ToLowerInvariant()), mode == _fundsMode, () => { _fundsMode = mode; _message = null; Open(); }, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
r.End();
|
2026-11-07 23:03:59 +08:00
|
|
|
|
// 入金只收 Poinbase 上架的币;出金另外列出账户里还有余额的其他币(旧存档里充进来的 CRT、CRA)
|
|
|
|
|
|
var coins = Exchange.Listed.Concat(_fundsMode == FundsMode.Send ? SwapCoins.Where(a => !Exchange.Listed.Contains(a) && w.Get(a) > 1e-9) : Enumerable.Empty<string>()).ToList();
|
|
|
|
|
|
if (!coins.Contains(_fundsAsset)) { _fundsAsset = Assets.CRC; _fundsBank = true; }
|
2026-11-05 07:19:38 +08:00
|
|
|
|
r.BeginRow(4f);
|
2026-11-07 23:03:59 +08:00
|
|
|
|
foreach (var c in coins)
|
2026-11-05 07:19:38 +08:00
|
|
|
|
{
|
|
|
|
|
|
var coin = c;
|
|
|
|
|
|
PledgerSkin.Pick(r, coin, coin == _fundsAsset, () => { _fundsAsset = coin; if (coin != Assets.CRC) _fundsBank = false; Open(); }, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
if (_fundsAsset == Assets.CRC)
|
|
|
|
|
|
{
|
|
|
|
|
|
r.BeginRow(4f);
|
|
|
|
|
|
PledgerSkin.Pick(r, L.T("funds.bank"), _fundsBank, () => { _fundsBank = true; Open(); }, _picked);
|
|
|
|
|
|
PledgerSkin.Pick(r, L.T("funds.wallet"), !_fundsBank, () => { _fundsBank = false; Open(); }, _picked);
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
r.AddSeparator();
|
|
|
|
|
|
if (_fundsMode == FundsMode.Receive)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_fundsBank)
|
|
|
|
|
|
{
|
|
|
|
|
|
AmountRow(r);
|
|
|
|
|
|
r.AddButton(L.T("trade.deposit"), Deposit);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
r.AddLabel(L.T("poin.deposit", d.Address("poinbase")), "deposit_dim");
|
|
|
|
|
|
r.AddButton(L.T("hw.copy"), () => { Clipboard.Copy(d.Address("poinbase")); _message = L.T("hw.copied.addr", d.Address("poinbase")); Open(); });
|
|
|
|
|
|
}
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_fundsBank)
|
|
|
|
|
|
{
|
|
|
|
|
|
AmountRow(r);
|
|
|
|
|
|
AmlPreview(r);
|
|
|
|
|
|
r.AddButton(L.T("trade.withdraw"), Withdraw);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
r.AddLabel(L.T("funds.to"));
|
|
|
|
|
|
r.BeginPanel(RightW - 30f, -1f, TextAnchor.MiddleCenter, 0f, 0f);
|
|
|
|
|
|
_clear.Add(r.Last.image);
|
|
|
|
|
|
r.AddInput("0x", _fundsTo, s => _fundsTo = s, "funds_to", s => _fundsTo = s);
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
if (d.Wallets.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
r.BeginRow(4f);
|
|
|
|
|
|
foreach (var ow in d.Wallets)
|
|
|
|
|
|
{
|
|
|
|
|
|
var own = ow;
|
|
|
|
|
|
PledgerSkin.Pick(r, WalletWindow.NameOf(own), ChainAddress.Same(_fundsTo, own.Address), () => { _fundsTo = own.Address; Open(); }, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
AmountRow(r);
|
|
|
|
|
|
r.AddButton(L.T("funds.go"), FundsSend);
|
|
|
|
|
|
foreach (var t in Funds.Active)
|
|
|
|
|
|
{
|
|
|
|
|
|
r.AddLabel(L.T("funds.sending", Fmt.Num(t.Amount), t.Asset, ChainAddress.Short(t.To)), "funds_sending_dim");
|
|
|
|
|
|
r.AddLabel(Funds.StatusText(t), "funds_status_" + t.Id);
|
|
|
|
|
|
r.AddProgressBar(Funds.Progress(t), "funds_prog_" + t.Id);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void FundsSend()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!ReadAmount(out double v)) return;
|
|
|
|
|
|
string err = Funds.Start(_fundsAsset, v, _fundsTo);
|
|
|
|
|
|
_message = err != null ? L.T(err) : null;
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
2026-11-03 22:58:24 +08:00
|
|
|
|
|
2026-11-04 00:26:50 +08:00
|
|
|
|
private static string _perpProduct = "PSC";
|
|
|
|
|
|
private static bool _perpLong = true;
|
|
|
|
|
|
private static int _perpLev = 3;
|
2026-11-04 15:34:38 +08:00
|
|
|
|
private static readonly float[] PerpWBase = new[] { 80f, 70f, 110f, 60f, 70f };
|
|
|
|
|
|
private static float[] PerpW => Sc(PerpWBase);
|
2026-11-04 10:01:41 +08:00
|
|
|
|
private static readonly TextAnchor[] PerpAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight };
|
2026-11-04 00:26:50 +08:00
|
|
|
|
|
|
|
|
|
|
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))));
|
2026-11-04 10:01:41 +08:00
|
|
|
|
if (bk.Debt > 0) l.AddLabel(L.T("perp.debt", Fmt.Credits(bk.Debt), Math.Max(0, Tuning.PerpDebtDays - (day - bk.DebtSince))), "perp_debt_note");
|
|
|
|
|
|
if (bk.Banned(day)) l.AddLabel(L.T("perp.banned", bk.BannedUntil - day + 1), "perp_banned_note");
|
2026-11-04 00:26:50 +08:00
|
|
|
|
l.AddSeparator();
|
2026-11-04 10:01:41 +08:00
|
|
|
|
if (bk.Positions.Count == 0) l.AddLabel(L.T("perp.none"), "perp_none_dim");
|
2026-11-04 00:26:50 +08:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-11-04 10:01:41 +08:00
|
|
|
|
TableHeader(l, PerpW.Sum() + 8f, PerpW, PerpAligns, 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"));
|
2026-11-04 00:26:50 +08:00
|
|
|
|
foreach (var p in bk.Positions)
|
|
|
|
|
|
{
|
2026-11-04 10:01:41 +08:00
|
|
|
|
var pos = p;
|
|
|
|
|
|
double spot = PerpSpot(pos.Product);
|
|
|
|
|
|
bool selPos = pos.Product == _perpProduct;
|
|
|
|
|
|
TableRow(l, PerpW.Sum() + 8f, PerpW, PerpAligns,
|
|
|
|
|
|
new[] {
|
|
|
|
|
|
pos.Product + " " + L.T(pos.Long ? "perp.long" : "perp.short") + " " + Math.Round(pos.Leverage, 1) + "×",
|
|
|
|
|
|
Fmt.Credits(pos.Notional(spot)),
|
|
|
|
|
|
Fmt.Num(pos.Entry) + " → " + Fmt.Num(spot),
|
|
|
|
|
|
Fmt.Credits(pos.Pnl(spot)),
|
|
|
|
|
|
Fmt.Num(pos.LiqPrice)
|
|
|
|
|
|
},
|
|
|
|
|
|
selPos,
|
|
|
|
|
|
() => { _perpProduct = pos.Product; Open(); });
|
2026-11-04 00:26:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
l.AddSeparator();
|
|
|
|
|
|
foreach (var p in PerpBook.Products)
|
2026-11-04 10:01:41 +08:00
|
|
|
|
l.AddLabel(L.T("perp.rate", p, Fmt.Num(PerpSpot(p)), (bk.Rate(p) * 100).ToString("+0.00;-0.00;0.00", CultureInfo.InvariantCulture)), "perp_rate_dim");
|
|
|
|
|
|
l.AddLabel(L.T("perp.fund", Fmt.Credits(bk.InsuranceFund)), "perp_fund_dim");
|
2026-11-04 08:47:34 +08:00
|
|
|
|
l.AddSeparator();
|
2026-11-04 08:51:31 +08:00
|
|
|
|
CandleChart.Add(l, "perp", _perpProduct);
|
2026-11-04 00:26:50 +08:00
|
|
|
|
}, r =>
|
|
|
|
|
|
{
|
2026-11-05 01:25:46 +08:00
|
|
|
|
var held = bk.Positions.Count > 0 ? bk.Positions[0] : null;
|
|
|
|
|
|
if (held != null) { _perpProduct = held.Product; _perpLong = held.Long; _perpLev = Math.Max(1, (int)Math.Round(held.Leverage)); }
|
|
|
|
|
|
r.AddHeader(L.T(held == null ? "perp.open" : "perp.col.pos"));
|
|
|
|
|
|
if (held == null)
|
2026-11-04 00:26:50 +08:00
|
|
|
|
{
|
2026-11-05 01:25:46 +08:00
|
|
|
|
r.BeginRow(4f);
|
|
|
|
|
|
foreach (var p in PerpBook.Products)
|
|
|
|
|
|
{
|
|
|
|
|
|
var product = p;
|
|
|
|
|
|
PledgerSkin.Pick(r, product, product == _perpProduct, () => { _perpProduct = product; Open(); }, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
r.BeginRow(4f);
|
|
|
|
|
|
PledgerSkin.Pick(r, L.T("perp.long"), _perpLong, () => { _perpLong = true; Open(); }, _picked);
|
|
|
|
|
|
PledgerSkin.Pick(r, L.T("perp.short"), !_perpLong, () => { _perpLong = false; Open(); }, _picked);
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
r.BeginRow(4f);
|
|
|
|
|
|
foreach (int lev in new[] { 1, 2, 3, 5 })
|
|
|
|
|
|
{
|
|
|
|
|
|
if (lev > PerpBook.MaxLeverage(_perpProduct)) continue;
|
|
|
|
|
|
int lv = lev;
|
|
|
|
|
|
PledgerSkin.Pick(r, lv + "×", lv == _perpLev, () => { _perpLev = lv; Open(); }, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
r.End();
|
2026-11-04 00:26:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
r.AddLabel(L.T("perp.margin"));
|
2026-11-05 01:25:46 +08:00
|
|
|
|
AmountRow(r, false);
|
2026-11-05 22:41:00 +08:00
|
|
|
|
if (held == null)
|
2026-11-04 00:26:50 +08:00
|
|
|
|
{
|
2026-11-05 22:41:00 +08:00
|
|
|
|
LiveLabel(r, () => PerpOpenPreview(1), "perp_preview_dim");
|
|
|
|
|
|
LiveLabel(r, () => PerpOpenPreview(2), "perp_preview_dim");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
LiveLabel(r, () => PerpClosePreview(held, 1), "perp_preview_dim");
|
|
|
|
|
|
LiveLabel(r, () => PerpClosePreview(held, 2), "perp_preview_dim");
|
2026-11-04 00:26:50 +08:00
|
|
|
|
}
|
2026-11-04 10:01:41 +08:00
|
|
|
|
r.BeginRow(6f);
|
2026-11-05 01:25:46 +08:00
|
|
|
|
if (held == null) r.AddButton(L.T("perp.do.open"), PerpOpen);
|
|
|
|
|
|
else r.AddButton(L.T("perp.do.add"), PerpAdd);
|
|
|
|
|
|
if (held != null) r.AddButton(L.T("perp.do.close"), PerpClose);
|
2026-11-04 00:26:50 +08:00
|
|
|
|
r.End();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 22:41:00 +08:00
|
|
|
|
/// <summary>开仓模拟:名义价值、预估平均成交价(含滑点)、手续费、强平价。line 1 是价格行,2 是费用和强平价行。</summary>
|
|
|
|
|
|
private static string PerpOpenPreview(int line)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!ParseAmount(out double m)) return "";
|
|
|
|
|
|
var bk = Session.Data.Perps;
|
|
|
|
|
|
int day = StoreStation.GetDayCounter();
|
|
|
|
|
|
double notional = m * _perpLev, spot = PerpSpot(_perpProduct), exec = bk.Exec(spot, notional, _perpLong, day);
|
|
|
|
|
|
if (line == 1) return L.T("perp.preview", Fmt.Credits(notional), Fmt.Num(exec), ((exec / spot - 1) * 100).ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture));
|
|
|
|
|
|
var ghost = new PerpPosition { Long = _perpLong, Entry = exec, Size = notional / exec, Margin = m };
|
|
|
|
|
|
return L.T("perp.preview2", Fmt.Num(notional * Tuning.PerpFee), Fmt.Num(ghost.LiqPrice));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>平仓模拟:按现价平掉整个仓位的预估成交价(含滑点)、手续费、回到账户的 CRC 和盈亏。</summary>
|
|
|
|
|
|
private static string PerpClosePreview(PerpPosition pos, int line)
|
|
|
|
|
|
{
|
|
|
|
|
|
var bk = Session.Data.Perps;
|
|
|
|
|
|
double spot = PerpSpot(pos.Product);
|
|
|
|
|
|
double exec = bk.Exec(spot, pos.Notional(spot), !pos.Long, StoreStation.GetDayCounter());
|
|
|
|
|
|
double fee = pos.Notional(exec) * Tuning.PerpFee;
|
|
|
|
|
|
double payout = Math.Max(0, pos.Equity(exec) - fee);
|
|
|
|
|
|
if (line == 1) return L.T("perp.sim.close", Fmt.Credits(pos.Notional(spot)), Fmt.Num(exec), ((exec / spot - 1) * 100).ToString("+0.0;-0.0;0.0", CultureInfo.InvariantCulture));
|
|
|
|
|
|
return L.T("perp.sim.close2", Fmt.Num(fee), Fmt.Num(payout), (payout - pos.Margin >= 0 ? "+" : "") + Fmt.Num(payout - pos.Margin));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:34:38 +08:00
|
|
|
|
private static readonly float[] PoolWBase = new[] { 110f, 50f, 64f, 64f, 50f, 78f };
|
|
|
|
|
|
private static float[] PoolW => Sc(PoolWBase);
|
2026-11-04 10:04:00 +08:00
|
|
|
|
private static readonly TextAnchor[] PoolAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight };
|
2026-11-04 08:28:43 +08:00
|
|
|
|
|
|
|
|
|
|
/// <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.AddHeader(L.T("pool.h"));
|
2026-11-04 10:04:00 +08:00
|
|
|
|
if (pools.Count == 0) { l.AddLabel(L.T("pool.none"), "pool_none_dim"); return; }
|
|
|
|
|
|
TableHeader(l, PoolW.Sum() + 8f, PoolW, PoolAligns, 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"));
|
2026-11-04 08:28:43 +08:00
|
|
|
|
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") : "");
|
2026-11-04 10:04:00 +08:00
|
|
|
|
TableRow(l, PoolW.Sum() + 8f, PoolW, PoolAligns,
|
|
|
|
|
|
new[] {
|
|
|
|
|
|
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
|
|
|
|
|
|
});
|
2026-11-04 08:28:43 +08:00
|
|
|
|
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();
|
2026-11-04 10:04:00 +08:00
|
|
|
|
l.AddSpace(2f);
|
2026-11-04 08:28:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("yield:" + p.Name), Asset = Assets.CRC, Amount = v, Spend = deposit ? v : 0, FeeAsset = deposit ? null : BestFeeAsset(w),
|
2026-11-04 08:28:43 +08:00
|
|
|
|
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();
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (list.Count == 0) l.AddLabel(L.T("air.none"), "air_none_dim");
|
2026-11-04 08:28:43 +08:00
|
|
|
|
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));
|
2026-11-05 01:37:23 +08:00
|
|
|
|
bool authed = d.Connected && Scams.Authorized(a, w);
|
|
|
|
|
|
l.AddLabel(authed ? L.T("air.authed", WalletWindow.NameOf(w)) : L.T("air.unauth"), authed ? "air_authed_dim" : "air_unauth_dim");
|
|
|
|
|
|
if (authed) l.AddButton(L.T("air.claim"), () => ClaimAirdrop(ad));
|
|
|
|
|
|
else l.AddButton(L.T("air.auth"), () => AuthorizeAirdrop(ad));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
l.AddSpace(4f);
|
2026-11-04 08:28:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, r =>
|
|
|
|
|
|
{
|
|
|
|
|
|
r.AddHeader(L.T("air.approvals", WalletWindow.NameOf(w)));
|
|
|
|
|
|
var aps = d.Scams.Approvals.Where(x => ChainAddress.Same(x.Wallet, w.Address)).ToList();
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (aps.Count == 0) r.AddLabel(L.T("air.approvals.none"), "air_app_none_dim");
|
2026-11-04 08:28:43 +08:00
|
|
|
|
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));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
r.AddSpace(4f);
|
2026-11-04 08:28:43 +08:00
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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));
|
|
|
|
|
|
|
2026-11-05 01:37:23 +08:00
|
|
|
|
/// <summary>为这个空投授权当前连接的钱包:在钱包上签授权合约,摘要里写明授权范围(钓鱼空投的范围会写得很大)。</summary>
|
|
|
|
|
|
private static void AuthorizeAirdrop(Airdrop a)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
if (!d.Connected) { _message = L.T("web3.err.noconn"); Open(); return; }
|
|
|
|
|
|
_message = L.T("hw.wait");
|
|
|
|
|
|
Open();
|
|
|
|
|
|
var w = d.Hot;
|
|
|
|
|
|
WalletWindow.OpenForBill(new WalletWindow.Bill
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("airdrop:" + a.Name), Asset = Assets.CRC, Amount = 0, Spend = 0, FeeAsset = BestFeeAsset(w),
|
2026-11-05 01:37:23 +08:00
|
|
|
|
Summary = L.T("air.auth.sum", a.Name, ApproveText(a.Approve, a.Asset)),
|
|
|
|
|
|
Execute = pw =>
|
|
|
|
|
|
{
|
|
|
|
|
|
Scams.Authorize(a, pw, StoreStation.GetDayCounter());
|
|
|
|
|
|
_message = L.T("air.authed.msg", a.Name);
|
|
|
|
|
|
Log.Info($"授权空投 {a.Name}:钱包 {pw.Address}");
|
|
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 08:28:43 +08:00
|
|
|
|
private static void ClaimAirdrop(Airdrop a)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-05 01:37:23 +08:00
|
|
|
|
if (!d.Connected || !Scams.Authorized(a, d.Hot)) { _message = L.T("air.err.unauth"); Open(); return; }
|
2026-11-04 08:28:43 +08:00
|
|
|
|
_message = L.T("hw.wait");
|
|
|
|
|
|
Open();
|
|
|
|
|
|
var w = d.Hot;
|
|
|
|
|
|
WalletWindow.OpenForBill(new WalletWindow.Bill
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("airdrop:" + a.Name), Asset = Assets.CRC, Amount = 0, Spend = 0, FeeAsset = BestFeeAsset(w),
|
2026-11-04 08:28:43 +08:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("airdrop:" + ap.Spender), Asset = Assets.CRC, Amount = 0, Spend = 0, FeeAsset = BestFeeAsset(w),
|
2026-11-04 08:28:43 +08:00
|
|
|
|
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();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 09:32:59 +08:00
|
|
|
|
private enum YieldTab { Pools, Mine }
|
|
|
|
|
|
private static YieldTab _yieldTab = YieldTab.Pools;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>理财页:两个子页。"存钱"是 NPC 开的第三方收益池,你存进去吃利息(可能是骗局);"自己发"是你当庄开高息理财(庞氏),下层的人往你这儿存。</summary>
|
|
|
|
|
|
private static void YieldPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
2026-11-04 10:04:00 +08:00
|
|
|
|
b.BeginRow(4f);
|
2026-11-04 09:32:59 +08:00
|
|
|
|
foreach (YieldTab t in Enum.GetValues(typeof(YieldTab)))
|
|
|
|
|
|
{
|
|
|
|
|
|
var tab = t;
|
|
|
|
|
|
string name = L.T("yield.sub." + t.ToString().ToLowerInvariant());
|
2026-11-04 10:04:00 +08:00
|
|
|
|
PledgerSkin.Pick(b, name, tab == _yieldTab, () => { _yieldTab = tab; _message = null; Open(); }, _picked);
|
2026-11-04 09:32:59 +08:00
|
|
|
|
}
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
if (_yieldTab == YieldTab.Mine) { PonziPage(b); return; }
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
var w = d.Hot;
|
|
|
|
|
|
int day = StoreStation.GetDayCounter();
|
|
|
|
|
|
Columns(b, l => YieldPools(l, w, day), r =>
|
|
|
|
|
|
{
|
|
|
|
|
|
r.AddHeader(L.T("pool.side.h"));
|
|
|
|
|
|
r.AddLabel(L.T("predict.acct", WalletWindow.NameOf(w), Fmt.Num(w.Avail(Assets.CRC))));
|
|
|
|
|
|
AmountRow(r);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 08:11:03 +08:00
|
|
|
|
private static double _ponziRate = 0.04;
|
|
|
|
|
|
private static bool _ponziConfirm;
|
|
|
|
|
|
|
|
|
|
|
|
private static int LowerRep() => StoreReputation.GetStoreReputation(StoreReputation.FACTION_LOWER_LEVEL)?.GetReputation() ?? 0;
|
|
|
|
|
|
|
2026-11-04 08:16:57 +08:00
|
|
|
|
/// <summary>理财页:开一个高息理财产品,下层的人往里存钱;池子随时可以卷走,但每晚都有被举报的风险。开张和卷走是链上合约调用,用当前钱包,要签名;之后每晚垫付的利息从发行人钱包出。</summary>
|
2026-11-04 08:11:03 +08:00
|
|
|
|
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"));
|
2026-11-04 08:16:57 +08:00
|
|
|
|
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))));
|
2026-11-04 08:11:03 +08:00
|
|
|
|
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);
|
2026-11-04 10:04:00 +08:00
|
|
|
|
l.AddLabel(L.T("ponzi.wind", L.T(risk < 0.05 ? "ponzi.wind.low" : risk < 0.10 ? "ponzi.wind.mid" : "ponzi.wind.high")), risk >= 0.10 ? "ponzi_risk_note" : risk >= 0.05 ? "ponzi_risk_dim" : "ponzi_risk_ok");
|
2026-11-04 08:11:03 +08:00
|
|
|
|
}
|
2026-11-04 10:04:00 +08:00
|
|
|
|
else l.AddLabel(pz.Done ? L.T("ponzi.done") : day < Tuning.PonziMinDay ? L.T("ponzi.early", Tuning.PonziMinDay) : L.T("ponzi.idle"), "ponzi_status_dim");
|
2026-11-04 08:11:03 +08:00
|
|
|
|
}, 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;
|
2026-11-04 08:16:57 +08:00
|
|
|
|
var iw = d.Own(pz.Wallet) ?? d.Hot;
|
|
|
|
|
|
_message = L.T("hw.wait");
|
2026-11-04 08:11:03 +08:00
|
|
|
|
Open();
|
2026-11-04 08:16:57 +08:00
|
|
|
|
WalletWindow.OpenForBill(new WalletWindow.Bill
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("ponzi"), Asset = Assets.CRC, Amount = 0, Spend = 0, FeeAsset = BestFeeAsset(iw),
|
2026-11-04 08:16:57 +08:00
|
|
|
|
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();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-11-04 08:11:03 +08:00
|
|
|
|
});
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-11-04 09:32:59 +08:00
|
|
|
|
if (pz.Done || day < Tuning.PonziMinDay) return;
|
2026-11-04 08:11:03 +08:00
|
|
|
|
r.AddHeader(L.T("ponzi.open"));
|
2026-11-04 10:04:00 +08:00
|
|
|
|
r.BeginRow(4f);
|
2026-11-04 08:11:03 +08:00
|
|
|
|
foreach (double rate in Tuning.PonziRates)
|
|
|
|
|
|
{
|
|
|
|
|
|
double rt = rate;
|
|
|
|
|
|
string t = (rate * 100).ToString("0", CultureInfo.InvariantCulture) + "%";
|
2026-11-04 10:04:00 +08:00
|
|
|
|
PledgerSkin.Pick(r, t, Math.Abs(rt - _ponziRate) < 1e-9, () => { _ponziRate = rt; Open(); }, _picked);
|
2026-11-04 08:11:03 +08:00
|
|
|
|
}
|
|
|
|
|
|
r.End();
|
2026-11-04 10:04:00 +08:00
|
|
|
|
r.AddLabel(L.T("ponzi.ad", Fmt.Num(Tuning.PonziAdCost)), "ponzi_ad_dim");
|
2026-11-04 08:11:03 +08:00
|
|
|
|
r.AddButton(L.T("ponzi.start"), () =>
|
|
|
|
|
|
{
|
2026-11-04 08:16:57 +08:00
|
|
|
|
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");
|
2026-11-04 08:11:03 +08:00
|
|
|
|
Open();
|
2026-11-04 08:16:57 +08:00
|
|
|
|
WalletWindow.OpenForBill(new WalletWindow.Bill
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("ponzi"), Asset = Assets.CRC, Amount = Tuning.PonziAdCost,
|
2026-11-04 08:16:57 +08:00
|
|
|
|
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();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-11-04 08:11:03 +08:00
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 07:26:27 +08:00
|
|
|
|
private static int _predPick;
|
2026-11-04 07:53:12 +08:00
|
|
|
|
private enum PredTab { Markets, Intel }
|
|
|
|
|
|
private static PredTab _predTab = PredTab.Markets;
|
2026-11-04 07:26:27 +08:00
|
|
|
|
|
2026-11-04 07:53:12 +08:00
|
|
|
|
private static string P2(double x) => x.ToString("0.00", CultureInfo.InvariantCulture);
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>预测页:盘口(左边列表与最近结果,右边选中盘口的价格、线索、买卖)和情报(情报贩子)。下注和卖出是链上合约调用,用当前钱包的 CRC,要在钱包上签名;持仓记在签名的钱包名下。</summary>
|
2026-11-04 07:26:27 +08:00
|
|
|
|
private static void PredictPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
2026-11-04 07:53:12 +08:00
|
|
|
|
if (_predTab == PredTab.Intel && !Unlock.BlackUnlocked) _predTab = PredTab.Markets;
|
|
|
|
|
|
if (Unlock.BlackUnlocked)
|
|
|
|
|
|
{
|
2026-11-04 10:06:07 +08:00
|
|
|
|
b.BeginRow(4f);
|
2026-11-04 07:53:12 +08:00
|
|
|
|
foreach (PredTab t in Enum.GetValues(typeof(PredTab)))
|
|
|
|
|
|
{
|
|
|
|
|
|
var tab = t;
|
|
|
|
|
|
string name = L.T("predict.tab." + t.ToString().ToLowerInvariant());
|
2026-11-04 10:06:07 +08:00
|
|
|
|
PledgerSkin.Pick(b, name, tab == _predTab, () => { _predTab = tab; _message = null; Open(); }, _picked);
|
2026-11-04 07:53:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_predTab == PredTab.Intel) { IntelPage(b); return; }
|
2026-11-04 07:26:27 +08:00
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
var bk = d.Predict;
|
2026-11-04 07:33:42 +08:00
|
|
|
|
var w = d.Hot;
|
2026-11-04 07:26:27 +08:00
|
|
|
|
int day = StoreStation.GetDayCounter();
|
2026-11-04 08:36:17 +08:00
|
|
|
|
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();
|
2026-11-04 07:26:27 +08:00
|
|
|
|
var sel = open.Find(m => m.Id == _predPick) ?? open.FirstOrDefault();
|
|
|
|
|
|
_predPick = sel?.Id ?? 0;
|
|
|
|
|
|
Columns(b, l =>
|
|
|
|
|
|
{
|
|
|
|
|
|
l.AddHeader(L.T("predict.h"));
|
2026-11-04 07:33:42 +08:00
|
|
|
|
l.AddLabel(L.T("predict.acct", WalletWindow.NameOf(w), Fmt.Num(w.Avail(Assets.CRC))));
|
2026-11-04 07:26:27 +08:00
|
|
|
|
l.AddSeparator();
|
2026-11-04 08:47:34 +08:00
|
|
|
|
// 选中 PSC 价格盘时,在列表上方画日 K 和阈值线
|
2026-11-04 08:51:31 +08:00
|
|
|
|
if (sel != null && sel.Kind == PredictKind.Price) { CandleChart.Add(l, "predict", Assets.PSC, sel.Param); l.AddSeparator(); }
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (open.Count == 0) l.AddLabel(L.T("predict.none"), "pred_none_dim");
|
2026-11-04 07:26:27 +08:00
|
|
|
|
foreach (var m in open)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mk = m;
|
2026-11-04 07:33:42 +08:00
|
|
|
|
var all = bk.PosAll(m.Id);
|
2026-11-04 07:26:27 +08:00
|
|
|
|
string q = PredictGame.Question(m);
|
|
|
|
|
|
l.BeginRow(4f);
|
2026-11-04 10:06:07 +08:00
|
|
|
|
PledgerSkin.Pick(l, q, m.Id == _predPick, () => { _predPick = mk.Id; _message = null; Open(); }, _picked);
|
2026-11-04 07:26:27 +08:00
|
|
|
|
l.End();
|
|
|
|
|
|
string state = m.IsOpen(day) ? "" : " " + L.T("predict.closed");
|
2026-11-04 08:36:17 +08:00
|
|
|
|
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 ? "↑" : "↓") : "";
|
2026-11-04 10:06:07 +08:00
|
|
|
|
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, "pred_row_dim");
|
2026-11-04 08:36:17 +08:00
|
|
|
|
}
|
|
|
|
|
|
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))
|
2026-11-04 10:06:07 +08:00
|
|
|
|
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)), "pred_stat_dim");
|
2026-11-04 07:26:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (bk.Recent.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
l.AddSeparator();
|
|
|
|
|
|
foreach (var r in Enumerable.Reverse(bk.Recent))
|
2026-11-04 10:06:07 +08:00
|
|
|
|
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) : "—"), "pred_res_dim");
|
2026-11-04 07:26:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, r =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (sel == null) { r.AddHeader(L.T("predict.h")); return; }
|
|
|
|
|
|
r.AddHeader(L.T("predict.pick"));
|
|
|
|
|
|
r.AddLabel(PredictGame.Question(sel));
|
2026-11-04 07:53:12 +08:00
|
|
|
|
r.AddLabel(L.T("predict.price", P2(sel.PYes), P2(1 - sel.PYes)));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
foreach (var clue in PredictGame.Clues(sel)) r.AddLabel("· " + clue, "pred_clue_dim");
|
2026-11-04 07:33:42 +08:00
|
|
|
|
foreach (var pos in bk.PosAll(sel.Id))
|
2026-11-04 07:26:27 +08:00
|
|
|
|
{
|
2026-11-04 07:33:42 +08:00
|
|
|
|
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)));
|
2026-11-04 07:53:12 +08:00
|
|
|
|
if (pos.Wallet == w.Address)
|
|
|
|
|
|
{
|
2026-11-04 10:06:07 +08:00
|
|
|
|
r.AddLabel(L.T("predict.pos3", P2(pos.Cost / pos.Shares), P2(sel.PriceOf(pos.Yes))), "pred_pos_dim");
|
|
|
|
|
|
r.AddLabel(L.T("predict.pos2", Fmt.Num(pos.Cost), Fmt.Num(pos.Shares)), "pred_pos_dim");
|
|
|
|
|
|
r.AddLabel(L.T("predict.pos4", Fmt.Num(sel.SellValue(pos.Yes, pos.Shares) * (1 - Tuning.PredictFee))), "pred_pos_dim");
|
2026-11-04 07:53:12 +08:00
|
|
|
|
}
|
2026-11-04 07:26:27 +08:00
|
|
|
|
}
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (!sel.IsOpen(day)) { r.AddLabel(L.T("predict.closed.note"), "pred_closed_note"); return; }
|
2026-11-04 07:26:27 +08:00
|
|
|
|
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));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
r.AddLabel(L.T("predict.preview", L.T(yes ? "predict.yes" : "predict.no"), Fmt.Num(shares), P2(v / shares), Fmt.Num(shares - v)), "pred_prev_dim");
|
2026-11-04 07:26:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-04 10:06:07 +08:00
|
|
|
|
r.BeginRow(6f);
|
2026-11-04 07:26:27 +08:00
|
|
|
|
r.AddButton(L.T("predict.buy.yes"), () => PredictBuy(sel, true));
|
|
|
|
|
|
r.AddButton(L.T("predict.buy.no"), () => PredictBuy(sel, false));
|
|
|
|
|
|
r.End();
|
2026-11-04 10:06:07 +08:00
|
|
|
|
r.AddSpace(4f);
|
2026-11-04 07:26:27 +08:00
|
|
|
|
r.AddButton(L.T("predict.sell"), () => PredictSell(sel));
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 07:53:12 +08:00
|
|
|
|
/// <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"));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (!PredictGame.BrokerOpen) { l.AddLabel(L.T("intel.locked", Tuning.IntelRep), "intel_locked_note"); return; }
|
2026-11-04 07:53:12 +08:00
|
|
|
|
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();
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (d.Brief.Count == 0) l.AddLabel(L.T("intel.brief.none"), "intel_brief_dim");
|
2026-11-04 07:53:12 +08:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-11-04 10:06:07 +08:00
|
|
|
|
l.AddLabel(L.T("intel.brief.day", d.BriefDay), "intel_day_dim");
|
|
|
|
|
|
foreach (var line in d.Brief) l.AddLabel(line, "intel_line_dim");
|
2026-11-04 07:53:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, 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();
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (list.Count == 0) r.AddLabel(L.T("intel.tips.none"), "intel_tips_none_dim");
|
2026-11-04 07:53:12 +08:00
|
|
|
|
foreach (var m in list)
|
|
|
|
|
|
{
|
|
|
|
|
|
var mk = m;
|
|
|
|
|
|
r.AddLabel(PredictGame.Question(m));
|
|
|
|
|
|
var tip = d.Predict.TipFor(m.Id);
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (tip != null) { r.AddLabel(L.T("predict.clue.tip", L.T("predict.tier." + tip.Tier), L.T(tip.Claim ? "predict.yes" : "predict.no")), "intel_tip_dim"); continue; }
|
2026-11-04 07:53:12 +08:00
|
|
|
|
for (int t = 0; t < 3; t++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int tier = t;
|
2026-11-07 22:24:36 +08:00
|
|
|
|
r.AddButton(L.T("intel.tip.buy", (int)Math.Round(Tuning.IntelTipAcc[t] * 100), Fmt.Num(Tuning.IntelTipPrice[t] * m.Loss)), () => IntelBuyTip(mk, tier));
|
2026-11-04 15:34:38 +08:00
|
|
|
|
r.Last?.SetPreferredSize(RightW - 30f, 28f * F);
|
2026-11-04 10:34:49 +08:00
|
|
|
|
if (t < 2) r.AddSpace(2f);
|
2026-11-04 07:53:12 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("intel"), Asset = Assets.CRT, Amount = price, Summary = L.T("intel.brief.sum", Fmt.Num(price)),
|
2026-11-04 07:53:12 +08:00
|
|
|
|
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;
|
2026-11-07 22:24:36 +08:00
|
|
|
|
double price = Tuning.IntelTipPrice[tier] * m.Loss;
|
2026-11-04 07:53:12 +08:00
|
|
|
|
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
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("intel"), Asset = Assets.CRT, Amount = price, Summary = L.T("intel.tip.sum", Fmt.Num(price), PredictGame.Question(m)),
|
2026-11-04 07:53:12 +08:00
|
|
|
|
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();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 07:26:27 +08:00
|
|
|
|
private static void PredictBuy(PredictMarket m, bool yes)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!ReadAmount(out double v)) return;
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-04 07:33:42 +08:00
|
|
|
|
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");
|
2026-11-04 07:26:27 +08:00
|
|
|
|
Open();
|
2026-11-04 07:33:42 +08:00
|
|
|
|
WalletWindow.OpenForBill(new WalletWindow.Bill
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("predict"), Asset = Assets.CRC, Amount = v,
|
2026-11-04 07:33:42 +08:00
|
|
|
|
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();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-11-04 07:26:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void PredictSell(PredictMarket m)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-04 07:33:42 +08:00
|
|
|
|
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");
|
2026-11-04 07:26:27 +08:00
|
|
|
|
Open();
|
2026-11-04 07:33:42 +08:00
|
|
|
|
WalletWindow.OpenForBill(new WalletWindow.Bill
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("predict"), Asset = Assets.CRC, Amount = 0, Spend = 0, FeeAsset = BestFeeAsset(w),
|
2026-11-04 07:33:42 +08:00
|
|
|
|
Summary = L.T("predict.sum.sell", PredictGame.Question(m)),
|
|
|
|
|
|
Execute = pw =>
|
|
|
|
|
|
{
|
2026-11-08 14:17:41 +08:00
|
|
|
|
var res = d.Predict.SellAll(pw, m, StoreStation.GetDayCounter(), Session.Price, out double got, out double taint, out double over);
|
2026-11-04 07:33:42 +08:00
|
|
|
|
_message = res == PredictResult.Ok ? L.T("predict.sold", Fmt.Num(got)) : L.T("predict.err." + res);
|
2026-11-08 14:17:41 +08:00
|
|
|
|
if (res == PredictResult.Ok && taint + over > 1e-9)
|
2026-11-04 08:36:17 +08:00
|
|
|
|
{
|
2026-11-08 14:17:41 +08:00
|
|
|
|
_message = L.T("predict.sold.caught", Fmt.Num(got), Fmt.Credits(taint)) + TaintCrime.Commit(over);
|
|
|
|
|
|
Log.Info($"内幕卖出:收回 {got:0},污点 +{taint:0},超出 {over:0}");
|
2026-11-04 08:36:17 +08:00
|
|
|
|
}
|
2026-11-04 07:33:42 +08:00
|
|
|
|
if (res == PredictResult.Ok) Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-11-04 07:26:27 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 00:26:50 +08:00
|
|
|
|
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);
|
2026-11-04 08:51:31 +08:00
|
|
|
|
if (res == PerpResult.Ok) CandleChart.Trade(_perpProduct, v * _perpLev);
|
2026-11-04 00:26:50 +08:00
|
|
|
|
_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;
|
2026-11-04 08:51:31 +08:00
|
|
|
|
CandleChart.Trade(_perpProduct, pos.Notional(PerpSpot(_perpProduct)));
|
2026-11-04 00:26:50 +08:00
|
|
|
|
double paid = d.Perps.Close(d.Poinbase, _perpProduct, PerpSpot(_perpProduct), StoreStation.GetDayCounter(), out double deficit);
|
2026-11-08 14:17:41 +08:00
|
|
|
|
// 操纵:平仓盈利,而且盈利方向和玩家自己推价的方向一致,按推价幅度被查:盈利照常留在 Poinbase 账户,这笔盈利记污点(带上限)
|
2026-11-04 08:03:44 +08:00
|
|
|
|
double push = d.Market.PlayerPush, profit = paid - margin;
|
|
|
|
|
|
bool aligned = _perpProduct == "PSC" && profit > 0 && (pos.Long ? push > 0 : push < 0);
|
2026-11-04 09:04:50 +08:00
|
|
|
|
if (aligned && d.Rng.Chance(Math.Min(1, Market.ManipChance(push) * StoryMods.Current.Enforce(StoreStation.GetDayCounter()))))
|
2026-11-04 08:03:44 +08:00
|
|
|
|
{
|
|
|
|
|
|
int day = StoreStation.GetDayCounter();
|
2026-11-08 14:17:41 +08:00
|
|
|
|
double over = d.Poinbase.AddTaintCapped(profit, Session.Price);
|
2026-11-04 08:03:44 +08:00
|
|
|
|
News.Show("pd_news_manip_" + day, L.T("news.manip.title"), L.T("news.manip.desc"), 3);
|
2026-11-08 14:17:41 +08:00
|
|
|
|
_message = L.T("perp.manip", Fmt.Credits(profit), Fmt.Credits(profit - over)) + TaintCrime.Commit(over);
|
|
|
|
|
|
Log.Info($"永续操纵被查:盈利 {profit:0} 记污点 {profit - over:0},超出 {over:0},推价 {push:0.000}");
|
2026-11-04 08:03:44 +08:00
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-11-04 00:26:50 +08:00
|
|
|
|
_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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 22:58:24 +08:00
|
|
|
|
/// <summary>Poinbase 提到当前钱包:托管账户直接转,不用签名。污点按比例随币走。</summary>
|
|
|
|
|
|
/// <summary>钱包页:全部硬件钱包,连接(设为当前)、打开实体钱包。</summary>
|
2026-11-05 16:31:42 +08:00
|
|
|
|
private enum WalletTab { List, Explorer }
|
|
|
|
|
|
private static WalletTab _walletTab = WalletTab.List;
|
|
|
|
|
|
|
2026-11-03 22:58:24 +08:00
|
|
|
|
private static void WalletsPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-05 21:32:30 +08:00
|
|
|
|
TabRows(b, Enum.GetValues(typeof(WalletTab)).Cast<WalletTab>().Select(t => { var tab = t; return new TabDef { Name = L.T("wallets.sub." + t.ToString().ToLowerInvariant()), On = tab == _walletTab, Click = () => { _walletTab = tab; _message = null; Open(); } }; }).ToList(), false);
|
2026-11-05 16:31:42 +08:00
|
|
|
|
if (_walletTab == WalletTab.Explorer) { ExplorerPage(b); return; }
|
2026-11-04 10:00:25 +08:00
|
|
|
|
b.AddLabel(L.T("wallets.count", d.Wallets.Count, Tuning.WalletMax), "wallets_count_dim");
|
2026-11-05 01:37:23 +08:00
|
|
|
|
if (d.Wallets.Count == 0) { b.AddLabel(L.T("wallets.none"), "wallets_none_dim"); return; }
|
2026-11-04 15:34:38 +08:00
|
|
|
|
bool wide = L.Wide && Ext < 100f;
|
2026-11-04 16:23:41 +08:00
|
|
|
|
float k = Ext < 100f ? 1f : (ScreenW - 70f) / 740f;
|
|
|
|
|
|
bool big = Ext >= 100f;
|
|
|
|
|
|
float btnCut = wide ? 112f : big ? 78f : 150f, statW = wide ? 92f : big ? 70f : 84f;
|
|
|
|
|
|
var cols = (wide ? new[] { 100f, 92f, 62f, 62f, 372f } : Ext < 100f ? new[] { 100f, 110f, 85f, 70f, 375f } : new[] { 165f, 100f, 70f, 115f, 290f }).Select(x => x * k).ToArray();
|
2026-11-04 10:00:25 +08:00
|
|
|
|
var aligns = new[] { TextAnchor.MiddleLeft, TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleLeft };
|
2026-11-04 15:34:38 +08:00
|
|
|
|
TableHeader(b, 740f * k, cols, aligns, L.T("wallets.col.name"), L.T("wallets.col.addr"), L.T("col.value"), L.T("wallets.col.taint"), "");
|
2026-11-03 22:58:24 +08:00
|
|
|
|
for (int i = 0; i < d.Wallets.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
var w = d.Wallets[i];
|
|
|
|
|
|
int idx = i;
|
2026-11-04 10:00:25 +08:00
|
|
|
|
bool isCurrent = idx == d.Current;
|
2026-11-04 11:06:38 +08:00
|
|
|
|
// 一行三块:数据格(整行反色的是这一块)、操作键(和顶栏页签一样的黑色触控键,放在纸面上)、连接状态靠右
|
|
|
|
|
|
b.BeginRow(4f, TextAnchor.MiddleLeft);
|
2026-11-04 15:34:38 +08:00
|
|
|
|
b.BeginPanel(cols[0] + cols[1] + cols[2] + cols[3] + 8f, 32f * F, TextAnchor.MiddleLeft, 4f, 0f);
|
2026-11-04 10:00:25 +08:00
|
|
|
|
_rows.Add(new PledgerSkin.RowDef
|
|
|
|
|
|
{
|
|
|
|
|
|
Root = b.Last.root,
|
|
|
|
|
|
Image = b.Last.image,
|
|
|
|
|
|
Selected = isCurrent,
|
|
|
|
|
|
OnClick = () => { d.Current = idx; Session.Save(); Open(); }
|
|
|
|
|
|
});
|
|
|
|
|
|
b.BeginRow(0f, TextAnchor.MiddleLeft);
|
|
|
|
|
|
Cell(b, cols[0], (isCurrent ? "● " : "") + WalletWindow.NameOf(w), false, TextAnchor.MiddleLeft);
|
|
|
|
|
|
Cell(b, cols[1], w.Address, false, TextAnchor.MiddleLeft);
|
|
|
|
|
|
Cell(b, cols[2], Fmt.Credits(w.Value(Session.Price)), false, TextAnchor.MiddleRight);
|
|
|
|
|
|
Cell(b, cols[3], Fmt.Credits(w.Taint), false, TextAnchor.MiddleRight);
|
2026-11-04 11:06:38 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
b.End();
|
2026-11-04 16:23:41 +08:00
|
|
|
|
b.BeginPanel(cols[4] - btnCut * k, 32f * F, TextAnchor.MiddleLeft, 0f, 0f);
|
2026-11-04 10:00:25 +08:00
|
|
|
|
_clear.Add(b.Last.image);
|
|
|
|
|
|
b.BeginRow(4f, TextAnchor.MiddleLeft);
|
2026-11-04 10:59:06 +08:00
|
|
|
|
RowKey(b, L.T("wallets.open"), () => WalletWindow.Open(w));
|
|
|
|
|
|
RowKey(b, L.T("hw.copy"), () => { Clipboard.Copy(w.Address); _message = L.T("hw.copied.addr", w.Address); Open(); });
|
|
|
|
|
|
RowKey(b, L.T("wallets.rename"), () => { _renamePick = w.Address; _renameText = w.Label ?? ""; _message = null; Open(); });
|
2026-11-05 01:37:23 +08:00
|
|
|
|
RowKey(b, _destroyPick == w.Address ? L.T("wallets.destroy.sure") : L.T("wallets.destroy"), () => DestroyWallet(w));
|
2026-11-04 10:59:06 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
b.End();
|
2026-11-04 16:23:41 +08:00
|
|
|
|
b.BeginPanel(statW * k, 32f * F, TextAnchor.MiddleRight, 0f, 0f);
|
2026-11-04 10:59:06 +08:00
|
|
|
|
_clear.Add(b.Last.image);
|
|
|
|
|
|
b.BeginRow(0f, TextAnchor.MiddleRight);
|
|
|
|
|
|
if (idx != d.Current) RowKey(b, L.T("wallets.use"), () => { d.Current = idx; Session.Save(); Open(); });
|
2026-11-04 14:55:11 +08:00
|
|
|
|
else { b.AddLabel(L.T("wallets.current"), "wallets_cur_dim"); PledgerSkin.FitCurrent(b); }
|
2026-11-03 22:58:24 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
2026-11-05 21:32:30 +08:00
|
|
|
|
if (d.Connected)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 选中的钱包:下面列出它持有的币和数量
|
|
|
|
|
|
b.AddSeparator();
|
|
|
|
|
|
b.AddHeader(L.T("web3.wallet.h") + " " + WalletWindow.NameOf(d.Hot));
|
|
|
|
|
|
Holdings(b, d.Hot, a => true);
|
|
|
|
|
|
}
|
2026-11-03 23:05:13 +08:00
|
|
|
|
if (_renamePick != null) RenameRow(b);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 10:59:06 +08:00
|
|
|
|
/// <summary>表格行里的操作键:固定 24 高,垂直居中(行高 32)。</summary>
|
|
|
|
|
|
private static void RowKey(CustomUIBuilder b, string text, Action onClick)
|
|
|
|
|
|
{
|
|
|
|
|
|
b.AddButton(text, onClick);
|
|
|
|
|
|
var go = b.Last.root;
|
|
|
|
|
|
if (go == null) return;
|
|
|
|
|
|
var le = go.GetComponent<LayoutElement>() ?? go.AddComponent<LayoutElement>();
|
2026-11-04 15:34:38 +08:00
|
|
|
|
le.minHeight = 26f * F; le.preferredHeight = 26f * F; le.flexibleHeight = 0f;
|
2026-11-04 10:59:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 23:05:13 +08:00
|
|
|
|
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);
|
2026-11-04 10:00:25 +08:00
|
|
|
|
_clear.Add(b.Last.image);
|
2026-11-03 23:05:13 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 01:37:23 +08:00
|
|
|
|
/// <summary>销毁钱包:里面的币永久丢失;点两次确认。之后回到这个地址的退款、回款、混币提回都丢了(发给不存在的地址)。销毁的是当前连接的钱包时,变成没有连接。</summary>
|
2026-11-03 23:05:13 +08:00
|
|
|
|
private static void DestroyWallet(Wallet w)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
int idx = d.Wallets.IndexOf(w);
|
2026-11-05 01:37:23 +08:00
|
|
|
|
if (idx < 0) return;
|
|
|
|
|
|
if (_destroyPick != w.Address) { _destroyPick = w.Address; _message = L.T("wallets.destroy.warn", WalletWindow.NameOf(w)); Open(); return; }
|
2026-11-03 23:05:13 +08:00
|
|
|
|
_destroyPick = null;
|
|
|
|
|
|
string name = WalletWindow.NameOf(w);
|
|
|
|
|
|
WalletWindow.Close(w);
|
2026-11-05 01:37:23 +08:00
|
|
|
|
var current = d.Connected ? d.Hot : null;
|
2026-11-03 23:05:13 +08:00
|
|
|
|
d.Wallets.RemoveAt(idx);
|
2026-11-05 01:37:23 +08:00
|
|
|
|
d.Current = current == null || current == w ? -1 : d.Wallets.IndexOf(current);
|
2026-11-03 23:05:13 +08:00
|
|
|
|
Log.Info($"销毁硬件钱包 {w.Address}{(w.Poisoned ? "(被投毒)" : "")}");
|
|
|
|
|
|
Session.Save();
|
|
|
|
|
|
_message = L.T("wallets.destroyed", name, w.Address);
|
|
|
|
|
|
Open();
|
2026-11-03 22:58:24 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <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();
|
2026-11-05 16:31:42 +08:00
|
|
|
|
_page = Page.Wallets; _walletTab = WalletTab.List;
|
2026-11-03 22:58:24 +08:00
|
|
|
|
_message = L.T("wallets.added", WalletWindow.NameOf(w), w.Address);
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 16:31:42 +08:00
|
|
|
|
private sealed class TabDef { public string Name; public bool On; public Action Click; }
|
|
|
|
|
|
private static float TabPx(string txt) => txt.Sum(c => c > 0x2E80 ? 18f : 9f) + 28f;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 一排页签:按字数估宽,超出一行就折到下一行。connect 为真时把连接钱包的控件放在最后一行右端,放不下就单独一行。
|
|
|
|
|
|
/// </summary>
|
|
|
|
|
|
private static void TabRows(CustomUIBuilder b, List<TabDef> tabs, bool connect)
|
|
|
|
|
|
{
|
|
|
|
|
|
float avail = ScreenW - 40f, used = 0f;
|
|
|
|
|
|
bool open = false;
|
|
|
|
|
|
foreach (var t in tabs)
|
|
|
|
|
|
{
|
|
|
|
|
|
float wpx = TabPx(t.Name);
|
|
|
|
|
|
if (open && used + wpx > avail) { b.End(); open = false; used = 0f; }
|
|
|
|
|
|
if (!open) { b.BeginRow(4f, TextAnchor.MiddleLeft); open = true; }
|
|
|
|
|
|
used += wpx + 4f;
|
|
|
|
|
|
PledgerSkin.Pick(b, t.Name, t.On, t.Click, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!connect) { if (open) b.End(); return; }
|
|
|
|
|
|
float widget = 120f + (Session.Data.Connected ? TabPx(L.T("web3.conn", WalletWindow.NameOf(Session.Data.Hot), ChainAddress.Short(Session.Data.Hot.Address))) : TabPx(L.T("web3.connect")));
|
|
|
|
|
|
if (open && used + widget > avail) { b.End(); open = false; }
|
|
|
|
|
|
if (!open) b.BeginRow(4f, TextAnchor.MiddleLeft);
|
|
|
|
|
|
b.AddFlexibleSpace();
|
|
|
|
|
|
ConnectWidget(b);
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool GrayAvailable => Unlock.MixerUnlocked || Unlock.BlackUnlocked;
|
|
|
|
|
|
private enum GrayTab { Mixer, Black }
|
|
|
|
|
|
private static GrayTab _grayTab = GrayTab.Mixer;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>灰网:混币器和商店,各自在人脉网络里解锁,解锁了哪个就有哪个。</summary>
|
|
|
|
|
|
private static void GrayPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_grayTab == GrayTab.Mixer && !Unlock.MixerUnlocked) _grayTab = GrayTab.Black;
|
|
|
|
|
|
if (_grayTab == GrayTab.Black && !Unlock.BlackUnlocked) _grayTab = GrayTab.Mixer;
|
|
|
|
|
|
if (Unlock.MixerUnlocked && Unlock.BlackUnlocked)
|
2026-11-05 21:32:30 +08:00
|
|
|
|
TabRows(b, Enum.GetValues(typeof(GrayTab)).Cast<GrayTab>().Select(t => { var tab = t; return new TabDef { Name = L.T("gray.sub." + t.ToString().ToLowerInvariant()), On = tab == _grayTab, Click = () => { _grayTab = tab; _message = null; Open(); } }; }).ToList(), false);
|
2026-11-05 16:31:42 +08:00
|
|
|
|
if (!Session.Data.Connected) b.AddLabel(L.T("web3.err.noconn"), "term_note");
|
|
|
|
|
|
if (_grayTab == GrayTab.Mixer) MixerPage(b); else BlackPage(b);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 15:23:15 +08:00
|
|
|
|
/// <summary>Web3:链上钱包与去中心化交易。</summary>
|
|
|
|
|
|
private static void Web3Page(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var w = Session.Data.Hot;
|
2026-11-05 16:31:42 +08:00
|
|
|
|
if (_web3Tab == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter())) _web3Tab = Web3Tab.Trade;
|
|
|
|
|
|
TabRows(b, Enum.GetValues(typeof(Web3Tab)).Cast<Web3Tab>()
|
|
|
|
|
|
.Where(t => t != Web3Tab.Predict || PredictGame.Available(StoreStation.GetDayCounter()))
|
2026-11-05 21:32:30 +08:00
|
|
|
|
.Select(t => { var tab = t; return new TabDef { Name = L.T("web3.sub." + t.ToString().ToLowerInvariant()), On = tab == _web3Tab, Click = () => { _web3Tab = tab; _message = null; Open(); } }; }).ToList(), false);
|
2026-11-05 16:31:42 +08:00
|
|
|
|
if (!Session.Data.Connected) b.AddLabel(L.T("web3.err.noconn"), "term_note");
|
|
|
|
|
|
if (_web3Tab == Web3Tab.Stake) { StakePage(b); return; }
|
|
|
|
|
|
if (_web3Tab == Web3Tab.Predict) { PredictPage(b); return; }
|
|
|
|
|
|
if (_web3Tab == Web3Tab.Ponzi) { YieldPage(b); return; }
|
|
|
|
|
|
if (_web3Tab == Web3Tab.Airdrop) { AirdropPage(b); return; }
|
2026-11-03 15:41:42 +08:00
|
|
|
|
Columns(b, l =>
|
|
|
|
|
|
{
|
2026-11-05 01:37:23 +08:00
|
|
|
|
l.AddHeader(L.T("web3.wallet.h") + (Session.Data.Connected ? " " + WalletWindow.NameOf(w) : ""));
|
2026-11-03 15:41:42 +08:00
|
|
|
|
Holdings(l, w, a => true);
|
|
|
|
|
|
l.AddSeparator();
|
|
|
|
|
|
l.AddLabel(L.T("wallet.total", Fmt.Credits(w.Value(Session.Price))));
|
2026-11-04 10:04:00 +08:00
|
|
|
|
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)), "taint_note");
|
2026-11-05 07:09:05 +08:00
|
|
|
|
PriceTable(l);
|
2026-11-04 08:51:31 +08:00
|
|
|
|
l.AddSeparator();
|
2026-11-04 10:04:00 +08:00
|
|
|
|
CandleChart.AddPicker(l, "web3", Assets.PSC, Open, _picked);
|
2026-11-03 15:41:42 +08:00
|
|
|
|
}, r =>
|
|
|
|
|
|
{
|
|
|
|
|
|
r.AddHeader(L.T("trade.h"));
|
2026-11-05 07:09:05 +08:00
|
|
|
|
SwapBlock(r);
|
2026-11-03 22:04:31 +08:00
|
|
|
|
if (Unlock.BlackUnlocked && BlackMarket.DeskOpen(StoreReputation.GetBMReputation())) PoneroBlock(r);
|
2026-11-03 15:41:42 +08:00
|
|
|
|
});
|
2026-11-03 15:23:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 01:37:23 +08:00
|
|
|
|
private static bool _connectOpen;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>Web3 页标签栏最右:已连接显示钱包名、地址和"断开连接",没有连接显示"连接钱包"。状态和钱包页的"已连接"是同一份。</summary>
|
|
|
|
|
|
private static void ConnectWidget(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
if (d.Connected)
|
|
|
|
|
|
{
|
|
|
|
|
|
var w = d.Hot;
|
|
|
|
|
|
b.AddLabel(L.T("web3.conn", WalletWindow.NameOf(w), ChainAddress.Short(w.Address)));
|
|
|
|
|
|
PledgerSkin.Pick(b, L.T("web3.disconnect"), false, () => { d.Current = -1; _connectOpen = false; WalletWindow.Reset(); Session.Save(); Open(); }, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
else PledgerSkin.Pick(b, L.T("web3.connect"), _connectOpen, () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (d.Wallets.Count == 0) { _message = L.T("wallets.none"); Open(); return; }
|
|
|
|
|
|
if (d.Wallets.Count == 1) { d.Current = 0; Session.Save(); Open(); return; }
|
|
|
|
|
|
_connectOpen = !_connectOpen; Open();
|
|
|
|
|
|
}, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>有多个钱包时,点"连接钱包"在标签栏下面列出钱包,选一个连接。</summary>
|
|
|
|
|
|
private static void ConnectPicker(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
if (!_connectOpen || d.Connected) { _connectOpen = false; return; }
|
|
|
|
|
|
b.BeginRow(4f);
|
|
|
|
|
|
for (int i = 0; i < d.Wallets.Count; i++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int idx = i; var w = d.Wallets[i];
|
|
|
|
|
|
PledgerSkin.Pick(b, WalletWindow.NameOf(w) + " " + ChainAddress.Short(w.Address), false, () => { d.Current = idx; _connectOpen = false; Session.Save(); Open(); }, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 07:22:01 +08:00
|
|
|
|
private static string _explorerQuery = "";
|
2026-11-05 22:25:48 +08:00
|
|
|
|
private static readonly float[] ExplWBase = new[] { 62f, 56f, 212f, 212f, 118f };
|
2026-11-05 07:22:01 +08:00
|
|
|
|
private static float[] ExplW => Sc(ExplWBase);
|
2026-11-05 22:25:48 +08:00
|
|
|
|
private static readonly TextAnchor[] ExplAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleLeft, TextAnchor.MiddleLeft, TextAnchor.MiddleLeft, TextAnchor.MiddleRight };
|
2026-11-05 07:22:01 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>地址的显示名:自己的钱包、Poinbase、混币器、全网矿池、黑市卖家,其余是缩写。</summary>
|
2026-11-05 22:25:48 +08:00
|
|
|
|
/// <summary>地址的名称:自己的钱包、矿池、Poinbase、混币器、灰网平台、兑换处、商家类型;没有名称返回 null。</summary>
|
|
|
|
|
|
private static string AddressName(string a)
|
2026-11-05 07:22:01 +08:00
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-05 22:25:48 +08:00
|
|
|
|
if (string.IsNullOrEmpty(a)) return null;
|
2026-11-05 21:43:04 +08:00
|
|
|
|
if (a == "network") a = d.Address("pool");
|
2026-11-05 07:22:01 +08:00
|
|
|
|
var own = d.Own(a);
|
2026-11-05 22:25:48 +08:00
|
|
|
|
if (own != null) return WalletWindow.NameOf(own);
|
|
|
|
|
|
if (ChainAddress.Same(a, d.Address("pool"))) return L.T("explorer.pool");
|
|
|
|
|
|
if (ChainAddress.Same(a, d.Address("poinbase"))) return "Poinbase";
|
2026-11-07 22:39:18 +08:00
|
|
|
|
// 其他收款方、合约按登记的键取名:空投、收益池带自己的名字
|
|
|
|
|
|
string key = d.Addresses.FirstOrDefault(kv => ChainAddress.Same(kv.Value, a)).Key;
|
|
|
|
|
|
if (key != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (key.StartsWith("airdrop:") || key.StartsWith("yield:")) return key.Substring(key.IndexOf(':') + 1);
|
|
|
|
|
|
switch (key)
|
|
|
|
|
|
{
|
|
|
|
|
|
case "mixer": return L.T("explorer.mixer");
|
|
|
|
|
|
case "platform": return L.T("merchant.Platform");
|
|
|
|
|
|
case "desk": return L.T("explorer.desk");
|
|
|
|
|
|
case "dex": return "DEX";
|
|
|
|
|
|
case "staking": return L.T("stake.h");
|
|
|
|
|
|
case "cra": return L.T("mint.h");
|
|
|
|
|
|
case "predict": return L.T("predict.h");
|
|
|
|
|
|
case "intel": return L.T("intel.h");
|
|
|
|
|
|
case "ponzi": return L.T("ponzi.h");
|
|
|
|
|
|
case "tumbler": return "Tumbler";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-11-05 22:25:48 +08:00
|
|
|
|
var sl = d.Black.Sellers.FirstOrDefault(x => ChainAddress.Same(x.Name, a));
|
|
|
|
|
|
return sl != null ? L.T("merchant." + sl.Type) : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string AddressFull(string a) => string.IsNullOrEmpty(a) ? "—" : a == "network" ? Session.Data.Address("pool") : ChainAddress.Normalize(a);
|
|
|
|
|
|
|
|
|
|
|
|
private static float TextPx(string t) => t.Sum(c => c > 0x2E80 ? 16f : 8.2f) * F;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>按列宽选显示内容:完整地址加名称放得下就都显示;放不下先去掉名称,再用缩写;被省略的地方给悬停提示(完整地址和名称)。</summary>
|
|
|
|
|
|
private static void AddressCell(string a, float width, out string text, out string tip)
|
|
|
|
|
|
{
|
|
|
|
|
|
string full = AddressFull(a), name = AddressName(a);
|
|
|
|
|
|
string both = name != null ? full + " (" + name + ")" : full;
|
|
|
|
|
|
float avail = (width - 8f) * 1.15f;
|
|
|
|
|
|
tip = null;
|
|
|
|
|
|
if (TextPx(both) <= avail) { text = both; return; }
|
|
|
|
|
|
tip = both;
|
|
|
|
|
|
text = TextPx(full) <= avail ? full : ChainAddress.Short(full);
|
2026-11-05 07:22:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>浏览器:公共数据,不需要连接钱包。查任意地址的转账,只显示真实发生的。</summary>
|
|
|
|
|
|
private static void ExplorerPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
b.BeginPanel(RightW - 30f, -1f, TextAnchor.MiddleCenter, 0f, 0f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
|
|
|
|
|
b.AddInput(L.T("explorer.search"), _explorerQuery, s => _explorerQuery = s, "explorer", s => { _explorerQuery = s; Open(); });
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
if (d.Wallets.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
b.BeginRow(4f);
|
|
|
|
|
|
PledgerSkin.Pick(b, L.T("explorer.all"), string.IsNullOrWhiteSpace(_explorerQuery), () => { _explorerQuery = ""; Open(); }, _picked);
|
|
|
|
|
|
foreach (var ow in d.Wallets)
|
|
|
|
|
|
{
|
|
|
|
|
|
var own = ow;
|
|
|
|
|
|
PledgerSkin.Pick(b, WalletWindow.NameOf(own), ChainAddress.Same(_explorerQuery, own.Address), () => { _explorerQuery = own.Address; Open(); }, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
b.AddSeparator();
|
|
|
|
|
|
var rows = d.Ledger.Of(_explorerQuery, Tuning.LedgerShow);
|
|
|
|
|
|
if (rows.Count == 0) { b.AddLabel(L.T("explorer.none"), "explorer_none_dim"); return; }
|
2026-11-05 22:25:48 +08:00
|
|
|
|
TableHeader(b, ExplW.Sum() + 8f, ExplW, ExplAligns, L.T("explorer.col.block"), L.T("explorer.col.day"), L.T("explorer.col.from"), L.T("explorer.col.to"), L.T("explorer.col.amount"));
|
|
|
|
|
|
var ew = ExplW;
|
2026-11-05 07:22:01 +08:00
|
|
|
|
foreach (var e in rows)
|
2026-11-05 22:25:48 +08:00
|
|
|
|
{
|
|
|
|
|
|
AddressCell(e.From, ew[2], out string fromText, out string fromTip);
|
|
|
|
|
|
AddressCell(e.To, ew[3], out string toText, out string toTip);
|
|
|
|
|
|
TableRow(b, ew.Sum() + 8f, ew, ExplAligns,
|
|
|
|
|
|
new[] { "#" + e.Block, L.T("explorer.day", e.Day), fromText, toText, Fmt.Num(e.Amount) + " " + e.Asset },
|
|
|
|
|
|
false, null, new[] { null, null, fromTip, toTip, null });
|
|
|
|
|
|
}
|
2026-11-05 07:22:01 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 22:04:31 +08:00
|
|
|
|
private enum BlackTab { Buy, Sell, Orders }
|
|
|
|
|
|
private static BlackTab _blackTab = BlackTab.Buy;
|
2026-11-03 19:48:26 +08:00
|
|
|
|
private static PayMode _mode = PayMode.Escrow;
|
2026-11-03 19:35:48 +08:00
|
|
|
|
private static string _deskAsset = Assets.PSC;
|
|
|
|
|
|
private static bool _deskIn = true;
|
2026-11-03 20:06:32 +08:00
|
|
|
|
private static readonly System.Collections.Generic.Dictionary<string, Sprite> IconCache = new System.Collections.Generic.Dictionary<string, Sprite>();
|
2026-11-03 19:35:48 +08:00
|
|
|
|
|
2026-11-03 20:06:32 +08:00
|
|
|
|
/// <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>
|
2026-11-03 19:35:48 +08:00
|
|
|
|
private static void BlackPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
int rep = StoreReputation.GetBMReputation();
|
|
|
|
|
|
int day = StoreStation.GetDayCounter();
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-04 10:10:34 +08:00
|
|
|
|
if (Night.ActiveEvents().Contains(MarketEvent.Crackdown)) { b.AddLabel(L.T("black.offline"), "black_offline_note"); return; }
|
|
|
|
|
|
if (!BlackMarket.Open(rep)) { b.AddLabel(L.T("black.closed", Tuning.BmOpenRep, rep), "black_closed_note"); return; }
|
|
|
|
|
|
b.BeginRow(4f);
|
2026-11-03 19:48:26 +08:00
|
|
|
|
foreach (BlackTab t in Enum.GetValues(typeof(BlackTab)))
|
2026-11-03 19:35:48 +08:00
|
|
|
|
{
|
2026-11-03 19:48:26 +08:00
|
|
|
|
var tab = t;
|
|
|
|
|
|
string name = L.T("black.sub." + t.ToString().ToLowerInvariant());
|
2026-11-05 21:32:30 +08:00
|
|
|
|
PledgerSkin.Pick(b, name, tab == _blackTab, () => { _blackTab = tab; _message = null; _sellView = SellView.Showcase; _stage.Clear(); _delist.Clear(); _coinsOpen = false; _checkout = false; Open(); }, _picked);
|
2026-11-03 19:35:48 +08:00
|
|
|
|
}
|
2026-11-03 19:48:26 +08:00
|
|
|
|
b.End();
|
2026-11-04 10:10:34 +08:00
|
|
|
|
if (d.Black.Closed(day)) { b.AddLabel(L.T("black.rugged", d.Black.ClosedUntil - day + 1), "black_rugged_note"); return; }
|
2026-11-04 11:18:45 +08:00
|
|
|
|
if (_checkout) { CheckoutPage(b, d); return; }
|
2026-11-03 22:04:31 +08:00
|
|
|
|
if (_blackTab == BlackTab.Orders) { OrdersPage(b, day); return; }
|
2026-11-03 20:23:21 +08:00
|
|
|
|
if (_blackTab == BlackTab.Sell) { SellPage(b, day); return; }
|
2026-11-03 22:04:31 +08:00
|
|
|
|
BuyPage(b, d, rep);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 11:06:38 +08:00
|
|
|
|
/// <summary>
|
|
|
|
|
|
/// 买货:左栏是卖家列表(第一个是平台现货,立即到货但贵,其余是商家,便宜但要等),右栏是这家的货架。
|
|
|
|
|
|
/// 点"购买"后右栏换成付款面板:选数量(和付款方式)、付款币种;商家不收手里的币时可以用 Web3 内置的自动换币(多付滑点)。论坛情报在最下面。
|
|
|
|
|
|
/// </summary>
|
2026-11-03 22:04:31 +08:00
|
|
|
|
private static void BuyPage(CustomUIBuilder b, SaveData d, int rep)
|
|
|
|
|
|
{
|
2026-11-03 19:35:48 +08:00
|
|
|
|
var w = d.Hot;
|
2026-11-03 22:04:31 +08:00
|
|
|
|
var sellers = d.Black.Sellers;
|
|
|
|
|
|
if (_merchant >= sellers.Count) _merchant = -1;
|
|
|
|
|
|
bool platform = _merchant < 0;
|
2026-11-04 11:06:38 +08:00
|
|
|
|
b.AddLabel(L.T("black.rep", rep, BlackMarket.MaxLevel(rep)) + (w.Taint > 0.5 ? " " + L.T("wallet.taint", Fmt.Credits(w.Taint)) : ""), "black_rep_dim");
|
|
|
|
|
|
b.BeginRow(8f, TextAnchor.UpperLeft);
|
|
|
|
|
|
b.BeginPanel(SellerW + 8f, -1f, TextAnchor.UpperLeft, 0f, 2f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
|
|
|
|
|
var names = new System.Collections.Generic.List<string> { L.T("merchant.Platform") };
|
|
|
|
|
|
for (int k = 0; k < sellers.Count; k++) names.Add(MerchantLabel(sellers[k], true));
|
|
|
|
|
|
for (int i = 0; i < names.Count; i++)
|
2026-11-03 19:35:48 +08:00
|
|
|
|
{
|
2026-11-04 11:06:38 +08:00
|
|
|
|
int idx = i - 1;
|
2026-11-04 11:18:45 +08:00
|
|
|
|
TableRow(b, SellerW, new[] { SellerW - 8f }, new[] { TextAnchor.MiddleLeft }, new[] { names[i] }, idx == _merchant, () => { _merchant = idx; _message = null; Open(); });
|
2026-11-03 20:06:32 +08:00
|
|
|
|
}
|
2026-11-03 22:04:31 +08:00
|
|
|
|
b.End();
|
2026-11-04 11:06:38 +08:00
|
|
|
|
b.BeginPanel(ShelfW, -1f, TextAnchor.UpperLeft, 0f, 4f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
2026-11-04 11:18:45 +08:00
|
|
|
|
ShopInfo(b, d, platform ? null : sellers[_merchant]);
|
|
|
|
|
|
if (platform) PlatformShelf(b, d);
|
2026-11-03 20:50:58 +08:00
|
|
|
|
else MerchantShelf(b, d);
|
2026-11-04 11:06:38 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
b.End();
|
2026-11-03 22:04:31 +08:00
|
|
|
|
b.AddSeparator();
|
|
|
|
|
|
b.AddHeader(L.T("black.forum.h"));
|
2026-11-04 10:10:34 +08:00
|
|
|
|
if (d.Black.Forum.Count == 0) b.AddLabel(L.T("black.forum.quiet"), "forum_quiet_dim");
|
2026-11-03 22:04:31 +08:00
|
|
|
|
foreach (var f in d.Black.Forum)
|
2026-11-04 10:10:34 +08:00
|
|
|
|
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), "forum_dim");
|
2026-11-03 22:04:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:34:38 +08:00
|
|
|
|
private static float SellerW => 176f * F;
|
|
|
|
|
|
private static float ShelfW => 560f + Ext * 0.78f;
|
2026-11-04 11:06:38 +08:00
|
|
|
|
|
2026-11-03 22:04:31 +08:00
|
|
|
|
private static void PlatformShelf(CustomUIBuilder b, SaveData d)
|
|
|
|
|
|
{
|
2026-11-04 10:10:34 +08:00
|
|
|
|
if (d.Black.Offers.Count == 0) { b.AddLabel(L.T("black.empty"), "platform_empty_dim"); return; }
|
2026-11-03 22:04:31 +08:00
|
|
|
|
Shelf(b, d.Black.Offers.Cast<BmGood>().ToList(), (c, g) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var offer = (BmOffer)g;
|
|
|
|
|
|
GoodHead(c, g, L.T("black.stock", offer.Qty));
|
2026-11-04 11:06:38 +08:00
|
|
|
|
c.AddLabel(L.T("black.price", offer.Price));
|
2026-11-03 22:04:31 +08:00
|
|
|
|
c.AddFlexibleSpace();
|
2026-11-04 11:18:45 +08:00
|
|
|
|
c.AddButton(L.T("black.buy"), () => AddToCart(-1, offer));
|
2026-11-04 15:34:38 +08:00
|
|
|
|
});
|
2026-11-04 11:06:38 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 20:23:21 +08:00
|
|
|
|
private static double _sellMult = 1.0;
|
|
|
|
|
|
|
2026-11-05 21:32:30 +08:00
|
|
|
|
private enum SellView { Showcase, Add, Fee }
|
|
|
|
|
|
private static SellView _sellView = SellView.Showcase;
|
|
|
|
|
|
private static readonly Dictionary<string, int> _stage = new Dictionary<string, int>();
|
|
|
|
|
|
private static readonly Dictionary<string, int> _delist = new Dictionary<string, int>();
|
|
|
|
|
|
private static bool _coinsOpen;
|
|
|
|
|
|
private static string _feeCoin;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>储藏区里同一种货(标识、型号、真伪、成色都相同)聚成一组。</summary>
|
|
|
|
|
|
private sealed class SellGroup
|
|
|
|
|
|
{
|
|
|
|
|
|
public string Key, Id, Model, FakeOf;
|
|
|
|
|
|
public double Health;
|
|
|
|
|
|
public long Ref;
|
|
|
|
|
|
public GoodKind Kind;
|
|
|
|
|
|
public readonly List<GameItem> Items = new List<GameItem>();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static List<SellGroup> SellGroups()
|
|
|
|
|
|
{
|
|
|
|
|
|
var map = new Dictionary<string, SellGroup>();
|
|
|
|
|
|
var list = new List<SellGroup>();
|
|
|
|
|
|
foreach (var it in BlackDelivery.Sellable())
|
|
|
|
|
|
{
|
|
|
|
|
|
string id = BlackDelivery.SellId(it);
|
|
|
|
|
|
if (id == null) continue;
|
|
|
|
|
|
var kind = BlackDelivery.KindOf(id);
|
|
|
|
|
|
var u = kind == GoodKind.Gpu ? Items.ToUnit(it) : null;
|
|
|
|
|
|
string key = BmSale.KeyOf(id, u?.Model, u?.FakeOf, u?.Health ?? 0);
|
|
|
|
|
|
if (!map.TryGetValue(key, out var g))
|
|
|
|
|
|
{
|
|
|
|
|
|
g = new SellGroup { Key = key, Id = id, Model = u?.Model, FakeOf = u?.FakeOf, Health = u?.Health ?? 0, Ref = BlackDelivery.RefValue(it), Kind = kind };
|
|
|
|
|
|
map[key] = g; list.Add(g);
|
|
|
|
|
|
}
|
|
|
|
|
|
g.Items.Add(it);
|
|
|
|
|
|
}
|
|
|
|
|
|
return list;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static double MultFor(string key) => Session.Data.Black.MultOf(key) ?? _sellMult;
|
|
|
|
|
|
private static int Staged(string key) => _stage.TryGetValue(key, out int n) ? n : 0;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>这一组预备数量再加一件会不会超限:返回提示文字,可以加返回 null。</summary>
|
|
|
|
|
|
private static string StageBlock(SellGroup g)
|
2026-11-03 20:23:21 +08:00
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-05 21:32:30 +08:00
|
|
|
|
int have = d.Black.ActiveOf(g.Key), staged = Staged(g.Key);
|
|
|
|
|
|
if (have + staged + 1 > Tuning.BmMaxStack) return L.T("black.sell.limit.stack", Tuning.BmMaxStack);
|
|
|
|
|
|
if (have == 0 && staged == 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
int kinds = d.Black.ActiveKinds + _stage.Count(kv => kv.Value > 0 && d.Black.ActiveOf(kv.Key) == 0);
|
|
|
|
|
|
if (kinds >= Tuning.BmMaxKinds) return L.T("black.sell.limit.kinds", Tuning.BmMaxKinds);
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static BmGood GoodOf(string id, GoodKind kind, string model, double health, int price) =>
|
|
|
|
|
|
new BmGood { Item = id, Kind = kind, Level = kind == GoodKind.Gpu ? 2 : Merchants.Info(id).Level, Model = model, Health = health, Price = price };
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>卖货:默认是已上架的橱窗;"上架更多"进入备货页,"货架"付平台费后上架。</summary>
|
|
|
|
|
|
private static void SellPage(CustomUIBuilder b, int day)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_sellView == SellView.Add) { SellAddPage(b); return; }
|
|
|
|
|
|
if (_sellView == SellView.Fee) { SellFeePage(b); return; }
|
|
|
|
|
|
ShowcasePage(b);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void ShowcasePage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
var listed = d.Black.Sales.Where(x => x.State == SaleState.Listed).GroupBy(x => x.Key).ToList();
|
|
|
|
|
|
b.AddLabel(L.T("black.sell.rating", d.Black.Rating) + " " + L.T("black.sell.kinds", d.Black.ActiveKinds, Tuning.BmMaxKinds), "sell_status_dim");
|
|
|
|
|
|
b.BeginRow(6f);
|
|
|
|
|
|
b.AddButton(L.T("black.sell.add"), () => { _sellView = SellView.Add; _delist.Clear(); _message = null; Open(); });
|
|
|
|
|
|
PledgerSkin.Pick(b, L.T("black.sell.coins"), _coinsOpen, () => { _coinsOpen = !_coinsOpen; Open(); }, _picked);
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
if (_coinsOpen)
|
|
|
|
|
|
{
|
|
|
|
|
|
b.BeginRow(4f);
|
|
|
|
|
|
foreach (var c in BlackMarket.SellCoins)
|
|
|
|
|
|
{
|
|
|
|
|
|
string coin = c;
|
|
|
|
|
|
PledgerSkin.Pick(b, coin, d.Black.AcceptedCoins.Contains(coin), () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!d.Black.ToggleCoin(coin)) _message = L.T("black.sell.nocoin");
|
|
|
|
|
|
else { _message = null; Session.Save(); }
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (listed.Count == 0) { b.AddLabel(L.T("black.sell.empty"), "sell_none_dim"); return; }
|
|
|
|
|
|
var cards = new Dictionary<BmGood, IGrouping<string, BmSale>>();
|
|
|
|
|
|
Shelf(b, listed.Select(g =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var s = g.First();
|
|
|
|
|
|
var kind = BlackDelivery.KindOf(s.Item);
|
|
|
|
|
|
var good = GoodOf(s.Item, kind, s.Model, s.Health, s.ListPrice);
|
|
|
|
|
|
cards[good] = g;
|
|
|
|
|
|
return good;
|
|
|
|
|
|
}).ToList(), (c, good) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var g = cards[good];
|
|
|
|
|
|
string key = g.Key; int count = g.Count();
|
|
|
|
|
|
GoodHead(c, good, L.T("black.sell.have", count));
|
|
|
|
|
|
c.AddLabel(L.T("black.price", good.Price));
|
|
|
|
|
|
c.AddFlexibleSpace();
|
|
|
|
|
|
int n = _delist.TryGetValue(key, out int cur) ? cur : 0;
|
|
|
|
|
|
c.AddButton(n > 0 ? L.T("black.sell.off.n", n) : L.T("black.sell.off"), () => { _delist[key] = (n + 1) % (count + 1); _message = null; Open(); });
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void ConfirmDelist()
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
int n = 0;
|
|
|
|
|
|
foreach (var kv in _delist.Where(kv => kv.Value > 0).ToList())
|
|
|
|
|
|
foreach (var s in d.Black.Sales.Where(x => x.State == SaleState.Listed && x.Key == kv.Key).Take(kv.Value).ToList())
|
|
|
|
|
|
{
|
|
|
|
|
|
d.Black.Delist(s);
|
|
|
|
|
|
Items.Give(BlackDelivery.Make(s.Item, s.Model, s.FakeOf, s.Health));
|
|
|
|
|
|
n++;
|
|
|
|
|
|
}
|
|
|
|
|
|
_delist.Clear();
|
|
|
|
|
|
_message = L.T("black.sell.delisted", n);
|
|
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void SellAddPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
var groups = SellGroups();
|
|
|
|
|
|
_stage.Keys.Where(k => groups.All(g => g.Key != k)).ToList().ForEach(k => _stage.Remove(k));
|
|
|
|
|
|
foreach (var g in groups) if (Staged(g.Key) > g.Items.Count) _stage[g.Key] = g.Items.Count;
|
2026-11-04 13:37:54 +08:00
|
|
|
|
b.BeginRow(8f, TextAnchor.UpperLeft);
|
|
|
|
|
|
b.BeginPanel(SellerW + 8f, -1f, TextAnchor.UpperLeft, 0f, 2f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
|
|
|
|
|
b.AddLabel(L.T("black.sell.mult"), "sell_mult_dim");
|
|
|
|
|
|
foreach (var m in Tuning.BmSellMults)
|
2026-11-03 20:23:21 +08:00
|
|
|
|
{
|
|
|
|
|
|
double mult = m;
|
2026-11-05 21:32:30 +08:00
|
|
|
|
TableRow(b, SellerW, new[] { SellerW - 8f }, new[] { TextAnchor.MiddleLeft },
|
|
|
|
|
|
new[] { "×" + m.ToString("0.0", CultureInfo.InvariantCulture) },
|
2026-11-04 13:37:54 +08:00
|
|
|
|
Math.Abs(mult - _sellMult) < 1e-9, () => { _sellMult = mult; Open(); });
|
2026-11-03 20:23:21 +08:00
|
|
|
|
}
|
2026-11-04 13:37:54 +08:00
|
|
|
|
b.AddSpace(4f);
|
|
|
|
|
|
b.AddLabel(L.T("black.sell.rating", d.Black.Rating), "sell_status_dim");
|
2026-11-05 21:32:30 +08:00
|
|
|
|
b.AddLabel(L.T("black.sell.kinds", d.Black.ActiveKinds, Tuning.BmMaxKinds), "sell_status_dim");
|
2026-11-04 13:37:54 +08:00
|
|
|
|
b.AddSpace(4f);
|
2026-11-05 21:32:30 +08:00
|
|
|
|
b.AddButton(L.T("black.sell.back"), () => { _sellView = SellView.Showcase; _stage.Clear(); _message = null; Open(); });
|
2026-11-04 13:37:54 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
b.BeginPanel(ShelfW, -1f, TextAnchor.UpperLeft, 0f, 4f);
|
|
|
|
|
|
_clear.Add(b.Last.image);
|
2026-11-05 21:32:30 +08:00
|
|
|
|
if (groups.Count == 0) b.AddLabel(L.T("black.sell.none"), "sell_none_dim");
|
|
|
|
|
|
else
|
2026-11-03 20:23:21 +08:00
|
|
|
|
{
|
2026-11-05 21:32:30 +08:00
|
|
|
|
var cards = new Dictionary<BmGood, SellGroup>();
|
|
|
|
|
|
Shelf(b, groups.Select(g =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var good = GoodOf(g.Id, g.Kind, g.Model, g.Health, BlackMarket.ListPriceOf(g.Ref, MultFor(g.Key)));
|
|
|
|
|
|
cards[good] = g;
|
|
|
|
|
|
return good;
|
|
|
|
|
|
}).ToList(), (c, good) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var g = cards[good];
|
|
|
|
|
|
GoodHead(c, good, "");
|
|
|
|
|
|
c.AddLabel(L.T("black.price", good.Price));
|
|
|
|
|
|
c.AddLabel(L.T("black.stock", g.Items.Count), "good_line2_dim");
|
|
|
|
|
|
c.AddFlexibleSpace();
|
|
|
|
|
|
c.BeginRow(4f, TextAnchor.MiddleCenter);
|
|
|
|
|
|
RowKey(c, "-", () => { if (Staged(g.Key) > 0) _stage[g.Key] = Staged(g.Key) - 1; _message = null; Open(); });
|
|
|
|
|
|
Cell(c, 34f, Staged(g.Key).ToString(), false, TextAnchor.MiddleCenter);
|
|
|
|
|
|
RowKey(c, "+", () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
string block = Staged(g.Key) >= g.Items.Count ? null : StageBlock(g);
|
|
|
|
|
|
if (block != null) { _message = block; Open(); return; }
|
|
|
|
|
|
if (Staged(g.Key) < g.Items.Count) _stage[g.Key] = Staged(g.Key) + 1;
|
|
|
|
|
|
_message = null; Open();
|
|
|
|
|
|
});
|
|
|
|
|
|
c.End();
|
|
|
|
|
|
});
|
2026-11-03 20:50:58 +08:00
|
|
|
|
}
|
2026-11-05 21:32:30 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
b.End();
|
2026-11-03 20:50:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 21:32:30 +08:00
|
|
|
|
private static double StageFee(List<SellGroup> groups) =>
|
|
|
|
|
|
groups.Sum(g => Staged(g.Key) * (double)BlackMarket.ListFee(BlackMarket.ListPriceOf(g.Ref, MultFor(g.Key))));
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>货架:左栏是准备上架的清单,右栏选收款币付平台费,付完才上架。</summary>
|
|
|
|
|
|
private static void SellFeePage(CustomUIBuilder b)
|
2026-11-03 20:23:21 +08:00
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-05 21:32:30 +08:00
|
|
|
|
var w = d.Hot;
|
|
|
|
|
|
var groups = SellGroups().Where(g => Staged(g.Key) > 0).ToList();
|
|
|
|
|
|
if (groups.Count == 0) { _sellView = SellView.Add; Open(); return; }
|
|
|
|
|
|
double fee = StageFee(groups);
|
|
|
|
|
|
var acc = Merchants.PlatformAccepts;
|
|
|
|
|
|
if (_feeCoin == null || !acc.Contains(_feeCoin)) _feeCoin = acc[0];
|
|
|
|
|
|
Columns(b, l =>
|
|
|
|
|
|
{
|
|
|
|
|
|
l.AddHeader(L.T("black.fee.h"));
|
|
|
|
|
|
foreach (var g in groups)
|
|
|
|
|
|
l.AddLabel(L.T("black.fee.line", ItemName(g.Id), Staged(g.Key), BlackMarket.ListPriceOf(g.Ref, MultFor(g.Key)), "×" + MultFor(g.Key).ToString("0.0", CultureInfo.InvariantCulture)), "cart_shop");
|
|
|
|
|
|
l.AddSeparator();
|
|
|
|
|
|
l.AddLabel(L.T("black.fee.total", Fmt.Credits(fee)), "cart_total");
|
|
|
|
|
|
}, r =>
|
|
|
|
|
|
{
|
2026-11-05 21:57:15 +08:00
|
|
|
|
r.AddHeader(L.T("black.fee.pay"));
|
2026-11-05 21:32:30 +08:00
|
|
|
|
r.BeginRow(4f);
|
|
|
|
|
|
foreach (var a in acc) { string asset = a; PledgerSkin.Pick(r, asset, asset == _feeCoin, () => { _feeCoin = asset; Open(); }, _picked); }
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
r.AddLabel(L.T("bpay.need", WalletWindow.Amt(WalletWindow.RoundUp(fee / Session.Price(_feeCoin))), _feeCoin, Fmt.Num(w.Avail(_feeCoin))), "bpay_need_dim");
|
|
|
|
|
|
r.AddButton(L.T("bpay.invoice"), () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
string coin = _feeCoin;
|
|
|
|
|
|
string summary = L.T("black.fee.sum", groups.Sum(g => Staged(g.Key)));
|
|
|
|
|
|
BeginPay(d.Address("platform"), summary, coin, fee, pw => ListStaged(coin, pw));
|
|
|
|
|
|
});
|
|
|
|
|
|
r.AddSpace(4f);
|
|
|
|
|
|
r.AddButton(L.T("black.sell.back"), () => { _sellView = SellView.Add; _message = null; Open(); });
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>付完平台费后:按预备数量逐件上架,货从储藏区拿走。</summary>
|
|
|
|
|
|
private static void ListStaged(string asset, Wallet pw)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
int day = StoreStation.GetDayCounter(), ok = 0, fail = 0;
|
|
|
|
|
|
foreach (var g in SellGroups())
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = Staged(g.Key);
|
|
|
|
|
|
if (n <= 0) continue;
|
|
|
|
|
|
double mult = MultFor(g.Key);
|
|
|
|
|
|
foreach (var it in g.Items.Take(n))
|
|
|
|
|
|
{
|
2026-11-07 22:24:36 +08:00
|
|
|
|
int heat = it.IsTag("STOLEN_VALUE_INT") ? it.GetTagReadonly("STOLEN_VALUE_INT").GetInt() : 0;
|
|
|
|
|
|
var s = d.Black.List(g.Id, g.Model, g.FakeOf, g.Health, g.Ref, mult, pw, asset, day, Session.Price, heat);
|
2026-11-05 21:32:30 +08:00
|
|
|
|
if (s == null) { fail++; continue; }
|
|
|
|
|
|
it.Destroy();
|
|
|
|
|
|
ok++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
_stage.Clear();
|
|
|
|
|
|
_sellView = SellView.Showcase;
|
|
|
|
|
|
_message = fail == 0 ? L.T("black.sell.listedall", ok) : L.T("black.sell.partial", ok, fail);
|
2026-11-03 20:23:21 +08:00
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 11:18:45 +08:00
|
|
|
|
// ——— 购物车 ———
|
|
|
|
|
|
|
|
|
|
|
|
private sealed class CartLine { public int Seller; public BmGood Good; public int Qty; }
|
|
|
|
|
|
private static readonly List<CartLine> _cart = new List<CartLine>();
|
2026-11-04 11:25:06 +08:00
|
|
|
|
private static bool _checkout;
|
|
|
|
|
|
private static readonly Dictionary<int, string> _ckSwap = new Dictionary<int, string>();
|
2026-11-04 11:18:45 +08:00
|
|
|
|
private static readonly Dictionary<int, PayMode> _ckMode = new Dictionary<int, PayMode>();
|
|
|
|
|
|
private static readonly Dictionary<int, string> _ckGroupCoin = new Dictionary<int, string>();
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>读档或换局时清空购物车(货架对象已经不是同一批了)。</summary>
|
2026-11-05 21:32:30 +08:00
|
|
|
|
public static void ClearCart() { _stage.Clear(); _delist.Clear(); _sellView = SellView.Showcase; _cart.Clear(); _checkout = false; _ckMode.Clear(); _ckGroupCoin.Clear(); _ckSwap.Clear(); }
|
2026-11-04 11:18:45 +08:00
|
|
|
|
|
|
|
|
|
|
private static int StockOf(BmGood g) => g is BmOffer o ? o.Qty : ((BmListing)g).Qty;
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>丢掉已经下架的货,数量压到库存以内。</summary>
|
|
|
|
|
|
private static void CartValidate(SaveData d)
|
|
|
|
|
|
{
|
|
|
|
|
|
_cart.RemoveAll(l => l.Good is BmOffer o ? !d.Black.Offers.Contains(o) : !d.Black.Catalog.Contains((BmListing)l.Good) || l.Seller >= d.Black.Sellers.Count);
|
|
|
|
|
|
foreach (var l in _cart) l.Qty = Math.Max(1, Math.Min(l.Qty, StockOf(l.Good)));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static int CartCount() => _cart.Sum(l => l.Qty);
|
|
|
|
|
|
|
|
|
|
|
|
private static void AddToCart(int seller, BmGood g)
|
|
|
|
|
|
{
|
|
|
|
|
|
var line = _cart.Find(l => l.Good == g);
|
|
|
|
|
|
if (line == null) _cart.Add(new CartLine { Seller = seller, Good = g, Qty = 1 });
|
|
|
|
|
|
else line.Qty = Math.Min(StockOf(g), line.Qty + 1);
|
|
|
|
|
|
_message = L.T("cart.added", ItemName(g.Item));
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string[] AcceptsOf(SaveData d, int seller) => seller < 0 ? Merchants.PlatformAccepts : Merchants.Accepts(d.Black.Sellers[seller].Type);
|
|
|
|
|
|
private static string AddressOf(SaveData d, int seller) => seller < 0 ? d.Address("platform") : d.Black.Sellers[seller].Name;
|
|
|
|
|
|
private static string ShopName(SaveData d, int seller) => seller < 0 ? L.T("merchant.Platform") : MerchantLabel(d.Black.Sellers[seller], true);
|
|
|
|
|
|
private static PayMode ModeOf(SaveData d, int seller) => seller >= 0 && d.Black.Sellers[seller].Advance && _ckMode.TryGetValue(seller, out var m) ? m : PayMode.Escrow;
|
|
|
|
|
|
|
|
|
|
|
|
private static double LineCredits(SaveData d, CartLine l) =>
|
|
|
|
|
|
l.Good is BmOffer o ? o.Price * l.Qty : BlackMarket.Pay(BlackMarket.OrderTotal(((BmListing)l.Good).Price, l.Qty), ModeOf(d, l.Seller));
|
|
|
|
|
|
|
|
|
|
|
|
private static double GroupCredits(SaveData d, int seller) => _cart.Where(l => l.Seller == seller).Sum(l => LineCredits(d, l));
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>悬浮购物车:商店页右下角一个常驻的小按钮,显示件数,点开进结算页。</summary>
|
2026-11-05 21:32:30 +08:00
|
|
|
|
private sealed class Fab { public string Text; public bool On; public Action Click; }
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>右下角悬浮按钮:买货页是购物车,卖货的备货页是货架,橱窗里有预备下架时是确定下架;其余页面没有。</summary>
|
|
|
|
|
|
private static Fab FabNow()
|
|
|
|
|
|
{
|
|
|
|
|
|
if (_page != Page.Gray || _grayTab != GrayTab.Black || _checkout || WalletWindow.Pending != null || !Unlock.BlackUnlocked) return null;
|
|
|
|
|
|
if (_blackTab == BlackTab.Buy) return new Fab { Text = L.T("cart.fab", CartCount()), On = CartCount() > 0, Click = () => { _checkout = true; _message = null; Open(); } };
|
|
|
|
|
|
if (_blackTab != BlackTab.Sell) return null;
|
|
|
|
|
|
if (_sellView == SellView.Add)
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = _stage.Values.Sum();
|
|
|
|
|
|
return new Fab { Text = L.T("shelf.fab", n), On = n > 0, Click = () => { if (n > 0) { _sellView = SellView.Fee; _message = null; Open(); } } };
|
|
|
|
|
|
}
|
|
|
|
|
|
if (_sellView == SellView.Showcase)
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = _delist.Values.Sum();
|
|
|
|
|
|
return n > 0 ? new Fab { Text = L.T("delist.fab", n), On = true, Click = ConfirmDelist } : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 11:18:45 +08:00
|
|
|
|
private static void CartFab(CustomUIWindow win)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (win?.root == null) return;
|
|
|
|
|
|
var old = win.root.transform.Find("pd_cart_fab");
|
2026-11-05 21:32:30 +08:00
|
|
|
|
var fab = FabNow();
|
|
|
|
|
|
if (fab == null) { if (old != null) UnityEngine.Object.Destroy(old.gameObject); return; }
|
2026-11-04 11:18:45 +08:00
|
|
|
|
if (old != null) UnityEngine.Object.DestroyImmediate(old.gameObject);
|
|
|
|
|
|
var go = new GameObject("pd_cart_fab", typeof(RectTransform), typeof(CanvasRenderer), typeof(Image), typeof(Button), typeof(LayoutElement));
|
|
|
|
|
|
go.transform.SetParent(win.root.transform, false);
|
|
|
|
|
|
go.GetComponent<LayoutElement>().ignoreLayout = true;
|
|
|
|
|
|
var rt = (RectTransform)go.transform;
|
|
|
|
|
|
rt.anchorMin = rt.anchorMax = new Vector2(1f, 0f);
|
|
|
|
|
|
rt.pivot = new Vector2(1f, 0f);
|
2026-11-05 21:32:30 +08:00
|
|
|
|
rt.sizeDelta = new Vector2(Mathf.Max(132f, fab.Text.Length * 9f + 40f), 36f);
|
2026-11-04 11:18:45 +08:00
|
|
|
|
rt.anchoredPosition = new Vector2(-34f, 34f);
|
|
|
|
|
|
var img = go.GetComponent<Image>();
|
2026-11-05 21:32:30 +08:00
|
|
|
|
PledgerSkin.KeyShape(img, fab.On ? PledgerSkin.Ink : PledgerSkin.Key);
|
2026-11-04 11:18:45 +08:00
|
|
|
|
var btn = go.GetComponent<Button>();
|
|
|
|
|
|
btn.transition = Selectable.Transition.None;
|
2026-11-05 21:32:30 +08:00
|
|
|
|
btn.onClick.AddListener(() => fab.Click());
|
2026-11-04 11:18:45 +08:00
|
|
|
|
var tgo = new GameObject("label", typeof(RectTransform), typeof(CanvasRenderer), typeof(TextMeshProUGUI));
|
|
|
|
|
|
tgo.transform.SetParent(go.transform, false);
|
|
|
|
|
|
var trt = (RectTransform)tgo.transform;
|
|
|
|
|
|
trt.anchorMin = Vector2.zero; trt.anchorMax = Vector2.one; trt.offsetMin = trt.offsetMax = Vector2.zero;
|
|
|
|
|
|
var tmp = tgo.GetComponent<TextMeshProUGUI>();
|
|
|
|
|
|
if (win.frame?.titleText != null) { tmp.font = win.frame.titleText.font; tmp.fontSharedMaterial = win.frame.titleText.fontSharedMaterial; }
|
2026-11-05 21:32:30 +08:00
|
|
|
|
tmp.text = fab.Text;
|
2026-11-04 11:18:45 +08:00
|
|
|
|
tmp.fontSize = 15f;
|
|
|
|
|
|
tmp.fontStyle = FontStyles.Bold;
|
|
|
|
|
|
tmp.alignment = TextAlignmentOptions.Center;
|
|
|
|
|
|
tmp.color = PledgerSkin.KeyText;
|
|
|
|
|
|
tmp.raycastTarget = false;
|
|
|
|
|
|
go.transform.SetAsLastSibling();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>右栏最前面:商家信息(成交、好评、到货天数、收的币、是否接受提前放款)。</summary>
|
|
|
|
|
|
private static void ShopInfo(CustomUIBuilder b, SaveData d, BmSeller seller)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (seller == null)
|
|
|
|
|
|
{
|
|
|
|
|
|
b.AddHeader(L.T("merchant.Platform"));
|
|
|
|
|
|
b.AddLabel(L.T("black.platform.line"), "shop_info_dim");
|
|
|
|
|
|
b.AddLabel(L.T("black.info.accepts", string.Join(" / ", Merchants.PlatformAccepts)), "shop_accepts_dim");
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
b.AddHeader(MerchantLabel(seller));
|
|
|
|
|
|
b.AddLabel(L.T("black.seller", "", seller.Trades, seller.Rating) + " " + L.T("black.days", seller.Days), "shop_info_dim");
|
|
|
|
|
|
b.AddLabel(L.T("black.info.accepts", string.Join(" / ", Merchants.Accepts(seller.Type))) + " " + L.T(seller.Advance ? "black.info.advance.yes" : "black.info.advance.no"), "shop_accepts_dim");
|
|
|
|
|
|
}
|
|
|
|
|
|
b.AddSeparator();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 11:25:06 +08:00
|
|
|
|
/// <summary>结算页:左栏是购物车详情(按商家分组,可增减数量、删除),右栏是付款:每家商家收款地址不同,所以逐家付款。</summary>
|
2026-11-04 11:18:45 +08:00
|
|
|
|
private static void CheckoutPage(CustomUIBuilder b, SaveData d)
|
|
|
|
|
|
{
|
|
|
|
|
|
CartValidate(d);
|
|
|
|
|
|
var w = d.Hot;
|
|
|
|
|
|
var groups = _cart.GroupBy(l => l.Seller).OrderBy(g => g.Key).ToList();
|
|
|
|
|
|
Columns(b, l =>
|
|
|
|
|
|
{
|
|
|
|
|
|
l.AddHeader(L.T("cart.h"));
|
|
|
|
|
|
if (_cart.Count == 0) { l.AddLabel(L.T("cart.empty"), "cart_empty_dim"); return; }
|
|
|
|
|
|
foreach (var g in groups)
|
|
|
|
|
|
{
|
|
|
|
|
|
int seller = g.Key;
|
|
|
|
|
|
l.AddLabel("<b>" + ShopName(d, seller) + "</b>", "cart_shop");
|
|
|
|
|
|
if (seller >= 0 && d.Black.Sellers[seller].Advance)
|
|
|
|
|
|
{
|
|
|
|
|
|
l.BeginRow(4f);
|
|
|
|
|
|
foreach (PayMode m in Enum.GetValues(typeof(PayMode)))
|
|
|
|
|
|
{
|
|
|
|
|
|
var mode = m;
|
|
|
|
|
|
PledgerSkin.Pick(l, L.T("black.mode." + m), ModeOf(d, seller) == mode, () => { _ckMode[seller] = mode; Open(); }, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
l.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
foreach (var line in g.ToList())
|
|
|
|
|
|
{
|
|
|
|
|
|
var ln = line;
|
|
|
|
|
|
l.BeginRow(4f, TextAnchor.MiddleLeft);
|
|
|
|
|
|
Cell(l, 150f, ItemName(ln.Good.Item), false, TextAnchor.MiddleLeft);
|
|
|
|
|
|
RowKey(l, "-", () => { ln.Qty--; if (ln.Qty < 1) _cart.Remove(ln); Open(); });
|
|
|
|
|
|
Cell(l, 26f, ln.Qty.ToString(), false, TextAnchor.MiddleCenter);
|
|
|
|
|
|
RowKey(l, "+", () => { ln.Qty = Math.Min(StockOf(ln.Good), ln.Qty + 1); Open(); });
|
|
|
|
|
|
Cell(l, 58f, Fmt.Credits(LineCredits(d, ln)), false, TextAnchor.MiddleRight);
|
|
|
|
|
|
RowKey(l, "×", () => { _cart.Remove(ln); Open(); });
|
|
|
|
|
|
l.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
l.AddSeparator();
|
|
|
|
|
|
l.AddLabel(L.T("bpay.total", Fmt.Credits(_cart.Sum(x => LineCredits(d, x)))), "cart_total");
|
|
|
|
|
|
}, r =>
|
|
|
|
|
|
{
|
|
|
|
|
|
r.AddHeader(L.T("bpay.h"));
|
|
|
|
|
|
if (_cart.Count == 0) { r.AddButton(L.T("cart.back"), () => { _checkout = false; Open(); }); return; }
|
2026-11-04 11:25:06 +08:00
|
|
|
|
foreach (var g in groups) GroupPay(r, d, w, g.Key);
|
2026-11-04 11:18:45 +08:00
|
|
|
|
r.AddSpace(4f);
|
|
|
|
|
|
r.AddButton(L.T("cart.back"), () => { _checkout = false; _message = null; Open(); });
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 11:25:06 +08:00
|
|
|
|
/// <summary>逐家付款:这家商家收的币里选一种,生成账单在钱包里转账;也可以用钱包里别的币自动换币(多付滑点)一次签名付清。</summary>
|
2026-11-04 11:18:45 +08:00
|
|
|
|
private static void GroupPay(CustomUIBuilder r, SaveData d, Wallet w, int seller)
|
|
|
|
|
|
{
|
|
|
|
|
|
var acc = AcceptsOf(d, seller);
|
|
|
|
|
|
if (!_ckGroupCoin.TryGetValue(seller, out var coin) || !acc.Contains(coin)) coin = _ckGroupCoin[seller] = acc[0];
|
|
|
|
|
|
double credits = GroupCredits(d, seller);
|
|
|
|
|
|
r.AddSeparator();
|
|
|
|
|
|
r.AddLabel(ShopName(d, seller) + " " + Fmt.Credits(credits), "cart_shop");
|
|
|
|
|
|
r.BeginRow(4f);
|
|
|
|
|
|
foreach (var a in acc) { string asset = a; PledgerSkin.Pick(r, asset, asset == coin, () => { _ckGroupCoin[seller] = asset; Open(); }, _picked); }
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
r.AddLabel(L.T("bpay.need", WalletWindow.Amt(WalletWindow.RoundUp(credits / Session.Price(coin))), coin, Fmt.Num(w.Avail(coin))), "bpay_need_dim");
|
|
|
|
|
|
r.AddButton(L.T("bpay.invoice"), () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var lines = _cart.Where(l => l.Seller == seller).ToList();
|
|
|
|
|
|
var mode = ModeOf(d, seller);
|
|
|
|
|
|
string summary = ShopName(d, seller) + " ×" + lines.Sum(l => l.Qty);
|
|
|
|
|
|
_checkout = false;
|
|
|
|
|
|
BeginPay(AddressOf(d, seller), summary, coin, credits, pw => { var r2 = ExecGroup(seller, lines, mode, coin, pw); Finish(CartMessage(r2.items, r2.failed)); });
|
|
|
|
|
|
});
|
2026-11-04 11:25:06 +08:00
|
|
|
|
double slipped = credits * (1 + Tuning.BmAutoSwapSlip);
|
|
|
|
|
|
var sources = w.Balances.Keys.Where(a => !acc.Contains(a) && a != Assets.Ponero && Session.Price(a) > 0 && w.Avail(a) * Session.Price(a) >= slipped).ToList();
|
|
|
|
|
|
if (sources.Count == 0) return;
|
|
|
|
|
|
if (!_ckSwap.TryGetValue(seller, out var src) || !sources.Contains(src)) src = _ckSwap[seller] = sources[0];
|
|
|
|
|
|
r.AddLabel(L.T("bpay.swap.h", coin), "bpay_swap_dim");
|
|
|
|
|
|
r.BeginRow(4f);
|
|
|
|
|
|
foreach (var s in sources) { string sc = s; PledgerSkin.Pick(r, sc, sc == src, () => { _ckSwap[seller] = sc; Open(); }, _picked); }
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
double needSrc = WalletWindow.RoundUp(slipped / Session.Price(src));
|
|
|
|
|
|
r.AddLabel(L.T("bpay.swap.need", WalletWindow.Amt(needSrc), src, (Tuning.BmAutoSwapSlip * 100).ToString("0", CultureInfo.InvariantCulture)), "bpay_swap_need_dim");
|
|
|
|
|
|
r.AddButton(L.T("bpay.swap.btn"), () => SwapGroup(seller, coin, src, credits, needSrc));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>自动换币并结算一家商家:一次合约调用,先把别的币按滑点换成这家收的币,再下单。</summary>
|
|
|
|
|
|
private static void SwapGroup(int seller, string coin, string src, double credits, double needSrc)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
var lines = _cart.Where(l => l.Seller == seller).ToList();
|
|
|
|
|
|
var mode = ModeOf(d, seller);
|
|
|
|
|
|
string summary = ShopName(d, seller) + " ×" + lines.Sum(l => l.Qty);
|
|
|
|
|
|
_checkout = false;
|
|
|
|
|
|
_message = L.T("hw.wait");
|
|
|
|
|
|
Open();
|
|
|
|
|
|
WalletWindow.OpenForBill(new WalletWindow.Bill
|
|
|
|
|
|
{
|
|
|
|
|
|
Contract = true, Asset = src, Amount = needSrc, Spend = needSrc,
|
|
|
|
|
|
Summary = L.T("bpay.swap.sum", summary, src, WalletWindow.Amt(needSrc), coin),
|
|
|
|
|
|
Execute = pw =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (pw.Avail(src) + 1e-9 < needSrc) { Finish(L.T("msg.balance")); return; }
|
|
|
|
|
|
double got = credits / Session.Price(coin) * 1.002;
|
|
|
|
|
|
pw.Add(src, -needSrc); pw.Add(coin, got); pw.RecordBuy(coin, got, credits);
|
|
|
|
|
|
CandleChart.Trade(src, credits); CandleChart.Trade(coin, credits);
|
|
|
|
|
|
Log.Info($"自动换币:{needSrc:0.####} {src} → {got:0.####} {coin}({credits:0} 信用点等值)");
|
2026-11-07 22:39:18 +08:00
|
|
|
|
// 一次签名两步:经 DEX 换币,再付给这家商家;分别记账
|
|
|
|
|
|
d.Chain(pw.Address, d.Address("dex"), src, needSrc);
|
|
|
|
|
|
d.Chain(d.Address("dex"), pw.Address, coin, got);
|
|
|
|
|
|
var paying = SaveData.Snapshot(pw);
|
2026-11-04 11:25:06 +08:00
|
|
|
|
var r2 = ExecGroup(seller, lines, mode, coin, pw);
|
2026-11-07 22:39:18 +08:00
|
|
|
|
d.ChainDiff(pw, paying, AddressOf(d, seller));
|
2026-11-04 11:25:06 +08:00
|
|
|
|
Finish(CartMessage(r2.items, r2.failed));
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
2026-11-04 11:18:45 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static void Finish(string message)
|
|
|
|
|
|
{
|
|
|
|
|
|
_message = message;
|
|
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>结算一家商家的购物车行:平台现货直接买、商家下订单,成功的行从购物车里拿掉。返回结果消息。</summary>
|
|
|
|
|
|
private static (int items, int failed) ExecGroup(int seller, List<CartLine> lines, PayMode mode, string asset, Wallet pw)
|
|
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
int items = 0, failed = 0;
|
|
|
|
|
|
foreach (var line in lines)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (line.Good is BmOffer o)
|
|
|
|
|
|
{
|
|
|
|
|
|
int n = BuyOfferCore(o, line.Qty, asset, pw);
|
|
|
|
|
|
items += n;
|
|
|
|
|
|
if (n < line.Qty) failed += line.Qty - n;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
var ord = d.Black.Place((BmListing)line.Good, line.Qty, mode, pw, asset, StoreStation.GetDayCounter(), Session.Price);
|
|
|
|
|
|
if (ord == null) failed += line.Qty; else items += ord.Count;
|
|
|
|
|
|
}
|
|
|
|
|
|
_cart.Remove(line);
|
|
|
|
|
|
}
|
|
|
|
|
|
return (items, failed);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static string CartMessage(int items, int failed) => failed == 0 ? L.T("cart.done", items) : L.T("cart.partial", items, failed);
|
|
|
|
|
|
|
2026-11-04 11:50:23 +08:00
|
|
|
|
private static string MerchantLabel(BmSeller s, bool shortName = false) => L.T("merchant." + s.Type) + " " + (shortName ? "·" + s.Name.Substring(Math.Max(0, s.Name.Length - 4)) : s.Name);
|
2026-11-03 21:25:48 +08:00
|
|
|
|
|
2026-11-03 22:04:31 +08:00
|
|
|
|
private static int _merchant = -1;
|
2026-11-03 20:50:58 +08:00
|
|
|
|
private static readonly int[] Qtys = { 1, 5, 10 };
|
|
|
|
|
|
|
2026-11-04 11:06:38 +08:00
|
|
|
|
/// <summary>商家货架:这家的成交数、好评和到货天数,下面是这周的货;点"购买"进付款面板。</summary>
|
2026-11-03 20:50:58 +08:00
|
|
|
|
private static void MerchantShelf(CustomUIBuilder b, SaveData d)
|
|
|
|
|
|
{
|
|
|
|
|
|
var sellers = d.Black.Sellers;
|
|
|
|
|
|
var seller = sellers[_merchant];
|
2026-11-04 11:06:38 +08:00
|
|
|
|
int merchant = _merchant;
|
|
|
|
|
|
var goods = d.Black.Catalog.Where(l => l.Seller == merchant).Cast<BmGood>().ToList();
|
2026-11-04 10:10:34 +08:00
|
|
|
|
if (goods.Count == 0) { b.AddLabel(L.T("black.empty"), "shelf_empty_dim"); return; }
|
2026-11-03 20:50:58 +08:00
|
|
|
|
Shelf(b, goods, (c, g) =>
|
|
|
|
|
|
{
|
|
|
|
|
|
var listing = (BmListing)g;
|
|
|
|
|
|
GoodHead(c, g, L.T("black.stock", listing.Qty));
|
2026-11-04 11:06:38 +08:00
|
|
|
|
c.AddLabel(L.T("black.price", BlackMarket.Pay(BlackMarket.OrderTotal(listing.Price, 1), PayMode.Escrow)));
|
2026-11-03 20:50:58 +08:00
|
|
|
|
c.AddFlexibleSpace();
|
2026-11-04 11:18:45 +08:00
|
|
|
|
c.AddButton(L.T("black.buy"), () => AddToCart(merchant, listing));
|
2026-11-04 11:06:38 +08:00
|
|
|
|
}, 3);
|
2026-11-03 20:50:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:34:38 +08:00
|
|
|
|
private static int ShelfCols => Ext < 100f ? 3 : Math.Max(3, (int)Math.Round(ShelfW / (172f * F)));
|
|
|
|
|
|
private static float CardW => Ext < 100f ? 172f : (ShelfW - 6f * (ShelfCols - 1)) / ShelfCols;
|
|
|
|
|
|
private static float CardH => 268f * F;
|
2026-11-03 20:06:32 +08:00
|
|
|
|
|
|
|
|
|
|
/// <summary>货架:一行 4 张商品卡。</summary>
|
2026-11-04 15:34:38 +08:00
|
|
|
|
private static void Shelf(CustomUIBuilder b, System.Collections.Generic.List<BmGood> goods, Action<CustomUIBuilder, BmGood> card, int cols = -1)
|
2026-11-03 20:06:32 +08:00
|
|
|
|
{
|
2026-11-04 15:34:38 +08:00
|
|
|
|
if (cols <= 0) cols = ShelfCols;
|
2026-11-04 11:06:38 +08:00
|
|
|
|
for (int i = 0; i < goods.Count; i += cols)
|
2026-11-03 20:06:32 +08:00
|
|
|
|
{
|
|
|
|
|
|
b.BeginRow(6f);
|
2026-11-04 11:06:38 +08:00
|
|
|
|
for (int k = i; k < Math.Min(goods.Count, i + cols); k++)
|
2026-11-03 20:06:32 +08:00
|
|
|
|
{
|
|
|
|
|
|
var g = goods[k];
|
|
|
|
|
|
b.BeginPanel(CardW, CardH, TextAnchor.UpperCenter, 6f, 3f);
|
2026-11-04 10:10:34 +08:00
|
|
|
|
if (b.Last != null && b.Last.image != null)
|
|
|
|
|
|
{
|
|
|
|
|
|
b.Last.image.sprite = MachineryStyle.White;
|
|
|
|
|
|
b.Last.image.color = PledgerSkin.RowNormal;
|
|
|
|
|
|
}
|
2026-11-03 20:06:32 +08:00
|
|
|
|
card(b, g);
|
|
|
|
|
|
b.End();
|
2026-11-03 19:35:48 +08:00
|
|
|
|
}
|
2026-11-03 20:06:32 +08:00
|
|
|
|
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>");
|
2026-11-04 15:34:38 +08:00
|
|
|
|
PledgerSkin.FitBox(c, 52f * F);
|
2026-11-04 00:58:14 +08:00
|
|
|
|
bool asic = g.Kind == GoodKind.Gpu && g.Model != null && Hardware.Specs.TryGetValue(g.Model, out var gs) && gs.Asic;
|
2026-11-04 10:10:34 +08:00
|
|
|
|
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 : "")), "good_kind_dim");
|
|
|
|
|
|
if (!string.IsNullOrEmpty(line2)) c.AddLabel(line2, "good_line2_dim");
|
2026-11-03 19:35:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 19:48:26 +08:00
|
|
|
|
private static string ItemName(string id) => BlackDelivery.ItemName(id);
|
2026-11-03 19:35:48 +08:00
|
|
|
|
|
2026-11-03 22:29:42 +08:00
|
|
|
|
/// <summary>开账单:收款地址、币种、金额(按信用点等值折成币),付款在硬件钱包里做。</summary>
|
2026-11-03 22:58:24 +08:00
|
|
|
|
private static void BeginPay(string address, string summary, string asset, double credits, Action<Wallet> execute) =>
|
2026-11-03 22:29:42 +08:00
|
|
|
|
BillFor(address, summary, asset, credits / Session.Price(asset), execute);
|
|
|
|
|
|
|
2026-11-03 22:58:24 +08:00
|
|
|
|
private static void BillFor(string address, string summary, string asset, double amount, Action<Wallet> execute)
|
2026-11-03 21:31:50 +08:00
|
|
|
|
{
|
2026-11-03 22:29:42 +08:00
|
|
|
|
WalletWindow.Pending = new WalletWindow.Bill { Address = address, Summary = summary, Asset = asset, Amount = WalletWindow.RoundUp(amount), Execute = execute };
|
2026-11-03 21:31:50 +08:00
|
|
|
|
_message = null;
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 22:29:42 +08:00
|
|
|
|
/// <summary>待付账单:收款地址和金额,去钱包里转账;也可以作废。</summary>
|
2026-11-03 21:31:50 +08:00
|
|
|
|
private static void PayPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
2026-11-03 22:29:42 +08:00
|
|
|
|
var p = WalletWindow.Pending;
|
2026-11-03 21:31:50 +08:00
|
|
|
|
b.AddHeader(L.T("black.pay.confirm"));
|
2026-11-03 22:29:42 +08:00
|
|
|
|
b.AddLabel(p.Summary);
|
2026-11-03 23:05:13 +08:00
|
|
|
|
b.BeginRow(8f, TextAnchor.MiddleLeft);
|
2026-11-04 10:10:34 +08:00
|
|
|
|
b.AddLabel(L.T("black.pay.to", p.Address), "pay_to_dim");
|
2026-11-03 23:05:13 +08:00
|
|
|
|
b.AddButton(L.T("hw.copy"), () => { Clipboard.Copy(p.Address); _message = L.T("hw.copied.addr", p.Address); Open(); });
|
|
|
|
|
|
b.End();
|
2026-11-03 22:29:42 +08:00
|
|
|
|
b.AddLabel(L.T("black.pay.amount", WalletWindow.Amt(p.Amount), p.Asset, Fmt.Credits(p.Amount * Session.Price(p.Asset))));
|
2026-11-04 10:10:34 +08:00
|
|
|
|
b.BeginRow(6f);
|
2026-11-03 22:29:42 +08:00
|
|
|
|
b.AddButton(L.T("black.pay.wallet"), () => WalletWindow.OpenForBill(p));
|
|
|
|
|
|
b.AddButton(L.T("black.pay.cancel"), () => { WalletWindow.Pending = null; _message = null; Open(); });
|
2026-11-03 21:31:50 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 22:58:24 +08:00
|
|
|
|
private static void BuyOffer(BmOffer o, int count, string asset, Wallet pw)
|
2026-11-04 11:18:45 +08:00
|
|
|
|
{
|
|
|
|
|
|
int bought = BuyOfferCore(o, count, asset, pw);
|
|
|
|
|
|
if (bought == 0) { _message = L.T("msg.balance"); Open(); return; }
|
|
|
|
|
|
_message = L.T("black.bought", ItemName(o.Item) + (bought > 1 ? " ×" + bought : ""));
|
|
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// <summary>平台现货买 count 件:每件从钱包扣币,货进储藏区。返回实际买到的件数。</summary>
|
|
|
|
|
|
private static int BuyOfferCore(BmOffer o, int count, string asset, Wallet pw)
|
2026-11-03 19:35:48 +08:00
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-03 20:06:32 +08:00
|
|
|
|
string item = o.Item, model = o.Model, fake = o.FakeOf;
|
|
|
|
|
|
double health = o.Health;
|
2026-11-03 22:04:31 +08:00
|
|
|
|
int bought = 0;
|
|
|
|
|
|
for (int i = 0; i < count; i++)
|
|
|
|
|
|
{
|
2026-11-03 22:58:24 +08:00
|
|
|
|
if (!d.Black.Buy(o, pw, asset, Session.Price)) break;
|
2026-11-03 22:04:31 +08:00
|
|
|
|
Items.Give(BlackDelivery.Make(item, model, fake, health));
|
|
|
|
|
|
bought++;
|
|
|
|
|
|
}
|
2026-11-04 11:18:45 +08:00
|
|
|
|
return bought;
|
2026-11-03 19:35:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 22:58:24 +08:00
|
|
|
|
private static void PlaceOrder(BmListing l, int count, string asset, PayMode mode, Wallet pw)
|
2026-11-03 19:48:26 +08:00
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-03 22:58:24 +08:00
|
|
|
|
var o = d.Black.Place(l, count, mode, pw, asset, StoreStation.GetDayCounter(), Session.Price);
|
2026-11-03 19:48:26 +08:00
|
|
|
|
if (o == null) { _message = L.T("msg.balance"); Open(); return; }
|
2026-11-03 20:50:58 +08:00
|
|
|
|
_message = L.T("black.ordered", ItemName(o.Item) + (o.Count > 1 ? " ×" + o.Count : ""), o.DueDay);
|
2026-11-03 19:48:26 +08:00
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 21:32:30 +08:00
|
|
|
|
/// <summary>在途订单:买入还没取货的订单,和卖出已成交、货款在托管中的订单;每条显示状态与到期日。</summary>
|
2026-11-03 22:04:31 +08:00
|
|
|
|
private static void OrdersPage(CustomUIBuilder b, int day)
|
2026-11-03 19:48:26 +08:00
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-05 21:32:30 +08:00
|
|
|
|
var buys = d.Black.Orders.Where(o => o.State == OrderState.Pending || o.State == OrderState.Ready).OrderByDescending(x => x.PlacedDay).ToList();
|
|
|
|
|
|
var sells = d.Black.Sales.Where(x => x.State == SaleState.Sold).OrderByDescending(x => x.ListedDay).ToList();
|
2026-11-04 13:37:54 +08:00
|
|
|
|
b.BeginRow(8f, TextAnchor.UpperLeft);
|
2026-11-05 21:32:30 +08:00
|
|
|
|
b.BeginPanel((SellerW + 8f + ShelfW) * 0.5f, -1f, TextAnchor.UpperLeft, 4f, 4f);
|
2026-11-04 13:37:54 +08:00
|
|
|
|
_clear.Add(b.Last.image);
|
2026-11-03 22:04:31 +08:00
|
|
|
|
b.AddHeader(L.T("black.orders.in"));
|
2026-11-05 21:32:30 +08:00
|
|
|
|
if (buys.Count == 0) b.AddLabel(L.T("black.mine.none"), "orders_in_none_dim");
|
|
|
|
|
|
foreach (var o in buys)
|
|
|
|
|
|
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.DueDay));
|
2026-11-04 13:37:54 +08:00
|
|
|
|
b.End();
|
2026-11-05 21:32:30 +08:00
|
|
|
|
b.BeginPanel((SellerW + 8f + ShelfW) * 0.5f, -1f, TextAnchor.UpperLeft, 4f, 4f);
|
2026-11-04 13:37:54 +08:00
|
|
|
|
_clear.Add(b.Last.image);
|
2026-11-03 22:04:31 +08:00
|
|
|
|
b.AddHeader(L.T("black.orders.out"));
|
2026-11-05 21:32:30 +08:00
|
|
|
|
if (sells.Count == 0) b.AddLabel(L.T("black.mine.none"), "orders_out_none_dim");
|
|
|
|
|
|
foreach (var sale in sells)
|
|
|
|
|
|
b.AddLabel(L.T("black.mine.sale", ItemName(sale.Item), sale.Ref, sale.ListPrice, sale.Mult.ToString("0.00", CultureInfo.InvariantCulture), L.T("black.sale.Sold", sale.PayDay, sale.PayAsset ?? Assets.CRT)));
|
2026-11-04 13:37:54 +08:00
|
|
|
|
b.End();
|
|
|
|
|
|
b.End();
|
2026-11-03 19:48:26 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 14:26:21 +08:00
|
|
|
|
/// <summary>隐私币兑换:Web3 交易页里的一个通用兑换块,带"隐私币"标签。要黑市声望,有每日额度;兑入时这部分价值的污点被清除。</summary>
|
2026-11-03 22:04:31 +08:00
|
|
|
|
private static void PoneroBlock(CustomUIBuilder r)
|
2026-11-03 19:35:48 +08:00
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
var w = d.Hot;
|
2026-11-03 22:04:31 +08:00
|
|
|
|
int day = StoreStation.GetDayCounter();
|
|
|
|
|
|
r.AddSeparator();
|
|
|
|
|
|
r.AddLabel(L.T("black.desk.pair", Fmt.Num(Session.Price(Assets.Ponero))));
|
2026-11-04 10:04:00 +08:00
|
|
|
|
r.AddLabel(L.T("black.desk.left", Fmt.Credits(d.Black.DeskLeft(day)), Fmt.Credits(Tuning.PoneroDailyCap)), "desk_left_dim");
|
|
|
|
|
|
r.BeginRow(4f);
|
2026-11-03 22:04:31 +08:00
|
|
|
|
foreach (var a in new[] { Assets.PSC, Assets.CRT, Assets.CRA })
|
2026-11-03 19:35:48 +08:00
|
|
|
|
{
|
2026-11-03 22:04:31 +08:00
|
|
|
|
var asset = a;
|
2026-11-04 10:04:00 +08:00
|
|
|
|
PledgerSkin.Pick(r, asset, asset == _deskAsset, () => { _deskAsset = asset; Open(); }, _picked);
|
2026-11-03 22:04:31 +08:00
|
|
|
|
}
|
|
|
|
|
|
r.End();
|
2026-11-04 10:04:00 +08:00
|
|
|
|
r.BeginRow(6f);
|
2026-11-03 22:04:31 +08:00
|
|
|
|
r.AddButton(L.T("black.desk.in"), () => { _deskIn = true; Swap(); });
|
|
|
|
|
|
r.AddButton(L.T("black.desk.out"), () => { _deskIn = false; Swap(); });
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
void Swap()
|
2026-11-03 19:35:48 +08:00
|
|
|
|
{
|
2026-11-03 22:04:31 +08:00
|
|
|
|
if (!ReadAmount(out double dv)) return;
|
2026-11-03 22:29:42 +08:00
|
|
|
|
bool into = _deskIn; string other = _deskAsset;
|
|
|
|
|
|
string asset = into ? other : Assets.Ponero;
|
|
|
|
|
|
_web3Tab = Web3Tab.Trade;
|
2026-11-03 22:58:24 +08:00
|
|
|
|
BillFor(d.Address("desk"), L.T("pay.swap", Fmt.Num(dv), asset), asset, dv, pw => DeskSwap(dv, into, other, pw));
|
2026-11-03 22:04:31 +08:00
|
|
|
|
}
|
2026-11-03 19:35:48 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 22:58:24 +08:00
|
|
|
|
private static void DeskSwap(double v, bool into, string other, Wallet pw)
|
2026-11-03 19:35:48 +08:00
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
2026-11-03 22:29:42 +08:00
|
|
|
|
_deskIn = into; _deskAsset = other;
|
2026-11-03 19:35:48 +08:00
|
|
|
|
int day = StoreStation.GetDayCounter();
|
2026-11-03 22:58:24 +08:00
|
|
|
|
double got = _deskIn ? d.Black.ToPonero(pw, _deskAsset, v, day, Session.Price) : d.Black.FromPonero(pw, _deskAsset, v, day, Session.Price);
|
2026-11-03 19:35:48 +08:00
|
|
|
|
if (got < 0) { _message = L.T("black.desk.fail"); Open(); return; }
|
2026-11-04 08:51:31 +08:00
|
|
|
|
{ double cr = v * Session.Price(_deskIn ? _deskAsset : Assets.Ponero); CandleChart.Trade(Assets.Ponero, cr); CandleChart.Trade(_deskAsset, cr); }
|
2026-11-03 19:35:48 +08:00
|
|
|
|
// 从 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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 16:31:42 +08:00
|
|
|
|
private enum Web3Tab { Trade, Stake, Airdrop, Predict, Ponzi }
|
2026-11-03 20:06:32 +08:00
|
|
|
|
private static Web3Tab _web3Tab = Web3Tab.Trade;
|
2026-11-03 16:39:40 +08:00
|
|
|
|
private static string _mixAsset = Assets.PSC;
|
|
|
|
|
|
private static int _mixRounds = 2;
|
2026-11-04 15:34:38 +08:00
|
|
|
|
private static readonly float[] MixWBase = new[] { 70f, 100f, 100f, 80f };
|
|
|
|
|
|
private static float[] MixW => Sc(MixWBase);
|
2026-11-04 10:06:07 +08:00
|
|
|
|
private static readonly TextAnchor[] MixAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight, TextAnchor.MiddleRight };
|
2026-11-03 16:39:40 +08:00
|
|
|
|
|
|
|
|
|
|
/// <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"));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
l.AddLabel(d.Story.MixerClosed ? L.T("mixer.status.closed") : d.Mixer.Sanctioned(day) ? L.T("mixer.sanctioned", d.Mixer.SanctionedUntil - day + 1) : L.T("mixer.ok"), d.Story.MixerClosed || d.Mixer.Sanctioned(day) ? "mixer_status_note" : "mixer_status_dim");
|
|
|
|
|
|
if (w.Taint > 0.5) l.AddLabel(L.T("wallet.taint", Fmt.Credits(w.Taint)), "taint_note");
|
2026-11-03 16:39:40 +08:00
|
|
|
|
l.AddSeparator();
|
|
|
|
|
|
l.AddHeader(L.T("mixer.pending"));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (d.Mixer.Pending.Count == 0) l.AddLabel(L.T("mixer.none"), "mixer_none_dim");
|
2026-11-03 16:39:40 +08:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-11-04 10:06:07 +08:00
|
|
|
|
TableHeader(l, MixW.Sum() + 8f, MixW, MixAligns, L.T("col.asset"), L.T("col.amount"), L.T("mixer.col.taint"), L.T("mixer.col.ready"));
|
2026-11-03 16:39:40 +08:00
|
|
|
|
foreach (var p in d.Mixer.Pending)
|
2026-11-04 10:06:07 +08:00
|
|
|
|
TableRow(l, MixW.Sum() + 8f, MixW, MixAligns, new[] { p.Asset, Fmt.Num(p.Amount), Fmt.Credits(p.Taint), L.T("mixer.day", p.ReadyDay) });
|
2026-11-03 16:39:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
}, r =>
|
|
|
|
|
|
{
|
|
|
|
|
|
r.AddHeader(L.T("mixer.deposit"));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (d.Story.MixerClosed) { r.AddLabel(L.T("mixer.closed"), "mixer_closed_note"); return; }
|
|
|
|
|
|
r.BeginRow(4f);
|
2026-11-03 16:39:40 +08:00
|
|
|
|
foreach (var a in new[] { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA })
|
|
|
|
|
|
{
|
|
|
|
|
|
var asset = a;
|
2026-11-04 10:06:07 +08:00
|
|
|
|
PledgerSkin.Pick(r, asset, asset == _mixAsset, () => { _mixAsset = asset; Open(); }, _picked);
|
2026-11-03 16:39:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
r.End();
|
2026-11-03 16:51:12 +08:00
|
|
|
|
r.AddLabel(L.T("mixer.balance", _mixAsset, Fmt.Num(w.Avail(_mixAsset))));
|
2026-11-03 16:39:40 +08:00
|
|
|
|
AmountRow(r);
|
|
|
|
|
|
r.AddLabel(L.T("mixer.rounds"));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
r.BeginRow(4f);
|
2026-11-03 16:39:40 +08:00
|
|
|
|
for (int n = 1; n <= Tuning.MixerMaxRounds; n++)
|
|
|
|
|
|
{
|
|
|
|
|
|
int rounds = n;
|
2026-11-04 10:06:07 +08:00
|
|
|
|
PledgerSkin.Pick(r, rounds.ToString(), rounds == _mixRounds, () => { _mixRounds = rounds; Open(); }, _picked);
|
2026-11-03 16:39:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double v) && v > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
double rate = Mixer.RoundRate(v * Session.Price(_mixAsset));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
r.AddLabel(L.T("mixer.preview", Fmt.Num(v * Math.Pow(1 - Mixer.Fee, _mixRounds)), _mixAsset, (int)Math.Round((1 - Math.Pow(1 - rate, _mixRounds)) * 100), day + _mixRounds), "mixer_preview_dim");
|
2026-11-03 16:39:40 +08:00
|
|
|
|
}
|
2026-11-03 22:29:42 +08:00
|
|
|
|
r.AddButton(L.T("mixer.go"), () =>
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!ReadAmount(out double mv)) return;
|
|
|
|
|
|
string asset = _mixAsset; int rounds = _mixRounds;
|
2026-11-03 22:58:24 +08:00
|
|
|
|
BillFor(d.Address("mixer"), L.T("pay.mix", Fmt.Num(mv), asset), asset, mv, pw => MixDeposit(mv, asset, rounds, pw));
|
2026-11-03 22:29:42 +08:00
|
|
|
|
});
|
2026-11-03 16:39:40 +08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 22:58:24 +08:00
|
|
|
|
private static void MixDeposit(double v, string asset, int rounds, Wallet pw)
|
2026-11-03 16:39:40 +08:00
|
|
|
|
{
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
int day = StoreStation.GetDayCounter();
|
2026-11-03 22:58:24 +08:00
|
|
|
|
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);
|
2026-11-03 22:29:42 +08:00
|
|
|
|
_message = dep == null ? L.T("mixer.min", Tuning.MixerMin) : L.T("mixer.done", Fmt.Num(v), asset, dep.ReadyDay);
|
2026-11-03 16:39:40 +08:00
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-04 15:34:38 +08:00
|
|
|
|
private static readonly float[] StakeWBase = new[] { 110f, 90f, 200f };
|
|
|
|
|
|
private static float[] StakeW => Sc(StakeWBase);
|
2026-11-04 10:04:00 +08:00
|
|
|
|
private static readonly TextAnchor[] StakeAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleRight, TextAnchor.MiddleRight };
|
2026-11-04 00:06:16 +08:00
|
|
|
|
|
|
|
|
|
|
/// <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"));
|
2026-11-04 10:04:00 +08:00
|
|
|
|
l.AddLabel(L.T("stake.rate", (st.CraRate * 100).ToString("0.0", CultureInfo.InvariantCulture), Tuning.CraUnlockDays) + (st.CraSubsidyLow ? " " + L.T("stake.low") : ""), st.CraSubsidyLow ? "stake_low_note" : "stake_rate_dim");
|
2026-11-04 00:06:16 +08:00
|
|
|
|
l.AddLabel(L.T("stake.cra", Fmt.Num(st.Cra), StableStatus(st, Assets.CRA, day)));
|
|
|
|
|
|
l.AddSeparator();
|
2026-11-04 10:04:00 +08:00
|
|
|
|
if (d.Staking.Lots.Count == 0) l.AddLabel(L.T("stake.none"), "stake_none_dim");
|
2026-11-04 00:06:16 +08:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2026-11-04 10:04:00 +08:00
|
|
|
|
TableHeader(l, StakeW.Sum() + 8f, StakeW, StakeAligns, L.T("stake.col.wallet"), L.T("stake.col.amount"), L.T("stake.col.queue"));
|
2026-11-04 00:06:16 +08:00
|
|
|
|
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)));
|
2026-11-04 10:04:00 +08:00
|
|
|
|
TableRow(l, StakeW.Sum() + 8f, StakeW, StakeAligns,
|
|
|
|
|
|
new[] { ow != null ? WalletWindow.NameOf(ow) : lot.Wallet, Fmt.Num(lot.Amount), queue });
|
2026-11-04 00:06:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
l.AddSeparator();
|
2026-11-05 07:14:38 +08:00
|
|
|
|
l.AddLabel(L.T("mint.reserve", (st.Ratio * 100).ToString("0", CultureInfo.InvariantCulture), (st.C * 100).ToString("0", CultureInfo.InvariantCulture)), "mint_reserve_dim");
|
2026-11-04 00:06:16 +08:00
|
|
|
|
l.AddLabel(L.T("spark.price", Fmt.Num(st.Spark)));
|
2026-11-05 07:14:38 +08:00
|
|
|
|
var mine = st.Orders.Where(o => d.Own(o.Wallet) != null).ToList();
|
|
|
|
|
|
if (mine.Count > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
l.AddSeparator();
|
|
|
|
|
|
l.AddLabel(L.T("mint.queue"));
|
|
|
|
|
|
foreach (var o in mine) l.AddLabel(L.T("mint.order", L.T(o.Mint ? "cra.mint" : "cra.burn"), Fmt.Num(o.Cra), o.Due), "mint_order_dim");
|
|
|
|
|
|
}
|
2026-11-04 00:06:16 +08:00
|
|
|
|
}, r =>
|
|
|
|
|
|
{
|
|
|
|
|
|
r.AddHeader(L.T("stake.ops", WalletWindow.NameOf(w)));
|
2026-11-04 10:04:00 +08:00
|
|
|
|
r.AddLabel(L.T("stake.avail", Fmt.Num(w.Avail(Assets.CRA)), Fmt.Num(d.Staking.Of(w.Address)?.Amount ?? 0)), "stake_avail_dim");
|
|
|
|
|
|
r.AddLabel(L.T("stake.avail2", Fmt.Num(w.Avail(Assets.SPARK))), "stake_avail2_dim");
|
2026-11-04 00:06:16 +08:00
|
|
|
|
AmountRow(r);
|
2026-11-04 10:04:00 +08:00
|
|
|
|
r.AddSpace(4f);
|
2026-11-05 07:14:38 +08:00
|
|
|
|
r.AddLabel(L.T("stake.coin"));
|
|
|
|
|
|
r.BeginRow(4f);
|
|
|
|
|
|
foreach (var c in StakeCoins)
|
|
|
|
|
|
{
|
|
|
|
|
|
var coin = c;
|
|
|
|
|
|
PledgerSkin.Pick(r, coin, coin == _stakeCoin, () => { _stakeCoin = coin; Open(); }, _picked);
|
|
|
|
|
|
}
|
|
|
|
|
|
r.End();
|
2026-11-04 10:04:00 +08:00
|
|
|
|
r.BeginRow(6f);
|
2026-11-04 00:06:16 +08:00
|
|
|
|
r.AddButton(L.T("stake.do"), () => StakeCall(true));
|
|
|
|
|
|
r.AddButton(L.T("stake.undo"), () => StakeCall(false));
|
|
|
|
|
|
r.End();
|
|
|
|
|
|
r.AddSeparator();
|
2026-11-05 07:14:38 +08:00
|
|
|
|
r.AddHeader(L.T("mint.h"));
|
|
|
|
|
|
if (double.TryParse((_amount ?? "").Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double mv) && mv > 0)
|
|
|
|
|
|
{
|
|
|
|
|
|
st.Collateral(mv, out double needCrc, out double needSpark);
|
|
|
|
|
|
r.AddLabel(L.T("mint.need", Fmt.Num(needCrc), Fmt.Num(needSpark)), "mint_need_dim");
|
|
|
|
|
|
}
|
2026-11-04 10:04:00 +08:00
|
|
|
|
r.BeginRow(6f);
|
2026-11-05 07:14:38 +08:00
|
|
|
|
r.AddButton(L.T("cra.mint"), () => MintCall(true));
|
|
|
|
|
|
r.AddButton(L.T("cra.burn"), () => MintCall(false));
|
2026-11-04 00:06:16 +08:00
|
|
|
|
r.End();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 07:14:38 +08:00
|
|
|
|
private static readonly string[] StakeCoins = { Assets.CRA, Assets.CRC, Assets.CRT, Assets.SPARK };
|
|
|
|
|
|
private static string _stakeCoin = Assets.CRA;
|
|
|
|
|
|
|
2026-11-04 00:06:16 +08:00
|
|
|
|
/// <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;
|
2026-11-05 07:14:38 +08:00
|
|
|
|
string coin = into ? _stakeCoin : Assets.CRA;
|
|
|
|
|
|
if (into ? w.Avail(coin) + 1e-9 < v : (d.Staking.Of(w.Address)?.Amount ?? 0) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
|
|
|
|
|
var q = into && coin != Assets.CRA ? d.Market.Quote(coin, Assets.CRA, v) : null;
|
2026-11-04 00:06:16 +08:00
|
|
|
|
_message = L.T("hw.wait");
|
|
|
|
|
|
Open();
|
|
|
|
|
|
WalletWindow.OpenForBill(new WalletWindow.Bill
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("staking"), Asset = coin, Amount = v, Spend = into ? v : 0, FeeAsset = into ? null : BestFeeAsset(w),
|
2026-11-05 07:14:38 +08:00
|
|
|
|
Summary = into && q != null ? L.T("stake.sum.via", Fmt.Num(v), coin, Fmt.Num(q.Out)) : L.T(into ? "stake.sum.in" : "stake.sum.out", Fmt.Num(v), Tuning.CraUnlockDays),
|
2026-11-04 00:06:16 +08:00
|
|
|
|
Execute = pw =>
|
|
|
|
|
|
{
|
|
|
|
|
|
int day = StoreStation.GetDayCounter();
|
2026-11-05 07:14:38 +08:00
|
|
|
|
double cra = v;
|
|
|
|
|
|
if (into && coin != Assets.CRA)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (pw.Avail(coin) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
|
|
|
|
|
|
double spent = v * Session.Price(coin);
|
|
|
|
|
|
cra = d.Market.Swap(coin, Assets.CRA, v);
|
|
|
|
|
|
pw.Add(coin, -v); pw.Add(Assets.CRA, cra); pw.RecordBuy(Assets.CRA, cra, spent);
|
|
|
|
|
|
CandleChart.Trade(coin, spent); CandleChart.Trade(Assets.CRA, spent);
|
|
|
|
|
|
}
|
|
|
|
|
|
bool ok = into ? d.Staking.Stake(pw, cra, Session.Price) : d.Staking.RequestUnstake(pw.Address, v, day);
|
|
|
|
|
|
_message = ok ? L.T(into ? "stake.done.in" : "stake.done.out", Fmt.Num(cra), day + Tuning.CraUnlockDays) : L.T("msg.balance");
|
2026-11-04 00:06:16 +08:00
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-05 07:14:38 +08:00
|
|
|
|
/// <summary>官方一级市场:存入 CRC 和 SPARK 铸造 CRA(铸出后直接进质押),或销毁 CRA 换回两者。排队等待,官方可能不兑现。</summary>
|
|
|
|
|
|
private static void MintCall(bool mint)
|
2026-11-04 00:06:16 +08:00
|
|
|
|
{
|
|
|
|
|
|
if (!ReadAmount(out double v)) return;
|
|
|
|
|
|
var d = Session.Data;
|
|
|
|
|
|
var w = d.Hot;
|
2026-11-05 07:14:38 +08:00
|
|
|
|
var st = d.Stable;
|
|
|
|
|
|
st.Collateral(v, out double needCrc, out double needSpark);
|
|
|
|
|
|
bool enough = mint ? w.Avail(Assets.CRC) + 1e-9 >= needCrc && w.Avail(Assets.SPARK) + 1e-9 >= needSpark : w.Avail(Assets.CRA) + 1e-9 >= v;
|
|
|
|
|
|
if (!enough) { _message = L.T("msg.balance"); Open(); return; }
|
|
|
|
|
|
int minD = Tuning.CraMintDaysMin, maxD = Tuning.CraMintDaysMax;
|
2026-11-04 00:06:16 +08:00
|
|
|
|
_message = L.T("hw.wait");
|
|
|
|
|
|
Open();
|
|
|
|
|
|
WalletWindow.OpenForBill(new WalletWindow.Bill
|
|
|
|
|
|
{
|
2026-11-07 22:39:18 +08:00
|
|
|
|
Contract = true, Party = Session.Data.Address("cra"), Asset = mint ? Assets.CRC : Assets.CRA, Amount = mint ? needCrc : v, Spend = mint ? needCrc : v,
|
2026-11-05 07:14:38 +08:00
|
|
|
|
Summary = mint ? L.T("mint.sum.in", Fmt.Num(v), Fmt.Num(needCrc), Fmt.Num(needSpark), minD, maxD) : L.T("mint.sum.out", Fmt.Num(v), Tuning.CraUnlockDays),
|
2026-11-04 00:06:16 +08:00
|
|
|
|
Execute = pw =>
|
|
|
|
|
|
{
|
2026-11-05 07:14:38 +08:00
|
|
|
|
int day = StoreStation.GetDayCounter();
|
|
|
|
|
|
var o = mint ? st.RequestMint(pw, v, day, d.Rng) : st.RequestBurn(pw, v, day);
|
|
|
|
|
|
_message = o == null ? L.T("msg.balance") : L.T(mint ? "mint.done.in" : "mint.done.out", o.Due);
|
2026-11-04 00:06:16 +08:00
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
},
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 16:51:12 +08:00
|
|
|
|
private static bool frozenPay(Wallet w, string asset) => w.Frozen.ContainsKey(asset);
|
|
|
|
|
|
|
2026-11-03 15:35:48 +08:00
|
|
|
|
private static string AvgText(Wallet w, string asset) => w.AvgBuy(asset) is double a ? Fmt.Num(a) : L.T("avg.none");
|
|
|
|
|
|
|
2026-11-03 09:11:46 +08:00
|
|
|
|
private static void ReportPage(CustomUIBuilder b)
|
|
|
|
|
|
{
|
|
|
|
|
|
var r = Session.Data.LastNight;
|
2026-11-04 10:06:07 +08:00
|
|
|
|
if (r == null) { b.AddLabel(L.T("report.none"), "report_none_dim"); return; }
|
2026-11-03 09:11:46 +08:00
|
|
|
|
b.AddHeader(L.T("report.day", r.Day));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
b.AddLabel(L.T("report.rigs", r.PoweredRigs, r.Rigs), "report_dim");
|
|
|
|
|
|
b.AddLabel(L.T("report.hash", Fmt.Num(r.Hash * 10), Fmt.Num(r.NetworkHash * 10)), "report_dim");
|
2026-11-03 09:11:46 +08:00
|
|
|
|
b.AddLabel(L.T("report.mined", Fmt.Num(r.Mined), Fmt.Credits(r.Mined * r.Price)));
|
2026-11-04 10:06:07 +08:00
|
|
|
|
b.AddLabel(L.T("report.energy", Fmt.Num(r.Kj)), "report_dim");
|
|
|
|
|
|
b.AddLabel(L.T("report.burnt", r.Burnt.Count), r.Burnt.Count > 0 ? "report_burnt_note" : "report_burnt_dim");
|
|
|
|
|
|
foreach (var n in r.Burnt) b.AddLabel(" " + n, "report_burnt_note");
|
|
|
|
|
|
b.AddLabel(L.T("report.price", Fmt.Num(r.Price)), "report_dim");
|
2026-11-03 09:11:46 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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);
|
2026-11-03 22:58:24 +08:00
|
|
|
|
var w = Session.Data.Poinbase;
|
2026-11-03 09:11:46 +08:00
|
|
|
|
w.Add(Assets.CRC, credits / Session.Data.Stable.Crc);
|
2026-11-03 15:35:48 +08:00
|
|
|
|
w.RecordBuy(Assets.CRC, credits / Session.Data.Stable.Crc, credits);
|
2026-11-03 09:11:46 +08:00
|
|
|
|
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;
|
2026-11-04 00:26:50 +08:00
|
|
|
|
if (d.Perps.Debt > 0) { _message = L.T("perp.debt.block"); Open(); return; }
|
2026-11-03 22:58:24 +08:00
|
|
|
|
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);
|
2026-11-03 09:11:46 +08:00
|
|
|
|
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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-07 22:24:36 +08:00
|
|
|
|
/// <summary>交易:Poinbase 是托管账户,直接成交。输入的数量是标的币:买入按池子反算要付的计价币。</summary>
|
2026-11-03 09:11:46 +08:00
|
|
|
|
private static void Trade(Pool p, bool buy)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (!ReadAmount(out double v)) return;
|
2026-11-05 07:09:05 +08:00
|
|
|
|
var w = Session.Data.Poinbase;
|
2026-11-07 22:24:36 +08:00
|
|
|
|
if (buy) v = p.QuoteIn(v);
|
2026-11-05 07:09:05 +08:00
|
|
|
|
string pay = buy ? p.Quote : p.Base;
|
2026-11-07 22:24:36 +08:00
|
|
|
|
if (double.IsInfinity(v) || w.Avail(pay) + 1e-9 < v) { _message = L.T(frozenPay(w, pay) ? "msg.frozen" : "msg.balance"); Open(); return; }
|
2026-11-05 07:09:05 +08:00
|
|
|
|
DoTrade(p, buy, v, w);
|
2026-11-03 22:29:42 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-11-03 22:58:24 +08:00
|
|
|
|
private static void DoTrade(Pool p, bool buy, double v, Wallet w)
|
2026-11-03 22:29:42 +08:00
|
|
|
|
{
|
2026-11-03 09:11:46 +08:00
|
|
|
|
string pay = buy ? p.Quote : p.Base, get = buy ? p.Base : p.Quote;
|
2026-11-03 16:51:12 +08:00
|
|
|
|
if (w.Avail(pay) + 1e-9 < v) { _message = L.T(frozenPay(w, pay) ? "msg.frozen" : "msg.balance"); Open(); return; }
|
2026-11-04 08:03:44 +08:00
|
|
|
|
double before = p.Price;
|
2026-11-03 09:11:46 +08:00
|
|
|
|
double got = buy ? p.BuyBase(v) : p.SellBase(v);
|
2026-11-04 08:51:31 +08:00
|
|
|
|
if (p == Session.Data.Market.Main) { Session.Data.Market.PlayerPush += Math.Log(p.Price / before); Log.Info($"现货推价:累计 {Session.Data.Market.PlayerPush:0.000}"); }
|
2026-11-03 15:35:48 +08:00
|
|
|
|
double spent = v * Session.Price(pay);
|
2026-11-04 08:51:31 +08:00
|
|
|
|
CandleChart.Trade(pay, spent); CandleChart.Trade(get, spent);
|
2026-11-03 09:11:46 +08:00
|
|
|
|
w.Add(pay, -v);
|
|
|
|
|
|
w.Add(get, got);
|
2026-11-03 15:35:48 +08:00
|
|
|
|
w.RecordBuy(get, got, spent);
|
2026-11-03 09:11:46 +08:00
|
|
|
|
_message = L.T("msg.trade", Fmt.Num(v), pay, Fmt.Num(got), get);
|
|
|
|
|
|
Session.Save();
|
|
|
|
|
|
Open();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|