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);
}
}
}