diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e4b2b4..65938a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ ## 未发布 +### 第六阶段:高收益与高风险(进行中) + +- CRA 质押(Web3 页"质押"子页):把 CRA 质押进革命军质押池,日息 0.5%(补贴池告急降到 0.2%),利息并入本金;取出要先申请,4 天后自动回到质押时的钱包,等待期间不计息。每个钱包一个仓位,污点与成本随币存入、按比例随币取出;仓位和解锁队列在质押页列出。质押、申请取出都是合约调用,要在钱包上签名;申请取出不花币,手续费用钱包里价值最高的币付。补贴池按玩家质押量一并消耗。 +- 预兆新闻:螺旋预兆出现的那晚上报纸("质押池传出异动"),补贴池告急降息时再上一条。 +- 平衡代币 SPARK:起始价 10 信用点,日波动 8%,每晚向 10 回归 3%,范围 3–30;革命军袭击上层 +15%,下层动荡 −10%;CRA 死亡螺旋三天里按螺旋前价格的 ×0.8 / ×0.3 / ×0.05 暴跌。可以和 CRA 按价格互相铸销,手续费 0.3%,也是合约调用。钱包的币种里多了 SPARK。 +- 游戏内验证:质押 400 CRA(手续费 1.5 CRA),申请取出 100(第 5 天),铸造 100 CRA 得 9.97 SPARK;第 6 天夜里 100 CRA 回到主钱包,利息 +1.5(300 × 0.5%);触发螺旋预兆后三晚:CRA 0.85 → … → 0.02,SPARK 5.94 → 0.371,质押仓位的数量继续涨(利息)但价格归零。没有在游戏里验证:报纸上的预兆新闻(没有打开报纸)、补贴池自然见底降息。 +- 夜间结算此前在测试存档里没有跑起来:调试面板的"CRA + PSC"只给币,不会解锁 Web3,夜间逻辑要先解锁("Black market +50 rep")。 + ### 第五阶段:硬件钱包(进行中) - 硬件钱包 PLEDGER:终端顶栏"钱包"打开的独立窗口。像素画的圆角石墨机身、黑色屏幕边框、浅色墨水屏,下沿两个物理按键(左 × 拒绝 / 返回,右 ✓ 下一步,签名要按住 1.2 秒,显示游戏自带的按住圆环)。屏幕页:主钱包(地址、余额、合计、污点)、发送、核对、交易、已确认。 diff --git a/src/Decentralized.Domain/Stablecoins.cs b/src/Decentralized.Domain/Stablecoins.cs index 42e6d85..dc1c641 100644 --- a/src/Decentralized.Domain/Stablecoins.cs +++ b/src/Decentralized.Domain/Stablecoins.cs @@ -15,6 +15,8 @@ namespace Decentralized.Domain public double CraSubsidy = Tuning.CraSubsidy0; public int CraWarnDay = -1, CraSpiralDay = -1; public double RevPower = 50; + /// 平衡代币 SPARK 的价格(信用点);SparkPre 是螺旋开始前一刻的价格,螺旋三天按它的倍数走。 + public double Spark = Tuning.SparkStart, SparkPre = -1; public List RevHistory = new List(); public double Price(string asset) @@ -24,6 +26,7 @@ namespace Decentralized.Domain case Assets.CRC: return Crc; case Assets.CRT: return Crt; case Assets.CRA: return Cra; + case Assets.SPARK: return Spark; default: return 1; } } @@ -42,6 +45,8 @@ namespace Decentralized.Domain Crt = rng.Range(Tuning.CrtDepegMin, Tuning.CrtDepegMax); CrtDepegEnd = day + rng.Range(Tuning.CrtDepegDaysMin, Tuning.CrtDepegDaysMax); } + if (ev == MarketEvent.RevRaidUpper) Spark *= Tuning.SparkRaidShock; + if (ev == MarketEvent.LowerUnrest) Spark *= Tuning.SparkUnrestShock; return 1; } @@ -52,6 +57,26 @@ namespace Decentralized.Domain /// 镇压开始时是否冻结玩家的 CRT:按黑市声望,声望越低越容易被黑市"出卖"。 public static bool CrtFrozenByCrackdown(double blackMarketRep, Rng rng) => rng.Chance(CrtFreezeChance(blackMarketRep)); + /// 铸造:用 CRA 按当前价换 SPARK,收手续费。余额不足返回 -1。 + public double Mint(Wallet w, double cra) + { + if (cra <= 0 || Spark <= 0 || w.Avail(Assets.CRA) + 1e-9 < cra) return -1; + double got = cra * Cra / Spark * (1 - Tuning.SparkFee); + w.Add(Assets.CRA, -cra); w.Add(Assets.SPARK, got); + w.RecordBuy(Assets.SPARK, got, cra * Cra); + return got; + } + + /// 销毁:用 SPARK 按当前价换 CRA,收手续费。余额不足返回 -1。 + public double Burn(Wallet w, double spark) + { + if (spark <= 0 || Cra <= 0 || w.Avail(Assets.SPARK) + 1e-9 < spark) return -1; + double got = spark * Spark / Cra * (1 - Tuning.SparkFee); + w.Add(Assets.SPARK, -spark); w.Add(Assets.CRA, got); + w.RecordBuy(Assets.CRA, got, spark * Spark); + return got; + } + public bool CraSubsidyLow => CraSubsidy <= Tuning.CraSubsidy0 * Tuning.CraSubsidyWarn; /// CRA 的预兆:外生螺旋的新闻已出,或补贴池告急(利率下调)。螺旋开始后不再算预兆。 @@ -95,7 +120,18 @@ namespace Decentralized.Domain } } if (CraSpiralDay > 0 && day >= CraSpiralDay) - Cra = Tuning.CraSpiralPath[Math.Min(day - CraSpiralDay, Tuning.CraSpiralPath.Length - 1)]; + { + int k = Math.Min(day - CraSpiralDay, Tuning.CraSpiralPath.Length - 1); + Cra = Tuning.CraSpiralPath[k]; + if (SparkPre < 0) SparkPre = Spark; + Spark = SparkPre * Tuning.SparkSpiralPath[Math.Min(day - CraSpiralDay, Tuning.SparkSpiralPath.Length - 1)]; + } + else + { + Spark *= Math.Exp(rng.Gauss() * Tuning.SparkVol); + Spark += (Tuning.SparkStart - Spark) * Tuning.SparkRevert; + Spark = Math.Max(Tuning.SparkStart * Tuning.SparkMinMult, Math.Min(Tuning.SparkStart * Tuning.SparkMaxMult, Spark)); + } return crcFactor; } } diff --git a/src/Decentralized.Domain/Staking.cs b/src/Decentralized.Domain/Staking.cs new file mode 100644 index 0000000..19f0303 --- /dev/null +++ b/src/Decentralized.Domain/Staking.cs @@ -0,0 +1,90 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Decentralized.Domain +{ + /// 一个钱包在革命军质押池里的仓位:计息的本金、排队等解锁的部分,以及存入时带来的污点与成本。 + public sealed class StakeLot + { + public string Wallet; + public double Amount; + public List> Unlocking = new List>(); + public double Taint, Basis; + + public double Total => Amount + Unlocking.Sum(u => u.Value); + } + + /// 一次到期或利息的结果,供夜间日志使用。 + public sealed class StakeEvent + { + public string Wallet; + public double Interest, Released; + } + + /// + /// 革命军 CRA 质押池:质押的 CRA 每晚按日息计息(利息并入本金),取出要先申请,等 天,期间不计息、 + /// 也不能提前取。到期自动回到质押时的钱包。质押的币不会被冻结,但仓位里仍是 CRA:螺旋来临时到期取回的币只值当时的价格。 + /// 污点与成本随币存入、按比例随币取出。 + /// + public sealed class Staking + { + public List Lots = new List(); + + public StakeLot Of(string wallet) => Lots.Find(l => ChainAddress.Same(l.Wallet, wallet)); + + /// 全部计息中的质押量,补贴池按它算支出。 + public double Earning => Lots.Sum(l => l.Amount); + + /// 质押:从钱包取出 CRA,污点与成本按价值比例随币留在仓位里。余额不足返回 false。 + public bool Stake(Wallet w, double amount, Func price) + { + if (amount <= 0 || w.Avail(Assets.CRA) + 1e-9 < amount) return false; + double total = w.Value(price); + double share = total > 0 ? Math.Min(1, amount * price(Assets.CRA) / total) : 0; + double taint = w.Taint * share, basis = w.Basis * share; + w.Add(Assets.CRA, -amount); w.Taint -= taint; w.Basis -= basis; + var lot = Of(w.Address) ?? AddLot(w.Address); + lot.Amount += amount; lot.Taint += taint; lot.Basis += basis; + return true; + } + + private StakeLot AddLot(string wallet) { var l = new StakeLot { Wallet = wallet }; Lots.Add(l); return l; } + + /// 申请取出:这部分从计息本金里挪到排队区,day + 解锁天数 到期。额度不足返回 false。 + public bool RequestUnstake(string wallet, double amount, int day) + { + var lot = Of(wallet); + if (lot == null || amount <= 0 || lot.Amount + 1e-9 < amount) return false; + lot.Amount -= amount; + lot.Unlocking.Add(new KeyValuePair(day + Tuning.CraUnlockDays, amount)); + return true; + } + + /// 每晚:计息(日息 rate,利息并入本金),到期的部分回到钱包。返回有变化的仓位。 + public List Night(int day, double rate, Func wallet) + { + var evs = new List(); + foreach (var lot in Lots.ToList()) + { + var ev = new StakeEvent { Wallet = lot.Wallet }; + ev.Interest = lot.Amount * rate; + lot.Amount += ev.Interest; + foreach (var u in lot.Unlocking.Where(x => day >= x.Key).ToList()) + { + double total = lot.Total; + double share = total > 0 ? Math.Min(1, u.Value / total) : 0; + double taint = lot.Taint * share, basis = lot.Basis * share; + lot.Taint -= taint; lot.Basis -= basis; + lot.Unlocking.Remove(u); + var w = wallet(lot.Wallet); + w.Add(Assets.CRA, u.Value); w.Taint += taint; w.Basis += basis; + ev.Released += u.Value; + } + if (lot.Total < 1e-9) Lots.Remove(lot); + if (ev.Interest > 0 || ev.Released > 0) evs.Add(ev); + } + return evs; + } + } +} diff --git a/src/Decentralized.Domain/Tuning.cs b/src/Decentralized.Domain/Tuning.cs index 0effe91..4ad92fb 100644 --- a/src/Decentralized.Domain/Tuning.cs +++ b/src/Decentralized.Domain/Tuning.cs @@ -55,6 +55,11 @@ namespace Decentralized.Domain public const double CraSpiralWeekly = 0.02; public const int CraWarnLeadMin = 4, CraWarnLeadMax = 7; public static readonly double[] CraSpiralPath = { 0.85, 0.30, 0.02 }; + /// 平衡代币 SPARK:起始价与长期锚(信用点)、日波动率、每晚向锚回归的比例、价格上下限(锚的倍数);铸销手续费; + /// 革命军袭击上层 / 下层动荡开始时的冲击;CRA 死亡螺旋三天里相对螺旋前价格的倍数。 + public const double SparkStart = 10, SparkVol = 0.08, SparkRevert = 0.03, SparkMinMult = 0.3, SparkMaxMult = 3, SparkFee = 0.003; + public const double SparkRaidShock = 1.15, SparkUnrestShock = 0.90; + public static readonly double[] SparkSpiralPath = { 0.8, 0.3, 0.05 }; // 合规出金与 AML(阈值以当前房租为单位) public const double AmlBigRents = 5, AmlCumRents = 15; diff --git a/src/Decentralized/DealerStock.cs b/src/Decentralized/DealerStock.cs index 0458ed7..a55a476 100644 --- a/src/Decentralized/DealerStock.cs +++ b/src/Decentralized/DealerStock.cs @@ -68,6 +68,13 @@ namespace Decentralized Items.Give(item); Log.Info($"买入测试:卡 #{item.uniqueId} 买前报价 {before},买下后价值 {item.unitValue},真实 {CardValue.True(Items.ToUnit(item))},说法 {(Items.GetClaim(item)?.Encode() ?? "无")}"); })); + ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "CRA + PSC (test)", () => { var w = Session.Data.Hot; w.Add(Assets.CRA, 1000); w.Add(Assets.PSC, 20); w.Basis += 1000; Log.Info($"测试:{WalletWindow.NameOf(w)} CRA {w.Get(Assets.CRA):0}"); }); + ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "CRA spiral warning (test)", () => + { + var st = Session.Data.Stable; int day = StoreStation.GetDayCounter(); + st.CraWarnDay = day; st.CraSpiralDay = day + 3; + Log.Info($"测试:CRA 预兆今天,螺旋第 {st.CraSpiralDay} 天"); + }); ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Give terminal (test)", () => { Items.Give(ModHelper.SpawnItem(Items.Terminal)); }); ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Hardware wallets (test)", () => { Items.Give(Items.NewHwWallet(false)); Items.Give(Items.NewHwWallet(true)); Log.Info("测试:给了一个正品、一个被投毒的硬件钱包"); }); ModHelper.AddDebugFunction(ModInfo.Id, ModInfo.Name, "Chain drama (test)", () => Log.Guard("链上事件测试", () => diff --git a/src/Decentralized/ModFiles/Localization/en.csv b/src/Decentralized/ModFiles/Localization/en.csv index 8d42a1c..5768a6b 100644 --- a/src/Decentralized/ModFiles/Localization/en.csv +++ b/src/Decentralized/ModFiles/Localization/en.csv @@ -268,7 +268,7 @@ black.kind.Contraband.3,Contraband (high) black.kind.Tech,High-tech (markup) nightlog.bm.ready,"Black market: {0} arrived; a courier will come by once you open." item.web3guide.name,Web3 Handbook -item.web3guide.desc,"Bank: depositing turns credits into CRC in your Poinbase account; withdrawing turns CRC back into credits and goes through compliance screening, which looks at the Poinbase account's taint. Trading: buys are paid in the quote coin, sells in the traded coin. Mixer: pick a coin, amount and rounds; each round takes a fee and lowers taint, rounds equal days of waiting, and the coins come back to the same address; the more you deposit, the worse the cleaning. Hardware wallets: on-chain coins sit in PLEDGER hardware wallets, each with its own balances and taint; open and connect them on the terminal's Wallets page. The Web3 page's trading, black market and mixer use the connected wallet, and coin-paying customers and mining rewards go into it too. Payments (black market, mixer, Ponero desk, listing fees) start as an invoice on the terminal; type the payee address on the wallet and hold ✓ to sign. The last two characters of an address are a checksum, so a one-character typo is usually caught; once in a while it happens to be valid and the coins go to an address nobody uses. DEX swaps also need a signature on the wallet. Poinbase is a custodial account and can withdraw straight to the connected wallet; depositing from a wallet means sending to Poinbase's deposit address, which takes only CRC and PSC. More hardware wallets are sold by Dr. Jackson; drag one onto the trading terminal to add it, up to 5. The black market has cheaper ones. A wallet you no longer want can be destroyed on the Wallets page once its coins are moved out. Transfers pay a fee: slow, normal or fast, and gas prices rise and fall over roughly a week; when the network is congested fees rise and slow transactions may be dropped from the mempool. Check the full address before sending. Black market: coin only, no CRC, 30% above normal channels, with extra markup on high-tech goods and GPUs; the coin you pay leaves its share of taint with the seller and the goods carry no stolen tag; GPUs may be fake, inspect them yourself. Orders are cheaper than spot but take days and may be intercepted; merchants sell by type at their own price factor, 5+ items get 5% off and 10+ get 10% off, and a bulk merchant appears at random with the lowest prices but six days of waiting; escrow costs 5%, is released on delivery and refunded if the seller runs; advance release takes another 10% off, but a scammer keeps the money and only sellers with 30+ trades accept; the platform itself can rug, be taken over by enforcement, or a seller can be arrested, after which your orders get traced and crime logged; forum rumors can be warnings. An intercepted parcel sometimes turns into enforcement at your door: signing logs the crime on the spot and seizes the goods, refusing only costs the payment. Selling: on the black market's Sell tab, list contraband, high-tech goods and GPUs from storage at 0.6x to 1.8x the reference price. The higher the multiplier, the slower the sale (below 0.9x about a day, 1.0x about two, 1.8x about fourteen), and the likelier and larger the taint on the payout, which also grows with how much you sold in the last 7 days; at most 10 listings at a time, 5% listing fee, repricing a listing costs 2% and the sale speed follows the new multiplier, 8% commission, payout held 3 days before reaching your wallet; a fake card may be spotted and returned, hurting your rating; if the platform rugs, listed goods and unreleased payouts are gone. Anonymous-coin swap (Web3 trade page, needs black-market standing): Ponero is a privacy coin and swapping in wipes the taint on that value; 6% fee each way with a daily limit; coin swapped out of Ponero has no origin and cash-out screening gets stricter." +item.web3guide.desc,"Bank: depositing turns credits into CRC in your Poinbase account; withdrawing turns CRC back into credits and goes through compliance screening, which looks at the Poinbase account's taint. Trading: buys are paid in the quote coin, sells in the traded coin. Mixer: pick a coin, amount and rounds; each round takes a fee and lowers taint, rounds equal days of waiting, and the coins come back to the same address; the more you deposit, the worse the cleaning. Hardware wallets: on-chain coins sit in PLEDGER hardware wallets, each with its own balances and taint; open and connect them on the terminal's Wallets page. The Web3 page's trading, black market and mixer use the connected wallet, and coin-paying customers and mining rewards go into it too. Payments (black market, mixer, Ponero desk, listing fees) start as an invoice on the terminal; type the payee address on the wallet and hold ✓ to sign. The last two characters of an address are a checksum, so a one-character typo is usually caught; once in a while it happens to be valid and the coins go to an address nobody uses. DEX swaps also need a signature on the wallet. Poinbase is a custodial account and can withdraw straight to the connected wallet; depositing from a wallet means sending to Poinbase's deposit address, which takes only CRC and PSC. More hardware wallets are sold by Dr. Jackson; drag one onto the trading terminal to add it, up to 5. The black market has cheaper ones. A wallet you no longer want can be destroyed on the Wallets page once its coins are moved out. Transfers pay a fee: slow, normal or fast, and gas prices rise and fall over roughly a week; when the network is congested fees rise and slow transactions may be dropped from the mempool. Check the full address before sending. Staking: CRA staked in the Revolution pool earns a daily rate that compounds into the stake; withdrawing means requesting first and waiting four days, with no interest while waiting and no way to take it early. The pool pays from a subsidy fund; when it runs low the rate is cut and word of big holders pulling out may spread. Treat that as a warning: once it collapses CRA is worthless within three days and four days of unlocking is too slow. The balance token SPARK can be minted and burned against CRA at the going price (with a fee); its price swings hard and crashes with the collapse. Staking, minting and burning are contract calls and need a signature on the wallet. Black market: coin only, no CRC, 30% above normal channels, with extra markup on high-tech goods and GPUs; the coin you pay leaves its share of taint with the seller and the goods carry no stolen tag; GPUs may be fake, inspect them yourself. Orders are cheaper than spot but take days and may be intercepted; merchants sell by type at their own price factor, 5+ items get 5% off and 10+ get 10% off, and a bulk merchant appears at random with the lowest prices but six days of waiting; escrow costs 5%, is released on delivery and refunded if the seller runs; advance release takes another 10% off, but a scammer keeps the money and only sellers with 30+ trades accept; the platform itself can rug, be taken over by enforcement, or a seller can be arrested, after which your orders get traced and crime logged; forum rumors can be warnings. An intercepted parcel sometimes turns into enforcement at your door: signing logs the crime on the spot and seizes the goods, refusing only costs the payment. Selling: on the black market's Sell tab, list contraband, high-tech goods and GPUs from storage at 0.6x to 1.8x the reference price. The higher the multiplier, the slower the sale (below 0.9x about a day, 1.0x about two, 1.8x about fourteen), and the likelier and larger the taint on the payout, which also grows with how much you sold in the last 7 days; at most 10 listings at a time, 5% listing fee, repricing a listing costs 2% and the sale speed follows the new multiplier, 8% commission, payout held 3 days before reaching your wallet; a fake card may be spotted and returned, hurting your rating; if the platform rugs, listed goods and unreleased payouts are gone. Anonymous-coin swap (Web3 trade page, needs black-market standing): Ponero is a privacy coin and swapping in wipes the taint on that value; 6% fee each way with a daily limit; coin swapped out of Ponero has no origin and cash-out screening gets stricter." courier.odd1,...Package is here. courier.odd2,"(His hand stays in his pocket.) Sign here." courier.sign,Sign @@ -448,3 +448,32 @@ news.phish.title,Poinbase notice: deposit address migration news.phish.desc,"Poinbase has upgraded its wallet system. The old deposit address will be retired; please send assets to the new address {0}." black.forum.phish,"Someone posted the mixer's new address: {0}, saying the old one is being watched." hw.gas,"Gas ×{0}" +web3.sub.stake,Staking +stake.h,Revolution staking pool +stake.rate,"Daily rate {0}%; withdrawals wait {1} days with no interest" +stake.low,Subsidy running low; rate cut +stake.cra,"CRA price {0} {1}" +stake.none,Nothing staked. +stake.col.wallet,Wallet +stake.col.amount,Staked +stake.col.queue,Unlocking +stake.day, (day {0}) +spark.price,"Balance token SPARK {0} credits" +stake.ops,"{0}" +stake.avail,"CRA {0} Staked {1}" +stake.do,Stake +stake.undo,Request withdrawal +spark.h,Balance token +spark.mint,Mint SPARK +spark.burn,Burn SPARK +stake.sum.in,"Stake {0} CRA" +stake.sum.out,"Request withdrawal of {0} CRA (arrives in {1} days)" +stake.done.in,"Staked {0} CRA." +stake.done.out,"Requested withdrawal of {0} CRA; back in the wallet on day {1}." +nightlog.stakeint,"Staking interest +{0} CRA." +nightlog.unstaked,"{0} CRA withdrawn from staking returned to {1}." +news.crawarn.title,Unrest at the Revolution staking pool +news.crawarn.desc,Big holders are pulling out and the staking subsidy pool is running dry. Unlocking takes four days; some say it is time to move. +news.cralow.title,Revolution cuts staking rate +news.cralow.desc,The subsidy pool is nearly empty, and the Revolution has cut the CRA staking rate to a fraction. +stake.avail2,"SPARK {0}" diff --git a/src/Decentralized/ModFiles/Localization/zh.csv b/src/Decentralized/ModFiles/Localization/zh.csv index b7e1cf2..dbcdb3d 100644 --- a/src/Decentralized/ModFiles/Localization/zh.csv +++ b/src/Decentralized/ModFiles/Localization/zh.csv @@ -267,7 +267,7 @@ black.kind.Contraband.3,违禁品(高) black.kind.Tech,高科技物品(加价) nightlog.bm.ready,"黑市:{0} 到货,开店后会有快递员上门。" item.web3guide.name,Web3 手册 -item.web3guide.desc,"银行:存入把信用点换成 Poinbase 账户里的 CRC;提取把 CRC 换回信用点,要过合规审查,审查看 Poinbase 账户的污点。交易:买入按计价币的数量支付,卖出按所交易币的数量卖出。混币器:选币种、填数量和轮数,每轮扣手续费并降低污点,轮数即等待天数,到期提回同一个地址;存入越多,清洗越差。硬件钱包:链上的币放在 PLEDGER 硬件钱包里,每个钱包的余额和污点各算各的;在终端的钱包页打开、连接。Web3 页的交易、黑市、混币器用已连接的钱包,用币付款的顾客和挖矿收益也进它。付款(黑市、混币器、Ponero 兑换处、上架费)先在终端开账单,再在钱包里手输收款地址、按住 ✓ 签名;地址末尾两位是校验位,输错一位多半会被拦下,偶尔碰巧合法,币就转进了没人用的地址。DEX 兑换也要在钱包上签名。Poinbase 是托管账户,可以直接提到已连接的钱包;从钱包充值要转到 Poinbase 的充值地址,只收 CRC 和 PSC。更多硬件钱包向杰克逊博士购买,拖到交易终端上添加,最多 5 个;黑市上也有更便宜的。不要的钱包可以在钱包页销毁,要先把币转走。转账要付手续费,分慢、普通、快三档,gas 价格大约一周一个起落;网络拥堵时手续费变贵,慢档可能被挤出内存池。转账前核对完整地址。黑市:只收币,不收 CRC,价格比正常渠道贵 30%,高科技物品和显卡另有加价;付出去的币带着按比例的污点留在卖家那边,货不带赃物标签;显卡不保证真假,自己鉴定。订单比现货便宜,要等几天,路上可能被截获;商家按类型卖货,价格系数各不相同,一次订 5 件以上 95 折、10 件以上 9 折,大宗商家随机出现,价格最低但要等六天;托管多付 5% 托管费,收货后放款,卖家跑路时退款;提前放款再打九折,卖家若是骗子钱就没了,只有成交 30 单以上的卖家接受;平台自己也可能跑路,也可能被执法接管,或者卖家落网,之后你的订单会被追查、记罪证;论坛上的传闻可以当预兆。被截获的包裹有时会变成执法人员上门:签收当场记罪证并查封,拒签只损失货款。出售:在黑市的卖货页把储藏区里的违禁品、高科技物品和显卡上架,标价是参考价的 0.6 到 1.8 倍。倍率越高成交越慢(0.9 以下约一天,1.0 约两天,1.8 约十四天),货款带污点的可能性和比例也越高,近 7 天出售的量越大,污点越重;同时最多上架 10 件,上架收 5% 上架费,上架后可以改价(收 2% 改价费,标价按参考价重算,成交的快慢跟着新倍率走),成交扣 8% 抽成,货款托管 3 天后进钱包;卖假卡可能被买家看穿而退货,拉低好评;平台跑路时上架的货和未放款的货款都没了。匿名币兑换(Web3 交易页,要黑市声望):Ponero 是隐私币,兑入时这部分价值的污点被清除;兑入、兑出各收 6% 手续费,每天有额度;从 Ponero 兑出的币来路不明,出金审查会加重。" +item.web3guide.desc,"银行:存入把信用点换成 Poinbase 账户里的 CRC;提取把 CRC 换回信用点,要过合规审查,审查看 Poinbase 账户的污点。交易:买入按计价币的数量支付,卖出按所交易币的数量卖出。混币器:选币种、填数量和轮数,每轮扣手续费并降低污点,轮数即等待天数,到期提回同一个地址;存入越多,清洗越差。硬件钱包:链上的币放在 PLEDGER 硬件钱包里,每个钱包的余额和污点各算各的;在终端的钱包页打开、连接。Web3 页的交易、黑市、混币器用已连接的钱包,用币付款的顾客和挖矿收益也进它。付款(黑市、混币器、Ponero 兑换处、上架费)先在终端开账单,再在钱包里手输收款地址、按住 ✓ 签名;地址末尾两位是校验位,输错一位多半会被拦下,偶尔碰巧合法,币就转进了没人用的地址。DEX 兑换也要在钱包上签名。Poinbase 是托管账户,可以直接提到已连接的钱包;从钱包充值要转到 Poinbase 的充值地址,只收 CRC 和 PSC。更多硬件钱包向杰克逊博士购买,拖到交易终端上添加,最多 5 个;黑市上也有更便宜的。不要的钱包可以在钱包页销毁,要先把币转走。转账要付手续费,分慢、普通、快三档,gas 价格大约一周一个起落;网络拥堵时手续费变贵,慢档可能被挤出内存池。转账前核对完整地址。质押:把 CRA 质押进革命军质押池每天有日息,利息并入本金;取出要先申请,等四天才到账,等待期间没有利息,也不能提前拿。质押池靠补贴池付息,补贴池见底会降息,有时还会传出大户撤出的消息,那是预兆:一旦崩盘,三天内 CRA 就一文不值,四天的解锁期来不及。平衡代币 SPARK 可以和 CRA 按价格互相铸销(收手续费),价格波动很大,崩盘时会跟着暴跌。质押、铸销都是合约调用,要在钱包上签名。黑市:只收币,不收 CRC,价格比正常渠道贵 30%,高科技物品和显卡另有加价;付出去的币带着按比例的污点留在卖家那边,货不带赃物标签;显卡不保证真假,自己鉴定。订单比现货便宜,要等几天,路上可能被截获;商家按类型卖货,价格系数各不相同,一次订 5 件以上 95 折、10 件以上 9 折,大宗商家随机出现,价格最低但要等六天;托管多付 5% 托管费,收货后放款,卖家跑路时退款;提前放款再打九折,卖家若是骗子钱就没了,只有成交 30 单以上的卖家接受;平台自己也可能跑路,也可能被执法接管,或者卖家落网,之后你的订单会被追查、记罪证;论坛上的传闻可以当预兆。被截获的包裹有时会变成执法人员上门:签收当场记罪证并查封,拒签只损失货款。出售:在黑市的卖货页把储藏区里的违禁品、高科技物品和显卡上架,标价是参考价的 0.6 到 1.8 倍。倍率越高成交越慢(0.9 以下约一天,1.0 约两天,1.8 约十四天),货款带污点的可能性和比例也越高,近 7 天出售的量越大,污点越重;同时最多上架 10 件,上架收 5% 上架费,上架后可以改价(收 2% 改价费,标价按参考价重算,成交的快慢跟着新倍率走),成交扣 8% 抽成,货款托管 3 天后进钱包;卖假卡可能被买家看穿而退货,拉低好评;平台跑路时上架的货和未放款的货款都没了。匿名币兑换(Web3 交易页,要黑市声望):Ponero 是隐私币,兑入时这部分价值的污点被清除;兑入、兑出各收 6% 手续费,每天有额度;从 Ponero 兑出的币来路不明,出金审查会加重。" courier.odd1,……东西到了。 courier.odd2,"(他的手一直放在口袋里)在这儿签个字。" courier.sign,签收 @@ -447,3 +447,32 @@ news.phish.title,Poinbase 公告:充值地址迁移 news.phish.desc,"Poinbase 完成钱包系统升级,旧充值地址即将停用,请将资产转入新地址 {0}。" black.forum.phish,"有人贴了混币器的新地址:{0},说旧的被盯上了。" hw.gas,"Gas ×{0}" +web3.sub.stake,质押 +stake.h,革命军质押池 +stake.rate,"日息 {0}%,取出要等 {1} 天,等待期间没有利息" +stake.low,补贴池告急,日息已下调 +stake.cra,"CRA 价格 {0} {1}" +stake.none,还没有质押。 +stake.col.wallet,钱包 +stake.col.amount,质押 +stake.col.queue,等待取出 +stake.day,(第 {0} 天) +spark.price,"平衡代币 SPARK {0} 信用点" +stake.ops,"{0}" +stake.avail,"CRA {0} 已质押 {1}" +stake.do,质押 +stake.undo,申请取出 +spark.h,平衡代币 +spark.mint,铸造 SPARK +spark.burn,销毁 SPARK +stake.sum.in,"质押 {0} CRA" +stake.sum.out,"申请取出 {0} CRA({1} 天后到账)" +stake.done.in,"已质押 {0} CRA。" +stake.done.out,"已申请取出 {0} CRA,第 {1} 天回到钱包。" +nightlog.stakeint,"质押利息 +{0} CRA。" +nightlog.unstaked,"质押取出的 {0} CRA 回到了{1}。" +news.crawarn.title,革命军质押池传出异动 +news.crawarn.desc,有大户在大量撤出,质押补贴池告急。解锁需要四天,有人说该动手了。 +news.cralow.title,革命军下调质押日息 +news.cralow.desc,补贴池余额见底,革命军把 CRA 质押日息降到了原来的零头。 +stake.avail2,"SPARK {0}" diff --git a/src/Decentralized/Night.cs b/src/Decentralized/Night.cs index c4f8527..12e3033 100644 --- a/src/Decentralized/Night.cs +++ b/src/Decentralized/Night.cs @@ -156,7 +156,10 @@ namespace Decentralized d.Network.Night(day, d.Market.Price, hash); if (d.Network.Reward < rewardBefore) d.Market.OnHalving(d.Rng); if (d.Network.Reward < rewardBefore) News.Show("pd_news_halving_" + day, L.T("news.halving.title"), L.T("news.halving.desc"), 2); - double crcFactor = d.Stable.Night(day, d.Rng, 0); + var stakeEvents = d.Staking.Night(day, d.Stable.CraRate, d.WalletAt); + double crcFactor = d.Stable.Night(day, d.Rng, d.Staking.Earning); + if (d.Stable.CraWarnDay == day) News.Show("pd_news_crawarn_" + day, L.T("news.crawarn.title"), L.T("news.crawarn.desc"), 4); + if (d.Stable.CraSubsidyLow && !d.CraLowNews) { d.CraLowNews = true; News.Show("pd_news_cralow_" + day, L.T("news.cralow.title"), L.T("news.cralow.desc"), 3); } if (crcFactor < 1) foreach (var w in d.Wallets.Append(d.Poinbase)) if (w.Get(Assets.CRC) > 0) w.Balances[Assets.CRC] = w.Get(Assets.CRC) * crcFactor; @@ -189,6 +192,13 @@ namespace Decentralized ps.AddNightLog(L.T("nightlog.drained", WalletWindow.NameOf(w), Fmt.Credits(v)), "cc7755"); } ChainNight(d, day); + foreach (var e in stakeEvents) + { + var sw = d.Own(e.Wallet); + if (e.Released > 0) ps.AddNightLog(L.T("nightlog.unstaked", Fmt.Num(e.Released), sw != null ? WalletWindow.NameOf(sw) : e.Wallet)); + } + double interest = stakeEvents.Sum(e => e.Interest); + if (interest > 0) ps.AddNightLog(L.T("nightlog.stakeint", Fmt.Num(interest))); foreach (var name in report.Burnt) ps.AddNightLog(L.T("nightlog.burnt", name), "cc7755"); int unpowered = report.Rigs - report.PoweredRigs; if (unpowered > 0 && units.Count + unpowered > 0) ps.AddNightLog(L.T("nightlog.unpowered", unpowered), "cc7755"); diff --git a/src/Decentralized/Session.cs b/src/Decentralized/Session.cs index 1f81086..5970676 100644 --- a/src/Decentralized/Session.cs +++ b/src/Decentralized/Session.cs @@ -29,6 +29,10 @@ namespace Decentralized public Rng Rng = new Rng((ulong)DateTime.UtcNow.Ticks); public Market Market; public Stablecoins Stable = new Stablecoins(); + /// 革命军 CRA 质押池里玩家各钱包的仓位。 + public Staking Staking = new Staking(); + /// 质押日息下调的新闻已经发过。 + public bool CraLowNews; public Network Network = new Network(); /// 链上钱包(硬件钱包,第一个是主钱包),各自的余额、污点、成本、冻结独立。 public List Wallets = new List(); @@ -125,6 +129,7 @@ namespace Decentralized if (asset == Assets.PSC) return Data.Market.Price; var spec = Assets.Get(asset); if (asset == Assets.Ponero) return BlackMarket.PoneroPrice(Data.Market.Price); + if (asset == Assets.SPARK) return Data.Stable.Spark; if (spec != null && spec.Stable) return Data.Stable.Price(asset); return 0; } diff --git a/src/Decentralized/TerminalWindow.cs b/src/Decentralized/TerminalWindow.cs index 52eb007..1748b20 100644 --- a/src/Decentralized/TerminalWindow.cs +++ b/src/Decentralized/TerminalWindow.cs @@ -381,6 +381,7 @@ namespace Decentralized b.AddButton(tab == _web3Tab ? $"[{name}]" : name, () => { _web3Tab = tab; _message = null; _edit = null; Open(); }); } b.End(); + if (_web3Tab == Web3Tab.Stake) { StakePage(b); return; } if (_web3Tab == Web3Tab.Mining) { ReportPage(b); return; } if (_web3Tab == Web3Tab.Mixer) { MixerPage(b); return; } if (_web3Tab == Web3Tab.Black) { BlackPage(b); return; } @@ -844,7 +845,7 @@ namespace Decentralized Open(); } - private enum Web3Tab { Trade, Mining, Mixer, Black } + private enum Web3Tab { Trade, Stake, Mining, Mixer, Black } private static Web3Tab _web3Tab = Web3Tab.Trade; private static string _mixAsset = Assets.PSC; private static int _mixRounds = 2; @@ -915,6 +916,103 @@ namespace Decentralized Open(); } + private static readonly float[] StakeW = { 110f, 90f, 200f }; + + /// 质押页:革命军 CRA 质押池(日息、解锁排队)和平衡代币 SPARK 的铸销。都是合约调用,要在钱包上签名。 + private static void StakePage(CustomUIBuilder b) + { + var d = Session.Data; + var st = d.Stable; + var w = d.Hot; + int day = StoreStation.GetDayCounter(); + Columns(b, l => + { + l.AddHeader(L.T("stake.h")); + l.AddLabel(L.T("stake.rate", (st.CraRate * 100).ToString("0.0", CultureInfo.InvariantCulture), Tuning.CraUnlockDays) + (st.CraSubsidyLow ? " " + L.T("stake.low") : "")); + l.AddLabel(L.T("stake.cra", Fmt.Num(st.Cra), StableStatus(st, Assets.CRA, day))); + l.AddSeparator(); + if (d.Staking.Lots.Count == 0) l.AddLabel(L.T("stake.none")); + else + { + TableHeader(l, StakeW, L.T("stake.col.wallet"), L.T("stake.col.amount"), L.T("stake.col.queue")); + foreach (var lot in d.Staking.Lots) + { + var ow = d.Own(lot.Wallet); + string queue = lot.Unlocking.Count == 0 ? "—" : string.Join(" ", lot.Unlocking.Select(u => Fmt.Num(u.Value) + L.T("stake.day", u.Key))); + TableRow(l, StakeW, ow != null ? WalletWindow.NameOf(ow) : lot.Wallet, Fmt.Num(lot.Amount), queue); + } + } + l.AddSeparator(); + l.AddLabel(L.T("spark.price", Fmt.Num(st.Spark))); + }, r => + { + r.AddHeader(L.T("stake.ops", WalletWindow.NameOf(w))); + r.AddLabel(L.T("stake.avail", Fmt.Num(w.Avail(Assets.CRA)), Fmt.Num(d.Staking.Of(w.Address)?.Amount ?? 0))); + r.AddLabel(L.T("stake.avail2", Fmt.Num(w.Avail(Assets.SPARK)))); + AmountRow(r); + r.BeginRow(); + r.AddButton(L.T("stake.do"), () => StakeCall(true)); + r.AddButton(L.T("stake.undo"), () => StakeCall(false)); + r.End(); + r.AddSeparator(); + r.AddHeader(L.T("spark.h")); + r.BeginRow(); + r.AddButton(L.T("spark.mint"), () => SparkCall(true)); + r.AddButton(L.T("spark.burn"), () => SparkCall(false)); + r.End(); + }); + } + + /// 手续费用钱包里价值最高的那种币付。 + private static string BestFeeAsset(Wallet w) => + w.Balances.Where(k => k.Value > 1e-9 && Session.Price(k.Key) > 0).OrderByDescending(k => k.Value * Session.Price(k.Key)).Select(k => k.Key).FirstOrDefault() ?? Assets.PSC; + + private static void StakeCall(bool into) + { + if (!ReadAmount(out double v)) return; + var d = Session.Data; + var w = d.Hot; + if (into ? w.Avail(Assets.CRA) + 1e-9 < v : (d.Staking.Of(w.Address)?.Amount ?? 0) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; } + _message = L.T("hw.wait"); + Open(); + WalletWindow.OpenForBill(new WalletWindow.Bill + { + Contract = true, Asset = Assets.CRA, Amount = v, Spend = into ? v : 0, FeeAsset = into ? null : BestFeeAsset(w), + Summary = L.T(into ? "stake.sum.in" : "stake.sum.out", Fmt.Num(v), Tuning.CraUnlockDays), + Execute = pw => + { + int day = StoreStation.GetDayCounter(); + bool ok = into ? d.Staking.Stake(pw, v, Session.Price) : d.Staking.RequestUnstake(pw.Address, v, day); + _message = ok ? L.T(into ? "stake.done.in" : "stake.done.out", Fmt.Num(v), day + Tuning.CraUnlockDays) : L.T("msg.balance"); + Session.Save(); + Open(); + }, + }); + } + + private static void SparkCall(bool mint) + { + if (!ReadAmount(out double v)) return; + var d = Session.Data; + var w = d.Hot; + string pay = mint ? Assets.CRA : Assets.SPARK, get = mint ? Assets.SPARK : Assets.CRA; + if (w.Avail(pay) + 1e-9 < v) { _message = L.T("msg.balance"); Open(); return; } + double quote = mint ? v * d.Stable.Cra / d.Stable.Spark * (1 - Tuning.SparkFee) : v * d.Stable.Spark / d.Stable.Cra * (1 - Tuning.SparkFee); + _message = L.T("hw.wait"); + Open(); + WalletWindow.OpenForBill(new WalletWindow.Bill + { + Contract = true, Asset = pay, Amount = v, Summary = L.T("hw.swap", Fmt.Num(v), pay, Fmt.Num(quote), get), + Execute = pw => + { + double got = mint ? d.Stable.Mint(pw, v) : d.Stable.Burn(pw, v); + _message = got < 0 ? L.T("msg.balance") : L.T("msg.trade", Fmt.Num(v), pay, Fmt.Num(got), get); + Session.Save(); + Open(); + }, + }); + } + private static bool frozenPay(Wallet w, string asset) => w.Frozen.ContainsKey(asset); private static string AvgText(Wallet w, string asset) => w.AvgBuy(asset) is double a ? Fmt.Num(a) : L.T("avg.none"); diff --git a/src/Decentralized/WalletWindow.cs b/src/Decentralized/WalletWindow.cs index 21b85bc..8339191 100644 --- a/src/Decentralized/WalletWindow.cs +++ b/src/Decentralized/WalletWindow.cs @@ -29,6 +29,9 @@ namespace Decentralized public string Address, Asset, Summary; public double Amount; public bool Contract; + /// 合约调用时钱包里实际要花掉的数量(默认等于 Amount;取出质押这类不花币的调用为 0)和付手续费的币(默认用 Asset)。 + public double? Spend; + public string FeeAsset; public Action Execute; } @@ -65,7 +68,7 @@ namespace Decentralized } private static readonly Dictionary Devices = new Dictionary(); - private static readonly string[] SendAssets = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA, Assets.Ponero }; + private static readonly string[] SendAssets = { Assets.PSC, Assets.CRC, Assets.CRT, Assets.CRA, Assets.SPARK, Assets.Ponero }; /// 网络拥堵:夜间结算随机开始,持续一两天,期间手续费变贵、慢档和普通档确认变慢。 public static bool Congested @@ -483,7 +486,7 @@ namespace Decentralized private static float Progress(Device dev) => Total(dev) <= 0 ? 0 : Mathf.Clamp01(Elapsed(dev) / Total(dev)); /// 扣手续费的币:转账用转出的币,合约调用用账单上的币。 - private static string FeeAssetOf(Device dev) => Pending != null && Pending.Contract ? (Pending.Asset ?? Assets.PSC) : dev.Asset; + private static string FeeAssetOf(Device dev) => Pending != null && Pending.Contract ? (Pending.FeeAsset ?? Pending.Asset ?? Assets.PSC) : dev.Asset; /// 这笔交易的价值(信用点),决定确认数。 private static double ValueOf(Device dev) @@ -549,8 +552,9 @@ namespace Decentralized d.Block += 1 + d.Rng.Range(0, 3); if (Pending != null && Pending.Contract) { - string a = Pending.Asset ?? Assets.PSC; - if (dev.Wallet.Avail(a) + 1e-9 < Pending.Amount + FeeIn(a, dev.Tier)) { dev.Note = L.T("hw.err.fee"); Open(dev); return; } + string a = Pending.Asset ?? Assets.PSC, fa = FeeAssetOf(dev); + double spend = Pending.Spend ?? Pending.Amount; + if (a == fa ? dev.Wallet.Avail(a) + 1e-9 < spend + FeeIn(fa, dev.Tier) : dev.Wallet.Avail(a) + 1e-9 < spend || dev.Wallet.Avail(fa) + 1e-9 < FeeIn(fa, dev.Tier)) { dev.Note = L.T("hw.err.fee"); Open(dev); return; } dev.Tx = new Tx { Bill = Pending, Block = d.Block, Start = Time.unscaledTime }; } else diff --git a/tests/Decentralized.Domain.Tests/Program.cs b/tests/Decentralized.Domain.Tests/Program.cs index b15853c..8f4220b 100644 --- a/tests/Decentralized.Domain.Tests/Program.cs +++ b/tests/Decentralized.Domain.Tests/Program.cs @@ -808,6 +808,47 @@ internal static class Program Check(!ChainAddress.Valid("0x12345") && !ChainAddress.Valid("0xzzzzzzzz") && !ChainAddress.WellFormed("12345678ab"), "格式不对的地址不合法"); }); + Test("CRA 质押池:计息、解锁期、污点随币、螺旋", () => + { + Func price = a => a == Assets.PSC ? 100 : 1; + var w = new Wallet { Address = "0xa" }; w.Add(Assets.CRA, 1000); w.Add(Assets.PSC, 10); w.Taint = 400; w.Basis = 600; + var st = new Staking(); + Check(!st.Stake(w, 2000, price), "余额不足不能质押"); + Check(st.Stake(w, 500, price) && Math.Abs(w.Get(Assets.CRA) - 500) < 1e-9, "质押 500,钱包剩 500"); + double lotTaint = st.Of("0xa").Taint; + Check(Math.Abs(lotTaint - 400 * 500.0 / 2000) < 1e-6 && Math.Abs(w.Taint - (400 - lotTaint)) < 1e-6, $"污点按价值比例进仓位:{lotTaint}"); + var e1 = st.Night(1, Tuning.CraRate, _ => w); + Check(Math.Abs(st.Of("0xa").Amount - 500 * (1 + Tuning.CraRate)) < 1e-9 && e1.Count == 1, "每晚计息并入本金"); + Check(!st.RequestUnstake("0xa", 9999, 1) && st.RequestUnstake("0xa", 200, 1), "申请取出 200"); + double a0 = st.Of("0xa").Amount; + st.Night(2, Tuning.CraRate, _ => w); + Check(Math.Abs(st.Of("0xa").Amount - a0 * (1 + Tuning.CraRate)) < 1e-9, "排队的部分不计息"); + Check(w.Get(Assets.CRA) < 501, "解锁期没到不能取"); + for (int d = 3; d <= 1 + Tuning.CraUnlockDays; d++) st.Night(d, Tuning.CraRate, _ => w); + Check(Math.Abs(w.Get(Assets.CRA) - 700) < 1e-6, $"到期自动回钱包 200:{w.Get(Assets.CRA):0.##}"); + Check(w.Taint > 400 - lotTaint + 1, "污点按比例回来"); + // 螺旋:质押的 CRA 取回后只值当时的价格 + var s = new Stablecoins(); s.CraSpiralDay = 5; + for (int d = 5; d <= 7; d++) s.Night(d, new Rng(1), 0); + Check(s.Cra < 0.05 && s.Spark < Tuning.SparkStart * 0.1, $"螺旋后 CRA {s.Cra},SPARK {s.Spark:0.00}"); + }); + + Test("SPARK:随机游走有界,铸销来回只损手续费,事件冲击", () => + { + double lo = 99, hi = 0; + for (ulong seed = 1; seed <= 20; seed++) + { + var s = new Stablecoins(); var rng = new Rng(seed); + for (int d = 1; d <= 150 && s.CraWarnDay < 0; d++) { s.Night(d, rng, 0); lo = Math.Min(lo, s.Spark); hi = Math.Max(hi, s.Spark); } // 螺旋前 + } + Check(lo >= Tuning.SparkStart * Tuning.SparkMinMult - 1e-9 && hi <= Tuning.SparkStart * Tuning.SparkMaxMult + 1e-9 && hi / lo > 1.5, $"螺旋前的范围 {lo:0.0}–{hi:0.0}"); + var s2 = new Stablecoins(); var w = new Wallet { Address = "w" }; w.Add(Assets.CRA, 100); + double sp = s2.Mint(w, 100), back = s2.Burn(w, sp); + Check(Math.Abs(back - 100 * Math.Pow(1 - Tuning.SparkFee, 2)) < 1e-6 && s2.Mint(w, 1e6) < 0, $"来回 100 → {back:0.00},余额不足返回 -1"); + double before = s2.Spark; s2.OnEventStart(MarketEvent.RevRaidUpper, 1, new Rng(1)); + Check(Math.Abs(s2.Spark - before * Tuning.SparkRaidShock) < 1e-9, "袭击上层 +15%"); + }); + Test("相似地址、手续费、确认数", () => { var rng = new Rng(21);