第十阶段 stage 2(四):浏览器页(全局账本)、Web3 页签折行
This commit is contained in:
1 parent
e3e5ec2d6e
commit
a3e955ca59
17 files changed
+199
-6
No files matched your search
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace Decentralized.Domain
|
||||
{
|
||||
/// <summary>浏览器里的一条链上转账:区块、天、转出方、转入方、币种、数额、手续费。</summary>
|
||||
public sealed class LedgerEntry
|
||||
{
|
||||
public long Block;
|
||||
public int Day;
|
||||
public string From, To, Asset;
|
||||
public double Amount, Fee;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 全局账本:只记真实发生的转账(玩家的转出和转入、矿机收益、出金),不生成 NPC 流水;最新的在前,超过 <see cref="Tuning.LedgerMax"/> 条丢最旧的。
|
||||
/// </summary>
|
||||
public sealed class Ledger
|
||||
{
|
||||
public List<LedgerEntry> Entries = new List<LedgerEntry>();
|
||||
|
||||
public void Add(LedgerEntry e)
|
||||
{
|
||||
if (e == null || e.Amount <= 0) return;
|
||||
Entries.Insert(0, e);
|
||||
if (Entries.Count > Tuning.LedgerMax) Entries.RemoveRange(Tuning.LedgerMax, Entries.Count - Tuning.LedgerMax);
|
||||
}
|
||||
|
||||
/// <summary>某个地址相关(转出或转入)的记录,最新的在前,最多 limit 条。address 为空时返回全部最近的。</summary>
|
||||
public List<LedgerEntry> Of(string address, int limit)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(address)) return Entries.Take(limit).ToList();
|
||||
return Entries.Where(e => ChainAddress.Same(e.From, address) || ChainAddress.Same(e.To, address)).Take(limit).ToList();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -61,6 +61,8 @@ namespace Decentralized.Domain
|
||||
public const double CraRugPerDepeg = 1;
|
||||
/// <summary>Poinbase 出金到钱包的确认动画:基础秒数,加每个确认的秒数(确认数同 PLEDGER 的 TxConfirm*)。</summary>
|
||||
public const double FundsBaseSecs = 1.2, FundsConfSecs = 0.6;
|
||||
/// <summary>浏览器账本最多保留的转账条数,一次最多显示的条数。</summary>
|
||||
public const int LedgerMax = 200, LedgerShow = 12;
|
||||
public const double CraSpiralWeekly = 0.03;
|
||||
public const int CraWarnLeadMin = 4, CraWarnLeadMax = 7;
|
||||
public static readonly double[] CraSpiralPath = { 0.85, 0.30, 0.02 };
|
||||
|
||||
@@ -244,6 +244,8 @@ namespace Decentralized.Domain
|
||||
["IntelBriefPrice"] = Tuning.IntelBriefPrice,
|
||||
["IntelFake"] = Tuning.IntelFake,
|
||||
["IntelRep"] = Tuning.IntelRep,
|
||||
["LedgerMax"] = Tuning.LedgerMax,
|
||||
["LedgerShow"] = Tuning.LedgerShow,
|
||||
["ManipBase"] = Tuning.ManipBase,
|
||||
["ManipFree"] = Tuning.ManipFree,
|
||||
["ManipFreezeDays"] = Tuning.ManipFreezeDays,
|
||||
|
||||
@@ -97,6 +97,8 @@ namespace Decentralized
|
||||
double net = amt - fee;
|
||||
var own = d.Own(t.To);
|
||||
Wallet.Transfer(pb, own ?? new Wallet(), t.Asset, net, Session.Price);
|
||||
d.Block += 1;
|
||||
d.Ledger.Add(new LedgerEntry { Block = d.Block, Day = StoreStation.GetDayCounter(), From = d.Address("poinbase"), To = t.To, Asset = t.Asset, Amount = net, Fee = fee });
|
||||
if (own != null)
|
||||
{
|
||||
TxRules.Record(own.History, new TxRecord { Addr = d.Address("poinbase"), Asset = t.Asset, Amount = net, Day = StoreStation.GetDayCounter(), In = true });
|
||||
|
||||
@@ -930,3 +930,12 @@ funds.err.addr,Ungültige Adresse.
|
||||
funds.err.self,Das ist die Einzahlungsadresse von Poinbase.
|
||||
funds.err.known,Diese Adresse akzeptiert nur Zahlungen gegen Rechnung.
|
||||
funds.err.fee,Der Betrag deckt die Gebühr nicht.
|
||||
web3.sub.explorer,Explorer
|
||||
explorer.search,Adresse
|
||||
explorer.all,Neueste
|
||||
explorer.none,Keine Überweisungen.
|
||||
explorer.col.block,Block
|
||||
explorer.col.flow,Von → An
|
||||
explorer.col.amount,Betrag
|
||||
explorer.net,Netzwerk
|
||||
explorer.mixer,Mixer
|
||||
|
@@ -930,3 +930,12 @@ funds.err.addr,Invalid address.
|
||||
funds.err.self,That is the Poinbase deposit address.
|
||||
funds.err.known,That address only accepts payments against a bill.
|
||||
funds.err.fee,The amount does not cover the fee.
|
||||
web3.sub.explorer,Explorer
|
||||
explorer.search,Address
|
||||
explorer.all,Latest
|
||||
explorer.none,No transfers.
|
||||
explorer.col.block,Block
|
||||
explorer.col.flow,From → To
|
||||
explorer.col.amount,Amount
|
||||
explorer.net,Network
|
||||
explorer.mixer,Mixer
|
||||
|
@@ -930,3 +930,12 @@ funds.err.addr,Dirección no válida.
|
||||
funds.err.self,Esa es la dirección de depósito de Poinbase.
|
||||
funds.err.known,Esa dirección solo acepta pagos contra una factura.
|
||||
funds.err.fee,La cantidad no cubre la comisión.
|
||||
web3.sub.explorer,Explorador
|
||||
explorer.search,Dirección
|
||||
explorer.all,Recientes
|
||||
explorer.none,Sin transferencias.
|
||||
explorer.col.block,Bloque
|
||||
explorer.col.flow,De → A
|
||||
explorer.col.amount,Cantidad
|
||||
explorer.net,Red
|
||||
explorer.mixer,Mezclador
|
||||
|
@@ -930,3 +930,12 @@ funds.err.addr,Adresse invalide.
|
||||
funds.err.self,C'est l'adresse de dépôt de Poinbase.
|
||||
funds.err.known,Cette adresse n'accepte que les paiements d'une facture.
|
||||
funds.err.fee,Le montant ne couvre pas les frais.
|
||||
web3.sub.explorer,Explorateur
|
||||
explorer.search,Adresse
|
||||
explorer.all,Récent
|
||||
explorer.none,Aucun transfert.
|
||||
explorer.col.block,Bloc
|
||||
explorer.col.flow,De → Vers
|
||||
explorer.col.amount,Montant
|
||||
explorer.net,Réseau
|
||||
explorer.mixer,Mixeur
|
||||
|
@@ -930,3 +930,12 @@ funds.err.addr,アドレスが正しくありません。
|
||||
funds.err.self,Poinbase 自身の入金アドレスです。
|
||||
funds.err.known,そのアドレスは請求書に対する支払いのみ受け付けます。
|
||||
funds.err.fee,金額が手数料に満たません。
|
||||
web3.sub.explorer,エクスプローラー
|
||||
explorer.search,アドレス
|
||||
explorer.all,最新
|
||||
explorer.none,送金履歴はありません。
|
||||
explorer.col.block,ブロック
|
||||
explorer.col.flow,送信元 → 送信先
|
||||
explorer.col.amount,数量
|
||||
explorer.net,ネットワーク
|
||||
explorer.mixer,ミキサー
|
||||
|
Can't render this file because it contains an unexpected character in line 507 and column 24.
|
@@ -930,3 +930,12 @@ funds.err.addr,Nieprawidłowy adres.
|
||||
funds.err.self,To adres wpłat Poinbase.
|
||||
funds.err.known,Ten adres przyjmuje tylko płatności za rachunek.
|
||||
funds.err.fee,Kwota nie pokrywa opłaty.
|
||||
web3.sub.explorer,Eksplorator
|
||||
explorer.search,Adres
|
||||
explorer.all,Najnowsze
|
||||
explorer.none,Brak transferów.
|
||||
explorer.col.block,Blok
|
||||
explorer.col.flow,Od → Do
|
||||
explorer.col.amount,Kwota
|
||||
explorer.net,Sieć
|
||||
explorer.mixer,Mikser
|
||||
|
@@ -930,3 +930,12 @@ funds.err.addr,Неверный адрес.
|
||||
funds.err.self,Это адрес пополнения Poinbase.
|
||||
funds.err.known,Этот адрес принимает только оплату по счёту.
|
||||
funds.err.fee,Сумма не покрывает комиссию.
|
||||
web3.sub.explorer,Обозреватель
|
||||
explorer.search,Адрес
|
||||
explorer.all,Последние
|
||||
explorer.none,Переводов нет.
|
||||
explorer.col.block,Блок
|
||||
explorer.col.flow,От → Кому
|
||||
explorer.col.amount,Сумма
|
||||
explorer.net,Сеть
|
||||
explorer.mixer,Миксер
|
||||
|
@@ -929,3 +929,12 @@ funds.err.addr,地址格式或校验位不对。
|
||||
funds.err.self,这是 Poinbase 自己的充值地址。
|
||||
funds.err.known,该地址只接受对应账单的付款。
|
||||
funds.err.fee,数额不够付手续费。
|
||||
web3.sub.explorer,浏览器
|
||||
explorer.search,地址
|
||||
explorer.all,最近
|
||||
explorer.none,没有转账记录。
|
||||
explorer.col.block,区块
|
||||
explorer.col.flow,转出 → 转入
|
||||
explorer.col.amount,数额
|
||||
explorer.net,全网
|
||||
explorer.mixer,混币器
|
||||
|
@@ -87,6 +87,7 @@ namespace Decentralized
|
||||
var w = string.IsNullOrEmpty(addr) ? null : d.Own(addr);
|
||||
if (w == null) { lost += share; continue; }
|
||||
w.Add(Assets.PSC, share);
|
||||
d.Ledger.Add(new LedgerEntry { Block = d.Block, Day = StoreStation.GetDayCounter(), From = "network", To = w.Address, Asset = Assets.PSC, Amount = share });
|
||||
}
|
||||
if (lost > 1e-9)
|
||||
{
|
||||
|
||||
@@ -31,6 +31,8 @@ namespace Decentralized
|
||||
public Stablecoins Stable = new Stablecoins();
|
||||
/// <summary>革命军 CRA 质押池里玩家各钱包的仓位。</summary>
|
||||
public Staking Staking = new Staking();
|
||||
/// <summary>浏览器页的全局转账账本。</summary>
|
||||
public Ledger Ledger = new Ledger();
|
||||
/// <summary>Poinbase 永续合约账本(保证金用 Poinbase 账户的 CRC)。</summary>
|
||||
public PerpBook Perps = new PerpBook();
|
||||
/// <summary>预测市场:盘口、持仓、观测记录(下注资金用 Poinbase 账户的 CRC)。</summary>
|
||||
|
||||
@@ -1279,19 +1279,31 @@ namespace Decentralized
|
||||
if (_web3Tab == Web3Tab.Mixer && !Unlock.MixerUnlocked) _web3Tab = Web3Tab.Trade;
|
||||
if (_web3Tab == Web3Tab.Black && !Unlock.BlackUnlocked) _web3Tab = Web3Tab.Trade;
|
||||
if (_web3Tab == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter())) _web3Tab = Web3Tab.Trade;
|
||||
b.BeginRow(4f);
|
||||
foreach (Web3Tab t in Enum.GetValues(typeof(Web3Tab)))
|
||||
var tabs = Enum.GetValues(typeof(Web3Tab)).Cast<Web3Tab>()
|
||||
.Where(t => !((t == Web3Tab.Mixer && !Unlock.MixerUnlocked) || (t == Web3Tab.Black && !Unlock.BlackUnlocked) || (t == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter()))))
|
||||
.ToList();
|
||||
// 页签按字数估宽,超出一行就折到下一行;连接钱包的控件放在最后一行右端,放不下就单独一行
|
||||
float avail = ScreenW - 40f, used = 0f;
|
||||
Func<string, float> px = txt => txt.Sum(c => c > 0x2E80 ? 18f : 9f) + 28f;
|
||||
bool open = false;
|
||||
foreach (var t in tabs)
|
||||
{
|
||||
if ((t == Web3Tab.Mixer && !Unlock.MixerUnlocked) || (t == Web3Tab.Black && !Unlock.BlackUnlocked) || (t == Web3Tab.Predict && !PredictGame.Available(StoreStation.GetDayCounter()))) continue;
|
||||
var tab = t;
|
||||
string name = L.T("web3.sub." + t.ToString().ToLowerInvariant());
|
||||
float wpx = px(name);
|
||||
if (open && used + wpx > avail) { b.End(); open = false; used = 0f; }
|
||||
if (!open) { b.BeginRow(4f); open = true; }
|
||||
used += wpx + 4f;
|
||||
var tab = t;
|
||||
PledgerSkin.Pick(b, name, tab == _web3Tab, () => { _web3Tab = tab; _message = null; _edit = null; Open(); }, _picked);
|
||||
}
|
||||
float widget = 120f + (Session.Data.Connected ? px(L.T("web3.conn", WalletWindow.NameOf(Session.Data.Hot), ChainAddress.Short(Session.Data.Hot.Address))) : px(L.T("web3.connect")));
|
||||
if (open && used + widget > avail) { b.End(); open = false; }
|
||||
if (!open) b.BeginRow(4f);
|
||||
b.AddFlexibleSpace();
|
||||
ConnectWidget(b);
|
||||
b.End();
|
||||
ConnectPicker(b);
|
||||
if (!Session.Data.Connected) b.AddLabel(L.T("web3.err.noconn"), "term_note");
|
||||
if (!Session.Data.Connected && _web3Tab != Web3Tab.Explorer) 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; }
|
||||
@@ -1299,6 +1311,7 @@ namespace Decentralized
|
||||
if (_web3Tab == Web3Tab.Mining) { ReportPage(b); return; }
|
||||
if (_web3Tab == Web3Tab.Mixer) { MixerPage(b); return; }
|
||||
if (_web3Tab == Web3Tab.Black) { BlackPage(b); return; }
|
||||
if (_web3Tab == Web3Tab.Explorer) { ExplorerPage(b); return; }
|
||||
}
|
||||
Columns(b, l =>
|
||||
{
|
||||
@@ -1352,6 +1365,52 @@ namespace Decentralized
|
||||
b.End();
|
||||
}
|
||||
|
||||
private static string _explorerQuery = "";
|
||||
private static readonly float[] ExplWBase = new[] { 80f, 300f, 160f };
|
||||
private static float[] ExplW => Sc(ExplWBase);
|
||||
private static readonly TextAnchor[] ExplAligns = { TextAnchor.MiddleLeft, TextAnchor.MiddleLeft, TextAnchor.MiddleRight };
|
||||
|
||||
/// <summary>地址的显示名:自己的钱包、Poinbase、混币器、全网矿池、黑市卖家,其余是缩写。</summary>
|
||||
private static string AddressLabel(string a)
|
||||
{
|
||||
var d = Session.Data;
|
||||
if (string.IsNullOrEmpty(a)) return "—";
|
||||
if (a == "network") return L.T("explorer.net");
|
||||
var own = d.Own(a);
|
||||
if (own != null) return WalletWindow.NameOf(own);
|
||||
if (ChainAddress.Same(a, d.Address("poinbase"))) return "Poinbase";
|
||||
if (d.Addresses.TryGetValue("mixer", out var mx) && ChainAddress.Same(a, mx)) return L.T("explorer.mixer");
|
||||
return ChainAddress.Short(a);
|
||||
}
|
||||
|
||||
/// <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; }
|
||||
TableHeader(b, ExplW.Sum() + 8f, ExplW, ExplAligns, L.T("explorer.col.block"), L.T("explorer.col.flow"), L.T("explorer.col.amount"));
|
||||
foreach (var e in rows)
|
||||
TableRow(b, ExplW.Sum() + 8f, ExplW, ExplAligns,
|
||||
new[] { "#" + e.Block, AddressLabel(e.From) + " → " + AddressLabel(e.To), Fmt.Num(e.Amount) + " " + e.Asset });
|
||||
}
|
||||
|
||||
private enum BlackTab { Buy, Sell, Orders }
|
||||
private static BlackTab _blackTab = BlackTab.Buy;
|
||||
private static PayMode _mode = PayMode.Escrow;
|
||||
@@ -2184,7 +2243,7 @@ namespace Decentralized
|
||||
Open();
|
||||
}
|
||||
|
||||
private enum Web3Tab { Trade, Stake, Airdrop, Predict, Ponzi, Mining, Mixer, Black }
|
||||
private enum Web3Tab { Trade, Stake, Airdrop, Predict, Ponzi, Mining, Mixer, Black, Explorer }
|
||||
private static Web3Tab _web3Tab = Web3Tab.Trade;
|
||||
private static string _mixAsset = Assets.PSC;
|
||||
private static int _mixRounds = 2;
|
||||
|
||||
@@ -544,6 +544,7 @@ namespace Decentralized
|
||||
if (tx.To != null)
|
||||
{
|
||||
TxRules.Record(w.History, new TxRecord { Addr = tx.To, Asset = tx.Asset, Amount = amt, Day = day });
|
||||
d.Ledger.Add(new LedgerEntry { Block = tx.Block, Day = day, From = w.Address, To = tx.To, Asset = tx.Asset, Amount = amt, Fee = tx.FeeAsset == tx.Asset ? tx.Fee : 0 });
|
||||
var mine = d.Own(tx.To);
|
||||
if (mine != null) TxRules.Record(mine.History, new TxRecord { Addr = w.Address, Asset = tx.Asset, Amount = amt, Day = day, In = true });
|
||||
// 地址投毒:链上公开的转账被人盯上,过一两天会收到一笔来自相似地址的 0 币转入
|
||||
|
||||
@@ -410,6 +410,21 @@ internal static class Program
|
||||
Check(Stablecoins.PriceOfRatio(Tuning.CraSafeRatio) == 1 && Near(Stablecoins.PriceOfRatio(Tuning.CraCrashRatio), 1 - Tuning.CraDiscountMax, 1e-9), "价格曲线:安全线 1,崩盘线折价 15%");
|
||||
});
|
||||
|
||||
Test("浏览器账本:最新在前、按地址过滤、超过上限丢最旧", () =>
|
||||
{
|
||||
var l = new Ledger();
|
||||
string a = ChainAddress.Make(1), b = ChainAddress.Make(2), c = ChainAddress.Make(3);
|
||||
l.Add(new LedgerEntry { Block = 1, From = a, To = b, Asset = "PSC", Amount = 1 });
|
||||
l.Add(new LedgerEntry { Block = 2, From = b, To = c, Asset = "CRC", Amount = 5, Fee = 1 });
|
||||
l.Add(new LedgerEntry { Block = 3, From = c, To = a, Asset = "PSC", Amount = 0 });
|
||||
Check(l.Entries.Count == 2 && l.Entries[0].Block == 2, "数额为 0 不记;最新的在前");
|
||||
Check(l.Of(b, 10).Count == 2 && l.Of(a, 10).Count == 1 && l.Of(a.ToUpperInvariant(), 10).Count == 1 && l.Of("", 10).Count == 2, "按地址过滤,大小写不敏感,空地址是最近的全部");
|
||||
for (int i = 0; i < Tuning.LedgerMax + 30; i++) l.Add(new LedgerEntry { Block = 100 + i, From = a, To = b, Asset = "PSC", Amount = 1 });
|
||||
Check(l.Entries.Count == Tuning.LedgerMax && l.Entries[0].Block == 100 + Tuning.LedgerMax + 29 && l.Of("", 5).Count == 5, "上限 200 条");
|
||||
var back = Newtonsoft.Json.JsonConvert.DeserializeObject<Ledger>(Newtonsoft.Json.JsonConvert.SerializeObject(l));
|
||||
Check(back.Entries.Count == l.Entries.Count && back.Entries[0].Block == l.Entries[0].Block, "存档往返");
|
||||
});
|
||||
|
||||
Test("CRA 长期行为:200 天里有的局崩盘有的不崩,质押越多崩得越多,崩盘前有预兆", () =>
|
||||
{
|
||||
Func<double, (double crash, double lead)> run = staked =>
|
||||
|
||||
Reference in new issue
Block a user