From e3e5ec2d6e16004446bfca83e1b435863dbc85af Mon Sep 17 00:00:00 2001
From: ZHU MINCHAO <48154958+MinchaoZhu@users.noreply.github.com>
Date: Thu, 5 Nov 2026 07:19:38 +0800
Subject: [PATCH] =?UTF-8?q?=E7=AC=AC=E5=8D=81=E9=98=B6=E6=AE=B5=20stage=20?=
=?UTF-8?q?2=EF=BC=88=E4=B8=89=EF=BC=89=EF=BC=9APoinbase=20=E8=B5=84?=
=?UTF-8?q?=E9=87=91=E9=A1=B5=EF=BC=88=E5=85=A5=E9=87=91=E3=80=81=E5=87=BA?=
=?UTF-8?q?=E9=87=91=E3=80=81=E7=A1=AE=E8=AE=A4=E8=BF=9B=E5=BA=A6=E6=9D=A1?=
=?UTF-8?q?=EF=BC=89=E3=80=81=E9=93=B6=E8=A1=8C=E9=A1=B5=E5=8F=AA=E8=AF=BB?=
=?UTF-8?q?=E3=80=81Poinbase=20=E6=8E=A5=E6=94=B6=E6=89=80=E6=9C=89?=
=?UTF-8?q?=E5=B8=81?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/Decentralized.Domain/Tuning.cs | 2 +
src/Decentralized.Domain/TuningTable.cs | 2 +
src/Decentralized/DealerStock.cs | 1 +
src/Decentralized/Funds.cs | 113 +++++++++++++
.../ModFiles/Localization/de.csv | 19 ++-
.../ModFiles/Localization/en.csv | 19 ++-
.../ModFiles/Localization/es.csv | 19 ++-
.../ModFiles/Localization/fr.csv | 19 ++-
.../ModFiles/Localization/ja.csv | 19 ++-
.../ModFiles/Localization/pl.csv | 19 ++-
.../ModFiles/Localization/ru.csv | 19 ++-
.../ModFiles/Localization/zh.csv | 19 ++-
src/Decentralized/TerminalWindow.cs | 152 ++++++++++++------
src/Decentralized/WalletWindow.cs | 6 +-
14 files changed, 344 insertions(+), 84 deletions(-)
create mode 100644 src/Decentralized/Funds.cs
diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs
index 5124cd9..a38a5bf 100644
--- a/src/Decentralized.Domain/Tuning.cs
+++ b/src/Decentralized.Domain/Tuning.cs
@@ -59,6 +59,8 @@ namespace Decentralized.Domain
public const double CraEmbezzleMin = 0.4, CraEmbezzleMax = 0.8, CraLoot = 0.09;
public const int CraMintDaysMin = 1, CraMintDaysMax = 2;
public const double CraRugPerDepeg = 1;
+ /// Poinbase 出金到钱包的确认动画:基础秒数,加每个确认的秒数(确认数同 PLEDGER 的 TxConfirm*)。
+ public const double FundsBaseSecs = 1.2, FundsConfSecs = 0.6;
public const double CraSpiralWeekly = 0.03;
public const int CraWarnLeadMin = 4, CraWarnLeadMax = 7;
public static readonly double[] CraSpiralPath = { 0.85, 0.30, 0.02 };
diff --git a/src/Decentralized.Domain/TuningTable.cs b/src/Decentralized.Domain/TuningTable.cs
index 18e40fd..baf6abd 100644
--- a/src/Decentralized.Domain/TuningTable.cs
+++ b/src/Decentralized.Domain/TuningTable.cs
@@ -234,6 +234,8 @@ namespace Decentralized.Domain
["FakeSignChance"] = Tuning.FakeSignChance,
["FakeSupportChance"] = Tuning.FakeSupportChance,
["FakeTransferShare"] = Tuning.FakeTransferShare,
+ ["FundsBaseSecs"] = Tuning.FundsBaseSecs,
+ ["FundsConfSecs"] = Tuning.FundsConfSecs,
["HalvingDays"] = Tuning.HalvingDays,
["HalvingPassMax"] = Tuning.HalvingPassMax,
["HalvingPassMin"] = Tuning.HalvingPassMin,
diff --git a/src/Decentralized/DealerStock.cs b/src/Decentralized/DealerStock.cs
index 5158f70..2175ad7 100644
--- a/src/Decentralized/DealerStock.cs
+++ b/src/Decentralized/DealerStock.cs
@@ -183,6 +183,7 @@ namespace Decentralized
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Perp fund 0 (test)", () => { Session.Data.Perps.InsuranceFund = 0; Log.Info("测试:保险基金清零"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "SPARK x0.3 (test)", () => { var st = Session.Data.Stable; st.Spark *= 0.3; Log.Info($"测试:SPARK 价格 {st.Spark:0.00}"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Wallet gift: CRC, CRA, PSC (test)", () => { var w = Session.Data.Hot; w.Add(Assets.CRA, 1000); w.Add(Assets.CRC, 1000); w.Add(Assets.CRT, 3000); w.Add(Assets.PSC, 20); w.Basis += 5000; Log.Info($"测试:{WalletWindow.NameOf(w)} CRA {w.Get(Assets.CRA):0}"); });
+ ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Poinbase gift: CRC, PSC (test)", () => { var pb = Session.Data.Poinbase; pb.Add(Assets.CRC, 5000); pb.Add(Assets.PSC, 30); pb.Basis += 8000; Log.Info($"测试:Poinbase CRC {pb.Get(Assets.CRC):0}"); });
ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "CRA spiral warning (test)", () =>
{
var st = Session.Data.Stable; int day = StoreStation.GetDayCounter();
diff --git a/src/Decentralized/Funds.cs b/src/Decentralized/Funds.cs
new file mode 100644
index 0000000..bc22aa3
--- /dev/null
+++ b/src/Decentralized/Funds.cs
@@ -0,0 +1,113 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Decentralized.Domain;
+using UnityEngine;
+
+namespace Decentralized
+{
+ ///
+ /// Poinbase 的出金:转到钱包地址。托管账户发起,几秒钟的确认动画(模拟交易所触发合约执行)走完才落地;
+ /// 手续费固定中间档,从转出的币里扣。动画走完前币还在账户里,中途存档、退出都不会丢币。
+ /// 地址格式或校验位不对拒绝;自己的钱包到账;别的格式正确的地址按黑洞处理(币就没了)。
+ ///
+ internal static class Funds
+ {
+ internal sealed class Tx
+ {
+ public int Id, Confs;
+ public string Asset, To;
+ public double Amount, Fee;
+ public float Start, Total;
+ public bool Finished;
+ }
+
+ internal static readonly List Active = new List();
+ private static int seq;
+
+ /// 地址检查:返回 null 表示可以转,否则是错误文案的键。
+ internal static string Check(string to)
+ {
+ var d = Session.Data;
+ to = ChainAddress.Normalize(to ?? "");
+ if (!ChainAddress.Valid(to)) return "funds.err.addr";
+ if (ChainAddress.Same(to, d.Address("poinbase"))) return "funds.err.self";
+ if (d.Own(to) == null && WalletWindow.Known(to)) return "funds.err.known";
+ return null;
+ }
+
+ private static bool Congested
+ {
+ get { var d = Session.Data; int day = StoreStation.GetDayCounter(); return day >= d.CongestedFrom && day <= d.CongestedUntil; }
+ }
+
+ /// 开始一笔出金。余额不足或手续费吃掉全部数额返回错误键,成功返回 null。
+ internal static string Start(string asset, double amount, string to)
+ {
+ var d = Session.Data;
+ var err = Check(to);
+ if (err != null) return err;
+ var pb = d.Poinbase;
+ if (pb.Avail(asset) + 1e-9 < amount) return pb.Frozen.ContainsKey(asset) ? "msg.frozen" : "msg.balance";
+ double price = Session.Price(asset);
+ double fee = price > 0 ? TxRules.FeeCredits(FeeTier.Normal, Congested, d.Gas) / price : 0;
+ if (fee >= amount) return "funds.err.fee";
+ double value = amount * price;
+ int confs = TxRules.Confirmations(value);
+ Active.Add(new Tx
+ {
+ Id = ++seq, Asset = asset, To = ChainAddress.Normalize(to), Amount = amount, Fee = fee, Confs = confs,
+ Start = Time.unscaledTime, Total = (float)(Tuning.FundsBaseSecs + confs * Tuning.FundsConfSecs),
+ });
+ WalletDriver.Ensure();
+ return null;
+ }
+
+ internal static float Progress(Tx t) => t.Total <= 0 ? 1 : Mathf.Clamp01((Time.unscaledTime - t.Start) / t.Total);
+
+ internal static int Conf(Tx t) => Math.Max(1, Math.Min(t.Confs, 1 + (int)((Time.unscaledTime - t.Start) / Math.Max(0.01f, t.Total) * t.Confs)));
+
+ internal static string StatusText(Tx t) => L.T("funds.confirm", Conf(t), t.Confs);
+
+ /// 每帧:更新开着的窗口里的进度条,走完的落地。
+ internal static void Tick()
+ {
+ if (Active.Count == 0) return;
+ var m = CustomUIManager.Instance;
+ foreach (var t in Active.ToList())
+ {
+ if (Time.unscaledTime - t.Start >= t.Total) { Finish(t); continue; }
+ m?.GetElement(TerminalWindow.Id, "funds_status_" + t.Id)?.SetText(StatusText(t));
+ m?.GetElement(TerminalWindow.Id, "funds_prog_" + t.Id)?.SetProgress(Progress(t));
+ }
+ }
+
+ private static void Finish(Tx t)
+ {
+ if (t.Finished) return;
+ t.Finished = true;
+ Active.Remove(t);
+ Log.Guard("出金落地", () =>
+ {
+ var d = Session.Data;
+ var pb = d.Poinbase;
+ double amt = Math.Min(t.Amount, pb.Avail(t.Asset));
+ double fee = Math.Min(t.Fee, amt);
+ if (fee > 0) Wallet.Transfer(pb, new Wallet(), t.Asset, fee, Session.Price);
+ double net = amt - fee;
+ var own = d.Own(t.To);
+ Wallet.Transfer(pb, own ?? new Wallet(), t.Asset, net, Session.Price);
+ if (own != null)
+ {
+ TxRules.Record(own.History, new TxRecord { Addr = d.Address("poinbase"), Asset = t.Asset, Amount = net, Day = StoreStation.GetDayCounter(), In = true });
+ WalletWindow.RefreshDevice(own, true);
+ }
+ else Log.Info($"出金转进没人用的地址 {t.To}:{net} {t.Asset}");
+ Session.Save();
+ TerminalWindow.Refresh(own != null
+ ? L.T("funds.done", Fmt.Num(net), t.Asset, WalletWindow.NameOf(own))
+ : L.T("funds.done.void", Fmt.Num(net), t.Asset));
+ });
+ }
+ }
+}
diff --git a/src/Decentralized/ModFiles/Localization/de.csv b/src/Decentralized/ModFiles/Localization/de.csv
index 979eee5..30e343c 100644
--- a/src/Decentralized/ModFiles/Localization/de.csv
+++ b/src/Decentralized/ModFiles/Localization/de.csv
@@ -145,7 +145,6 @@ bank.cash.k,Credits
bank.rent.k,Miete
bank.rent.v,"{0}, fällig in {1} Tg."
bank.crc.k,Poinbase-Konto (CRC)
-bank.ops,Ein-/Auszahlung
poin.account.h,Poinbase-Konto
trade.h,Handel
web3.wallet.h,On-Chain-Wallet
@@ -378,12 +377,9 @@ hw.wait,Auf der Wallet bestätigen.
hw.swap,Tausch {0} {1} → ca. {2} {3}
tab.wallets,Wallets
hw.wallet.n,Wallet {0}
-hw.err.poinbase,Poinbase akzeptiert nur CRC und PSC.
hw.review.own,Eigene Wallet: {0}
hw.review.poinbase,Poinbase-Einzahlungsadresse
poin.deposit,Einzahlungsadresse: {0}
-poin.out,Auszahlen an {0}
-poin.out.done,{0} {1} an {2} ausgezahlt.
wallets.count,Wallets {0}/{1}
wallets.col.name,Name
wallets.col.addr,Adresse
@@ -919,3 +915,18 @@ nightlog.cra.burned,"{0} CRA eingelöst: +{1} CRC, +{2} SPARK ({3})."
nightlog.cra.lost,Der Herausgeber hat nicht reagiert: Der Auftrag ({1}) über {0} CRA ist verloren.
news.crawarn.desc,"Jemand zählt die Reserve der Revolution nach; gestaktes CRA lässt sich womöglich nicht vollständig einlösen. Das Entsperren dauert vier Tage; manche sagen, es sei Zeit zu handeln."
news.cralow.desc,Die Reservedeckung wird knapp; die Revolution hat den CRA-Staking-Zins auf einen Bruchteil gesenkt.
+poin.sub.funds,Guthaben
+funds.receive,Einzahlen
+funds.send,Auszahlen
+funds.bank,Bankkonto
+funds.wallet,Wallet
+funds.to,Zieladresse
+funds.go,Senden
+funds.sending,Sende {0} {1} an {2}
+funds.confirm,Bestätigung {0}/{1}
+funds.done,{0} {1} sind in {2} angekommen.
+funds.done.void,"{0} {1} gesendet; die Adresse hat keinen Besitzer, die Coins sind weg."
+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.
diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv
index 87cf2c1..abb4281 100644
--- a/src/Decentralized/ModFiles/Localization/en.csv
+++ b/src/Decentralized/ModFiles/Localization/en.csv
@@ -145,7 +145,6 @@ bank.cash.k,Credits
bank.rent.k,Rent
bank.rent.v,"{0}, due in {1} days"
bank.crc.k,Poinbase account (CRC)
-bank.ops,Deposit / withdraw
poin.account.h,Poinbase account
trade.h,Trade
web3.wallet.h,On-chain wallet
@@ -378,12 +377,9 @@ hw.wait,Confirm on the wallet.
hw.swap,Swap {0} {1} → about {2} {3}
tab.wallets,Wallets
hw.wallet.n,Wallet {0}
-hw.err.poinbase,Poinbase only accepts CRC and PSC.
hw.review.own,Own wallet: {0}
hw.review.poinbase,Poinbase deposit address
poin.deposit,Deposit address: {0}
-poin.out,Withdraw to {0}
-poin.out.done,{0} {1} withdrawn to {2}.
wallets.count,Wallets {0}/{1}
wallets.col.name,Name
wallets.col.addr,Address
@@ -919,3 +915,18 @@ nightlog.cra.burned,"Redeemed {0} CRA: +{1} CRC, +{2} SPARK ({3})."
nightlog.cra.lost,The issuer did not respond: the {1} order of {0} CRA is lost.
news.crawarn.desc,"Someone is counting the Revolution's reserve, and staked CRA may not be redeemable in full. Unlocking takes four days; some say it is time to move."
news.cralow.desc,"The reserve ratio is running low, and the Revolution has cut the CRA staking rate to a fraction."
+poin.sub.funds,Funds
+funds.receive,Receive
+funds.send,Send
+funds.bank,Bank account
+funds.wallet,Wallet
+funds.to,To address
+funds.go,Send
+funds.sending,Sending {0} {1} to {2}
+funds.confirm,Confirmation {0}/{1}
+funds.done,{0} {1} arrived in {2}.
+funds.done.void,{0} {1} sent; the address has no owner and the coins are gone.
+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.
diff --git a/src/Decentralized/ModFiles/Localization/es.csv b/src/Decentralized/ModFiles/Localization/es.csv
index 734cf25..c27169b 100644
--- a/src/Decentralized/ModFiles/Localization/es.csv
+++ b/src/Decentralized/ModFiles/Localization/es.csv
@@ -145,7 +145,6 @@ bank.cash.k,Créditos
bank.rent.k,Renta
bank.rent.v,"{0}, vence en {1} día(s)"
bank.crc.k,Cuenta de Poinbase (CRC)
-bank.ops,Depósito / retiro
poin.account.h,Cuenta de Poinbase
trade.h,Comercio
web3.wallet.h,Billetera en cadena
@@ -378,12 +377,9 @@ hw.wait,Confirma en la billetera.
hw.swap,Canjear {0} {1} → aprox. {2} {3}
tab.wallets,Billeteras
hw.wallet.n,Billetera {0}
-hw.err.poinbase,Poinbase solo acepta CRC y PSC.
hw.review.own,Billetera propia: {0}
hw.review.poinbase,Dirección de depósito de Poinbase
poin.deposit,Dirección de depósito: {0}
-poin.out,Retirar a {0}
-poin.out.done,Se retiraron {0} {1} a {2}.
wallets.count,Billeteras {0}/{1}
wallets.col.name,Nombre
wallets.col.addr,Dirección
@@ -919,3 +915,18 @@ nightlog.cra.burned,"{0} CRA redimidos: +{1} CRC, +{2} SPARK ({3})."
nightlog.cra.lost,El emisor no respondió: la orden ({1}) de {0} CRA se perdió.
news.crawarn.desc,Alguien está contando la reserva de la Revolución y quizá los CRA apostados no se puedan redimir por completo. Desbloquear tarda cuatro días; algunos dicen que es hora de actuar.
news.cralow.desc,La cobertura de la reserva se agota y la Revolución ha recortado el tipo de apuesta de CRA a una fracción.
+poin.sub.funds,Fondos
+funds.receive,Depósito
+funds.send,Retiro
+funds.bank,Cuenta bancaria
+funds.wallet,Monedero
+funds.to,Dirección de destino
+funds.go,Enviar
+funds.sending,Enviando {0} {1} a {2}
+funds.confirm,Confirmación {0}/{1}
+funds.done,{0} {1} llegaron a {2}.
+funds.done.void,{0} {1} enviados; la dirección no tiene dueño y las monedas se perdieron.
+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.
diff --git a/src/Decentralized/ModFiles/Localization/fr.csv b/src/Decentralized/ModFiles/Localization/fr.csv
index e4f20bb..06365c9 100644
--- a/src/Decentralized/ModFiles/Localization/fr.csv
+++ b/src/Decentralized/ModFiles/Localization/fr.csv
@@ -145,7 +145,6 @@ bank.cash.k,Crédits
bank.rent.k,Loyer
bank.rent.v,"{0}, échéance dans {1} jour(s)"
bank.crc.k,Compte Poinbase (CRC)
-bank.ops,Dépôt / Retrait
poin.account.h,Compte Poinbase
trade.h,Trading
web3.wallet.h,Portefeuille on-chain
@@ -378,12 +377,9 @@ hw.wait,Confirmez sur le portefeuille.
hw.swap,Échanger {0} {1} → environ {2} {3}
tab.wallets,Portefeuilles
hw.wallet.n,Portefeuille {0}
-hw.err.poinbase,Poinbase n'accepte que le CRC et le PSC.
hw.review.own,Portefeuille personnel : {0}
hw.review.poinbase,Adresse de dépôt Poinbase
poin.deposit,Adresse de dépôt : {0}
-poin.out,Retirer vers {0}
-poin.out.done,{0} {1} retirés vers {2}.
wallets.count,Portefeuilles {0}/{1}
wallets.col.name,Nom
wallets.col.addr,Adresse
@@ -919,3 +915,18 @@ nightlog.cra.burned,"{0} CRA rachetés : +{1} CRC, +{2} SPARK ({3})."
nightlog.cra.lost,L'émetteur n'a pas répondu : l'ordre ({1}) de {0} CRA est perdu.
news.crawarn.desc,Quelqu'un fait l'inventaire de la réserve de la Révolution ; les CRA stakés pourraient ne pas être remboursés en totalité. Le déblocage prend quatre jours ; certains disent qu'il est temps d'agir.
news.cralow.desc,La couverture de la réserve s'amenuise ; la Révolution a réduit le taux de staking du CRA à une fraction.
+poin.sub.funds,Fonds
+funds.receive,Dépôt
+funds.send,Retrait
+funds.bank,Compte bancaire
+funds.wallet,Portefeuille
+funds.to,Adresse de destination
+funds.go,Envoyer
+funds.sending,Envoi de {0} {1} vers {2}
+funds.confirm,Confirmation {0}/{1}
+funds.done,{0} {1} sont arrivés dans {2}.
+funds.done.void,"{0} {1} envoyés ; l'adresse n'a pas de propriétaire, les pièces sont perdues."
+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.
diff --git a/src/Decentralized/ModFiles/Localization/ja.csv b/src/Decentralized/ModFiles/Localization/ja.csv
index fc1e199..eef7d69 100644
--- a/src/Decentralized/ModFiles/Localization/ja.csv
+++ b/src/Decentralized/ModFiles/Localization/ja.csv
@@ -145,7 +145,6 @@ bank.cash.k,クレジット
bank.rent.k,家賃
bank.rent.v,{0}、期日:{1}日後
bank.crc.k,Poinbase口座(CRC)
-bank.ops,入出金
poin.account.h,Poinbase口座
trade.h,取引
web3.wallet.h,オンチェーンウォレット
@@ -378,12 +377,9 @@ hw.wait,ウォレットで確認してください。
hw.swap,スワップ {0} {1} → 約 {2} {3}
tab.wallets,ウォレット
hw.wallet.n,ウォレット {0}
-hw.err.poinbase,PoinbaseはCRCおよびPSCの入金のみ受け付けています。
hw.review.own,自身のウォレット:{0}
hw.review.poinbase,Poinbase入金アドレス
poin.deposit,入金アドレス:{0}
-poin.out,{0} へ出金
-poin.out.done,{0} {1} を {2} に出金しました。
wallets.count,ウォレット {0}/{1}
wallets.col.name,名前
wallets.col.addr,アドレス
@@ -919,3 +915,18 @@ nightlog.cra.burned,{0} CRA を償還:+{1} CRC、+{2} SPARK({3})。
nightlog.cra.lost,発行元から応答がありません:{0} CRA の{1}注文は失われました。
news.crawarn.desc,革命軍の準備金を数え直す者がいる。ステークした CRA は全額償還できないかもしれない。解除には四日かかる。動くなら今だという声もある。
news.cralow.desc,準備率が危うくなり、革命軍は CRA のステーキング日利を元の数分の一に引き下げた。
+poin.sub.funds,資金
+funds.receive,入金
+funds.send,出金
+funds.bank,銀行口座
+funds.wallet,ウォレット
+funds.to,送金先アドレス
+funds.go,送信
+funds.sending,{0} {1} を {2} に送金中
+funds.confirm,承認 {0}/{1}
+funds.done,{0} {1} が {2} に着金しました。
+funds.done.void,{0} {1} を送金しました。アドレスに持ち主がおらず、コインは戻りません。
+funds.err.addr,アドレスが正しくありません。
+funds.err.self,Poinbase 自身の入金アドレスです。
+funds.err.known,そのアドレスは請求書に対する支払いのみ受け付けます。
+funds.err.fee,金額が手数料に満たません。
diff --git a/src/Decentralized/ModFiles/Localization/pl.csv b/src/Decentralized/ModFiles/Localization/pl.csv
index e1eba28..e0a550c 100644
--- a/src/Decentralized/ModFiles/Localization/pl.csv
+++ b/src/Decentralized/ModFiles/Localization/pl.csv
@@ -145,7 +145,6 @@ bank.cash.k,Kredyty
bank.rent.k,Czynsz
bank.rent.v,"{0}, termin: {1} d."
bank.crc.k,Konto Poinbase (CRC)
-bank.ops,Wpłata / wypłata
poin.account.h,Konto Poinbase
trade.h,Handel
web3.wallet.h,Portfel on-chain
@@ -378,12 +377,9 @@ hw.wait,Potwierdź na portfelu.
hw.swap,Wymiana {0} {1} → około {2} {3}
tab.wallets,Portfele
hw.wallet.n,Portfel {0}
-hw.err.poinbase,Poinbase akceptuje tylko CRC i PSC.
hw.review.own,Własny portfel: {0}
hw.review.poinbase,Adres wpłaty Poinbase
poin.deposit,Adres wpłaty: {0}
-poin.out,Wypłać na {0}
-poin.out.done,Wypłacono {0} {1} na {2}.
wallets.count,Portfele {0}/{1}
wallets.col.name,Nazwa
wallets.col.addr,Adres
@@ -919,3 +915,18 @@ nightlog.cra.burned,"Umorzono {0} CRA: +{1} CRC, +{2} SPARK ({3})."
nightlog.cra.lost,Emitent nie odpowiedział: zlecenie ({1}) na {0} CRA przepadło.
news.crawarn.desc,"Ktoś liczy rezerwę Rewolucji i zastakowane CRA mogą nie zostać w pełni wypłacone. Odblokowanie trwa cztery dni; niektórzy mówią, że czas działać."
news.cralow.desc,"Pokrycie rezerwy maleje, a Rewolucja obniżyła stopę stakingu CRA do ułamka."
+poin.sub.funds,Środki
+funds.receive,Wpłata
+funds.send,Wypłata
+funds.bank,Konto bankowe
+funds.wallet,Portfel
+funds.to,Adres odbiorcy
+funds.go,Wyślij
+funds.sending,Wysyłanie {0} {1} na {2}
+funds.confirm,Potwierdzenie {0}/{1}
+funds.done,{0} {1} dotarło do {2}.
+funds.done.void,"Wysłano {0} {1}; adres nie ma właściciela, monety przepadły."
+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.
diff --git a/src/Decentralized/ModFiles/Localization/ru.csv b/src/Decentralized/ModFiles/Localization/ru.csv
index c8a339f..0c472ee 100644
--- a/src/Decentralized/ModFiles/Localization/ru.csv
+++ b/src/Decentralized/ModFiles/Localization/ru.csv
@@ -145,7 +145,6 @@ bank.cash.k,Кредиты
bank.rent.k,Аренда
bank.rent.v,"{0}, оплата через {1} дн."
bank.crc.k,Счёт Poinbase (CRC)
-bank.ops,Депозит / вывод
poin.account.h,Счёт Poinbase
trade.h,Торговля
web3.wallet.h,Ончейн-кошелёк
@@ -378,12 +377,9 @@ hw.wait,Подтвердите на кошельке.
hw.swap,Обмен {0} {1} → около {2} {3}
tab.wallets,Кошельки
hw.wallet.n,Кошелёк {0}
-hw.err.poinbase,Poinbase принимает только CRC и PSC.
hw.review.own,Свой кошелёк: {0}
hw.review.poinbase,Адрес депозита Poinbase
poin.deposit,Адрес депозита: {0}
-poin.out,Вывести на {0}
-poin.out.done,{0} {1} выведено на {2}.
wallets.count,Кошельки {0}/{1}
wallets.col.name,Название
wallets.col.addr,Адрес
@@ -919,3 +915,18 @@ nightlog.cra.burned,"Погашено {0} CRA: +{1} CRC, +{2} SPARK ({3})."
nightlog.cra.lost,Эмитент не ответил: заявка ({1}) на {0} CRA потеряна.
news.crawarn.desc,"Кто-то пересчитывает резерв Революции, и CRA в стейкинге могут не выкупить полностью. Разблокировка занимает четыре дня; кое-кто говорит, что пора действовать."
news.cralow.desc,"Покрытие резерва тает, и Революция снизила ставку по стейкингу CRA до малой доли."
+poin.sub.funds,Средства
+funds.receive,Ввод
+funds.send,Вывод
+funds.bank,Банковский счёт
+funds.wallet,Кошелёк
+funds.to,Адрес получателя
+funds.go,Отправить
+funds.sending,Отправка {0} {1} на {2}
+funds.confirm,Подтверждение {0}/{1}
+funds.done,{0} {1} поступили в {2}.
+funds.done.void,"{0} {1} отправлены; у адреса нет владельца, монеты потеряны."
+funds.err.addr,Неверный адрес.
+funds.err.self,Это адрес пополнения Poinbase.
+funds.err.known,Этот адрес принимает только оплату по счёту.
+funds.err.fee,Сумма не покрывает комиссию.
diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv
index ac5fa6e..f83b0df 100644
--- a/src/Decentralized/ModFiles/Localization/zh.csv
+++ b/src/Decentralized/ModFiles/Localization/zh.csv
@@ -144,7 +144,6 @@ bank.cash.k,"信用点"
bank.rent.k,"房租"
bank.rent.v,"{0},{1} 天后支付"
bank.crc.k,"Poinbase 账户(CRC)"
-bank.ops,"存取"
poin.account.h,"Poinbase 账户"
trade.h,"交易"
web3.wallet.h,"链上钱包"
@@ -377,12 +376,9 @@ hw.wait,等待钱包确认。
hw.swap,"兑换 {0} {1} → 约 {2} {3}"
tab.wallets,钱包
hw.wallet.n,"钱包 {0}"
-hw.err.poinbase,Poinbase 仅支持充值 CRC 与 PSC。
hw.review.own,"自己的钱包:{0}"
hw.review.poinbase,Poinbase 充值地址
poin.deposit,"充值地址:{0}"
-poin.out,"提到{0}"
-poin.out.done,"{0} {1} 已提到{2}。"
wallets.count,"钱包 {0}/{1}"
wallets.col.name,名称
wallets.col.addr,地址
@@ -918,3 +914,18 @@ nightlog.cra.burned,销毁 {0} CRA:收回 {1} CRC、{2} SPARK({3})。
nightlog.cra.lost,官方没有回应:{0} CRA 的{1}单丢失。
news.crawarn.desc,有人在清点革命军的储备,质押的 CRA 恐怕兑付不了。解锁需要四天,有人说该动手了。
news.cralow.desc,储备覆盖率告急,革命军把 CRA 质押日息降到了原来的零头。
+poin.sub.funds,资金
+funds.receive,入金
+funds.send,出金
+funds.bank,银行账户
+funds.wallet,钱包
+funds.to,收款地址
+funds.go,发送
+funds.sending,正在转出 {0} {1} 到 {2}
+funds.confirm,确认 {0}/{1}
+funds.done,{0} {1} 已到账:{2}。
+funds.done.void,已转出 {0} {1};该地址没有主人,币回不来了。
+funds.err.addr,地址格式或校验位不对。
+funds.err.self,这是 Poinbase 自己的充值地址。
+funds.err.known,该地址只接受对应账单的付款。
+funds.err.fee,数额不够付手续费。
diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs
index fbf9d4f..17571cb 100644
--- a/src/Decentralized/TerminalWindow.cs
+++ b/src/Decentralized/TerminalWindow.cs
@@ -201,24 +201,10 @@ namespace Decentralized
{
var ps = PlayerStore.Instance;
var w = new[] { 180f, 220f };
- Columns(b, l =>
- {
- l.AddHeader(L.T("entry.bank"));
- TableRow(l, w, L.T("bank.cash.k"), ps.playerCash.ToString());
- TableRow(l, w, L.T("bank.rent.k"), L.T("bank.rent.v", Session.Rent(), ps.GetRentDayCounter()));
- TableRow(l, w, L.T("bank.crc.k"), Fmt.Num(Session.Data.Poinbase.Get(Assets.CRC)));
- }, r =>
- {
- r.AddHeader(L.T("bank.ops"));
- AmountRow(r);
- AmlPreview(r);
- r.AddSpace(6f);
- r.AddButton(L.T("trade.deposit"), Deposit);
- r.Last?.SetPreferredSize(RightW - 30f, 28f * F);
- r.AddSpace(4f);
- r.AddButton(L.T("trade.withdraw"), Withdraw);
- r.Last?.SetPreferredSize(RightW - 30f, 28f * F);
- });
+ 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)));
}
/// 提取前的合规审查提示:仅保留账户污点比例(额度与命中概率按设计不公开)。
@@ -338,11 +324,11 @@ namespace Decentralized
{
any = true;
string asset = kv.Key;
- bool selected = (_page == Page.Poinbase && asset == _poinAsset);
+ bool selected = (_page == Page.Poinbase && _poinTab == PoinTab.Funds && asset == _fundsAsset);
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,
- () => { if (_page == Page.Poinbase) { _poinAsset = asset; Open(); } });
+ () => { if (_page == Page.Poinbase && _poinTab == PoinTab.Funds) { _fundsAsset = asset; if (asset != Assets.CRC) _fundsBank = false; Open(); } });
}
if (!any) b.AddLabel(L.T(w == Session.Data.Poinbase ? "poin.empty" : "wallet.empty"), "hold_empty_dim");
foreach (var kv in w.Frozen.OrderBy(k => k.Key).Where(k => show(k.Key)))
@@ -350,8 +336,8 @@ namespace Decentralized
}
/// Poinbase:合规交易所。左边账户持仓,右边交易;信用点的存取在银行。
- private enum PoinTab { Spot, Perp }
- private static PoinTab _poinTab = PoinTab.Spot;
+ private enum PoinTab { Funds, Spot, Perp }
+ private static PoinTab _poinTab = PoinTab.Funds;
private static void ExchangePage(CustomUIBuilder b, Venue venue)
{
@@ -366,6 +352,7 @@ namespace Decentralized
}
b.End();
if (_poinTab == PoinTab.Perp) { PerpPage(b); return; }
+ if (_poinTab == PoinTab.Funds) { FundsPage(b); return; }
Columns(b, l =>
{
CandleChart.AddPicker(l, "poin", Assets.PSC, Open, _picked);
@@ -373,29 +360,108 @@ namespace Decentralized
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");
- l.AddSeparator();
- l.BeginRow(8f, TextAnchor.MiddleLeft);
- l.AddLabel(L.T("poin.deposit", d.Address("poinbase")), "deposit_dim");
- l.AddButton(L.T("hw.copy"), () => { Clipboard.Copy(d.Address("poinbase")); _message = L.T("hw.copied.addr", d.Address("poinbase")); Open(); });
- l.End();
}, r =>
{
r.AddHeader(L.T("trade.h"));
AmountRow(r);
TradeRows(r, venue);
- r.AddSeparator();
- r.BeginRow(4f);
- foreach (var a in new[] { Assets.CRC, Assets.PSC })
- {
- var asset = a;
- PledgerSkin.Pick(r, asset, asset == _poinAsset, () => { _poinAsset = asset; Open(); }, _picked);
- }
- r.End();
- r.AddButton(L.T("poin.out", WalletWindow.NameOf(d.Hot)), PoinbaseOut);
});
}
- private static string _poinAsset = Assets.CRC;
+ private enum FundsMode { Receive, Send }
+ private static FundsMode _fundsMode = FundsMode.Receive;
+ private static string _fundsAsset = Assets.CRC, _fundsTo = "";
+ private static bool _fundsBank = true;
+
+ /// 资金页:入金和出金,先选币种。CRC 可以走银行账户或钱包,其他币只能走钱包。
+ 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();
+ r.BeginRow(4f);
+ foreach (var c in SwapCoins)
+ {
+ 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();
+ }
private static string _perpProduct = "PSC";
private static bool _perpLong = true;
@@ -1079,18 +1145,6 @@ namespace Decentralized
}
/// Poinbase 提到当前钱包:托管账户直接转,不用签名。污点按比例随币走。
- private static void PoinbaseOut()
- {
- if (!ReadAmount(out double v)) return;
- var d = Session.Data;
- if (d.Poinbase.Avail(_poinAsset) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; }
- Wallet.Transfer(d.Poinbase, d.Hot, _poinAsset, v, Session.Price);
- _message = L.T("poin.out.done", Fmt.Num(v), _poinAsset, WalletWindow.NameOf(d.Hot));
- Session.Save();
- WalletWindow.RefreshDevice(d.Hot);
- Open();
- }
-
/// 钱包页:全部硬件钱包,连接(设为当前)、打开实体钱包。
private static void WalletsPage(CustomUIBuilder b)
{
diff --git a/src/Decentralized/WalletWindow.cs b/src/Decentralized/WalletWindow.cs
index ff44044..7aad9a6 100644
--- a/src/Decentralized/WalletWindow.cs
+++ b/src/Decentralized/WalletWindow.cs
@@ -329,7 +329,7 @@ namespace Decentralized
if (bill.Asset != dev.Asset) return L.T("hw.err.asset", bill.Asset);
if (v + 1e-9 < bill.Amount) return L.T("hw.err.short", Amt(bill.Amount), bill.Asset);
}
- else if (IsPoinbase(to)) { if (dev.Asset != Assets.CRC && dev.Asset != Assets.PSC) return L.T("hw.err.poinbase"); }
+ else if (IsPoinbase(to)) { }
else if (Session.Data.Own(to) == null && Known(to)) return L.T("hw.err.nobill");
return null;
}
@@ -337,7 +337,7 @@ namespace Decentralized
private static Bill Match(string to) => Pending != null && !Pending.Contract && ChainAddress.Same(Pending.Address, to) ? Pending : null;
/// 有过往来的地址(平台、商家、混币器、兑换处):没有账单时对方不收。
- private static bool Known(string to)
+ internal static bool Known(string to)
{
var d = Session.Data;
return d.Addresses.Values.Any(a => ChainAddress.Same(a, to)) || d.Black.Sellers.Any(s => ChainAddress.Same(s.Name, to));
@@ -612,7 +612,7 @@ namespace Decentralized
instance = null;
}
- private void Update() => Log.Guard("钱包动画", WalletWindow.Tick);
+ private void Update() { Log.Guard("钱包动画", WalletWindow.Tick); Log.Guard("出金动画", Funds.Tick); }
}
/// ✓ 键上的按住计时:按满 1.2 秒签名,鼠标挪开或松手就重来。进度用游戏自带的按住圆环显示。