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 秒签名,鼠标挪开或松手就重来。进度用游戏自带的按住圆环显示。