using System; using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; namespace Decentralized.Domain { /// /// 一个链上地址:各币种余额,每种币里有几枚是脏的(),加上整个地址的成本(信用点)。 /// 污点随具体的币进出:收到带污点的币时记下其中几枚脏;花掉、转出这种币时按它的脏比例带走;兑换成别的币时脏比例跟过去。 /// 成本按转出的价值比例随币走。 /// public sealed class Wallet { public string Address; public Dictionary Balances = new Dictionary(); /// 每种币里脏的枚数(0 ≤ 脏 ≤ 余额)。 public Dictionary Dirty = new Dictionary(); /// 旧存档(数据版本 5 之前)整个钱包的污点额(信用点):只在读档迁移时读一次,之后为 0。 [JsonProperty("Taint")] public double LegacyTaint; /// 成本(信用点):出金时只对超出成本的部分征税。 public double Basis; /// 钱包编号("钱包 n"),添加时定下,销毁别的钱包也不变。 public string Name; /// 玩家起的名字;空时用默认名。 public string Label; /// 转账记录(最新的在前),PLEDGER 发送页的"最近"列表从这里取。 public List History = new List(); /// 硬件钱包被动过手脚(供应链投毒):别人手里有助记词。玩家看不到。 public bool Poisoned; /// 被投毒的钱包被转空前的期望天数:门槛处 7 天,按价值线性降到满额处 1 天;不到门槛不会被转。 public static double DrainDays(double value) { double t = Math.Min(1, (value - Tuning.WalletDrainMin) / (Tuning.WalletDrainFull - Tuning.WalletDrainMin)); return Tuning.WalletDrainSlowDays - (Tuning.WalletDrainSlowDays - 1) * t; } /// 被投毒的钱包今晚被转空的概率 = 1 / 期望天数(每晚独立掷一次)。 public static double DrainChance(double value) => value < Tuning.WalletDrainMin ? 0 : 1 / DrainDays(value); /// 转空:全部余额(含冻结的)连同污点、成本一起转走,返回转走的价值(信用点)。 public double Drain(Func price) { double v = Value(price); Balances.Clear(); Frozen.Clear(); FrozenUntil.Clear(); BuyQty.Clear(); BuyCost.Clear(); Dirty.Clear(); Basis = 0; return v; } /// 买入记录:每种币买入的数量与花费(信用点等值),用来显示买入均价。卖出或转出时按比例减少。 public Dictionary BuyQty = new Dictionary(), BuyCost = new Dictionary(); public void RecordBuy(string asset, double qty, double costCredits) { if (qty <= 0) return; BuyQty[asset] = (BuyQty.TryGetValue(asset, out var q) ? q : 0) + qty; BuyCost[asset] = (BuyCost.TryGetValue(asset, out var c) ? c : 0) + Math.Max(0, costCredits); } /// 买入均价(信用点);没有买入记录(例如全是挖出来的)返回 null。 public double? AvgBuy(string asset) => BuyQty.TryGetValue(asset, out var q) && q > 1e-9 ? BuyCost[asset] / q : (double?)null; /// 被发行方冻结的数量(只有可冻结的币:CRT 在镇压期间、CRC),和解冻的日子。冻结的币仍计入余额与估值,但不能花、不能转。 public Dictionary Frozen = new Dictionary(); public Dictionary FrozenUntil = new Dictionary(); /// 可动用的余额(扣掉冻结部分)。 public double Avail(string asset) => Math.Max(0, Get(asset) - (Frozen.TryGetValue(asset, out var f) ? f : 0)); /// 冻结该币当前余额的 share 比例,直到 untilDay(含)。已冻结的不重复冻。返回冻结的数量。 public double Freeze(string asset, double share, int untilDay) { double amount = Avail(asset) * Math.Max(0, Math.Min(1, share)); if (amount <= 1e-9) return 0; Frozen[asset] = (Frozen.TryGetValue(asset, out var f) ? f : 0) + amount; FrozenUntil[asset] = Math.Max(untilDay, FrozenUntil.TryGetValue(asset, out var u) ? u : 0); return amount; } /// 解冻到期的币,返回 (币种, 数量) 列表。 public List> Thaw(int day) { var list = new List>(); foreach (var a in new List(FrozenUntil.Keys)) if (day >= FrozenUntil[a]) { list.Add(new KeyValuePair(a, Frozen.TryGetValue(a, out var f) ? f : 0)); Frozen.Remove(a); FrozenUntil.Remove(a); } return list; } public double Get(string asset) => Balances.TryGetValue(asset, out var v) ? v : 0; public void Add(string asset, double amount) { if (amount == 0) return; if (amount < 0 && BuyQty.TryGetValue(asset, out var bq)) { double before = Get(asset), keep = before > 1e-12 ? Math.Max(0, 1 + amount / before) : 0; BuyQty[asset] = bq * keep; BuyCost[asset] *= keep; if (BuyQty[asset] < 1e-9) { BuyQty.Remove(asset); BuyCost.Remove(asset); } } double had = Get(asset), v = had + amount; if (v < -1e-9) throw new InvalidOperationException($"{Address}: {asset} 余额不足"); if (Math.Abs(v) < 1e-12) Balances.Remove(asset); else Balances[asset] = v; // 拿走的币按这种币的脏比例带走脏枚数 if (amount < 0 && Dirty.TryGetValue(asset, out var d)) SetDirty(asset, had > 1e-12 ? d * Math.Max(0, v) / had : 0); } /// 这种币里脏的枚数(不超过余额)。 public double DirtyOf(string asset) => Dirty.TryGetValue(asset, out var d) ? Math.Max(0, Math.Min(d, Get(asset))) : 0; /// 这种币的脏比例(0–1)。 public double DirtyRatio(string asset) { double g = Get(asset); return g > 1e-12 ? DirtyOf(asset) / g : 0; } /// 这种币里干净的、能动用的枚数(不超过可动用余额)。钱包往 Poinbase 最多只能充这么多。 public double CleanAvail(string asset) => Math.Max(0, Math.Min(Avail(asset), Get(asset) - DirtyOf(asset))); /// 按现价算的脏币总价值(信用点):钱包总表、日志用。 public double Taint(Func price) => Balances.Keys.Sum(a => DirtyOf(a) * price(a)); private void SetDirty(string asset, double d) { d = Math.Max(0, Math.Min(d, Get(asset))); if (d < 1e-9) Dirty.Remove(asset); else Dirty[asset] = d; } /// 收到 amount 枚币,其中 dirty 枚是脏的(不超过 amount)。 public void AddDirty(string asset, double amount, double dirty) { if (amount <= 0) return; Add(asset, amount); if (dirty > 0) SetDirty(asset, DirtyOf(asset) + Math.Min(dirty, amount)); } /// 把这种币里干净的 qty 枚记成脏的(刚收到的兑付被判内幕、操纵时用);不够的部分不记,返回没记上的枚数。 public double MarkDirty(string asset, double qty) { if (qty <= 0) return 0; double clean = Math.Max(0, Get(asset) - DirtyOf(asset)), mark = Math.Min(qty, clean); SetDirty(asset, DirtyOf(asset) + mark); return qty - mark; } /// 余额按比例缩水(CRC 托管银行减记):脏枚数同比例缩水。 public void Scale(string asset, double factor) { double g = Get(asset); if (g <= 0) return; double d = DirtyOf(asset); Add(asset, g * Math.Max(0, factor) - g); SetDirty(asset, d * Math.Max(0, factor)); } /// 兑换:付出 amount 枚 from(按它的脏比例带走脏枚数),收到 got 枚 to,脏比例跟过去;clean 为真时(兑入 Ponero)收到的是干净的。 public void Convert(string from, double amount, string to, double got, bool clean = false) { double r = DirtyRatio(from); Add(from, -amount); AddDirty(to, got, clean ? 0 : got * r); } /// 花掉(销毁)一笔币:脏枚数按这种币的脏比例带走,成本按这笔币占钱包总价值的比例带走。 public void Spend(string asset, double amount, Func price) { double total = Value(price), share = total > 0 ? Math.Min(1, amount * price(asset) / total) : 0; double basis = Basis * share; Add(asset, -amount); Basis -= basis; } /// 按价格表估值(信用点)。 public double Value(Func price) => Balances.Sum(kv => kv.Value * price(kv.Key)); /// 按信用点额折成的诈骗罪证:额 × ,单次封顶 (永续被判操纵、Tumbler 泄露用)。 public static double FraudFor(double credits) => Math.Min(Tuning.PredictFraudCap, Math.Max(0, credits) * Tuning.PredictFraudShare); /// /// 只转干净的币(往 Poinbase 充值):脏枚数全部留在原钱包,成本按价值比例带走。数量超过 的部分不转,返回实际转的数量。 /// public static double TransferClean(Wallet from, Wallet to, string asset, double amount, Func price) { amount = Math.Min(amount, from.CleanAvail(asset)); if (amount <= 0) return 0; double total = from.Value(price); double share = total > 0 ? Math.Min(1.0, amount * price(asset) / total) : 0; double basis = from.Basis * share, dirty = from.DirtyOf(asset); from.Add(asset, -amount); from.SetDirty(asset, dirty); from.Basis -= basis; to.Add(asset, amount); to.Basis += basis; return amount; } /// /// 转出:这种币按它的脏比例把脏枚数带过去,成本按转出价值占地址总价值的比例带走。 /// Ponero 等不可追踪资产转入时,不带脏枚数。 /// public static void Transfer(Wallet from, Wallet to, string asset, double amount, Func price) { if (amount <= 0) return; double total = from.Value(price); double share = total > 0 ? Math.Min(1.0, amount * price(asset) / total) : 0; double basis = from.Basis * share, dirty = amount * from.DirtyRatio(asset); from.Add(asset, -amount); from.Basis -= basis; var spec = Assets.Get(asset); to.AddDirty(asset, amount, spec == null || spec.Traceable ? dirty : 0); to.Basis += basis; } /// /// 存档迁移:旧的整钱包污点额(信用点)按当前价格摊成各币的脏枚数:脏比例 = min(1, 污点 / 可追踪币的价值),每种可追踪的币按这个比例记脏。 /// extra 是另外挪进来的旧污点(Poinbase 账户、永续仓位),一起摊;超出的部分作废。旧字段清零。 /// public void MigrateTaint(Func price, double extra = 0) { double t = LegacyTaint + extra; LegacyTaint = 0; if (t <= 0) return; var traceable = Balances.Keys.Where(a => Assets.Get(a) == null || Assets.Get(a).Traceable).ToList(); double v = traceable.Sum(a => Get(a) * price(a)); if (v <= 0) return; double r = Math.Min(1, t / v); foreach (var a in traceable) SetDirty(a, DirtyOf(a) + Get(a) * r); } } }