|
| 1 | +package api |
| 2 | + |
| 3 | +import ( |
| 4 | + "math/bits" |
| 5 | + "strconv" |
| 6 | + "strings" |
| 7 | + |
| 8 | + "github.com/pkg/errors" |
| 9 | +) |
| 10 | + |
| 11 | +// 精度常量(对齐 steemd STEEM_PRECISION_*)。 |
| 12 | +// STEEM 与 SBD 是 3 位小数,VESTS 是 6 位小数。 |
| 13 | +const ( |
| 14 | + PrecisionSteem = 3 |
| 15 | + PrecisionSbd = 3 |
| 16 | + PrecisionVests = 6 |
| 17 | +) |
| 18 | + |
| 19 | +// MaxSatoshis 对齐 STEEM_MAX_SATOSHIS = 2^62-1(steemd config.hpp:241)。 |
| 20 | +// 链上任何 asset 的原子单位绝对值都不允许超过此值。 |
| 21 | +const MaxSatoshis int64 = 4611686018427387903 // (1 << 62) - 1 |
| 22 | + |
| 23 | +// Asset 表示 Steem 链上资产:int64 原子单位 + symbol。 |
| 24 | +// 精度(小数位数)从 symbol 派生,仅在 ParseAsset/String 时参与,不单独存储, |
| 25 | +// 这样 Asset 始终是"链端真相"的精确表达,杜绝 float 中转。 |
| 26 | +// |
| 27 | +// 与 protocol 根包的 Asset(用于交易二进制序列化)不同:本类型是 |
| 28 | +// protocol/api 层的"解释型"原语,面向 wire JSON 字段(如 "1.500 STEEM")。 |
| 29 | +type Asset struct { |
| 30 | + Amount int64 |
| 31 | + Symbol string // STEEM | SBD | VESTS | TESTS | TBD |
| 32 | +} |
| 33 | + |
| 34 | +// symbolPrecision 返回 symbol 的小数位数;非法 symbol 报错。 |
| 35 | +// 对齐 steemd asset_symbol::decimals()。 |
| 36 | +func symbolPrecision(symbol string) (int, error) { |
| 37 | + switch strings.ToUpper(symbol) { |
| 38 | + case "STEEM", "SBD", "TESTS", "TBD": |
| 39 | + return PrecisionSteem, nil |
| 40 | + case "VESTS": |
| 41 | + return PrecisionVests, nil |
| 42 | + default: |
| 43 | + return 0, errors.Errorf("unknown asset symbol: %q", symbol) |
| 44 | + } |
| 45 | +} |
| 46 | + |
| 47 | +// ParseAsset 解析 "1.500 STEEM" 形式的 asset 字符串为 Asset。 |
| 48 | +// |
| 49 | +// 流程(全程零 float): |
| 50 | +// - 按单空格切分 [amountStr, symbol];symbol 转大写后校验合法性。 |
| 51 | +// - 按 '.' 切分整数/小数部分,拼成纯数字串后 strconv.ParseInt(不经 float 中转)。 |
| 52 | +// - 校验小数位数 == symbolPrecision(symbol)(VESTS 给 3 位小数即报错)。 |
| 53 | +// - 校验 0 <= amount <= MaxSatoshis(对齐 steemd asset::validate)。 |
| 54 | +// |
| 55 | +// 不接受负数:steemd wire 字段在本应用场景下均为非负(余额、供给量等)。 |
| 56 | +func ParseAsset(s string) (Asset, error) { |
| 57 | + parts := strings.Split(strings.TrimSpace(s), " ") |
| 58 | + if len(parts) != 2 { |
| 59 | + return Asset{}, errors.Errorf("invalid asset format: %q (expected 'amount symbol')", s) |
| 60 | + } |
| 61 | + |
| 62 | + amountStr := parts[0] |
| 63 | + symbol := strings.ToUpper(parts[1]) |
| 64 | + |
| 65 | + prec, err := symbolPrecision(symbol) |
| 66 | + if err != nil { |
| 67 | + return Asset{}, err |
| 68 | + } |
| 69 | + |
| 70 | + // 切出整数与小数部分,纯字符串操作,杜绝 float。 |
| 71 | + var neg bool |
| 72 | + intPart, fracPart := amountStr, "" |
| 73 | + if strings.HasPrefix(amountStr, "-") { |
| 74 | + neg = true |
| 75 | + intPart = amountStr[1:] |
| 76 | + } else if strings.HasPrefix(amountStr, "+") { |
| 77 | + intPart = amountStr[1:] |
| 78 | + } |
| 79 | + if dot := strings.Index(intPart, "."); dot >= 0 { |
| 80 | + intPart, fracPart = intPart[:dot], intPart[dot+1:] |
| 81 | + } |
| 82 | + |
| 83 | + // 校验整数/小数部分都是纯数字(空串也算非法,防止 "." / "1." / ".5")。 |
| 84 | + if intPart == "" && fracPart == "" { |
| 85 | + return Asset{}, errors.Errorf("invalid asset amount: %q", amountStr) |
| 86 | + } |
| 87 | + for _, ch := range intPart + fracPart { |
| 88 | + if ch < '0' || ch > '9' { |
| 89 | + return Asset{}, errors.Errorf("invalid asset amount: %q", amountStr) |
| 90 | + } |
| 91 | + } |
| 92 | + |
| 93 | + // 校验小数位数与 symbol 精度一致。 |
| 94 | + if len(fracPart) != prec { |
| 95 | + return Asset{}, errors.Errorf( |
| 96 | + "asset %q has %d decimal places, but symbol %s requires %d", |
| 97 | + s, len(fracPart), symbol, prec, |
| 98 | + ) |
| 99 | + } |
| 100 | + |
| 101 | + // 拼成纯数字串解析(去前导零交给 ParseInt 处理)。 |
| 102 | + combined := intPart + fracPart |
| 103 | + if combined == "" { |
| 104 | + combined = "0" |
| 105 | + } |
| 106 | + parsed, err := strconv.ParseInt(combined, 10, 64) |
| 107 | + if err != nil { |
| 108 | + // 大概率是溢出 int64。 |
| 109 | + return Asset{}, errors.Wrapf(err, "asset amount overflow: %q", amountStr) |
| 110 | + } |
| 111 | + if neg { |
| 112 | + parsed = -parsed |
| 113 | + } |
| 114 | + |
| 115 | + // 链端校验:|amount| <= MaxSatoshis,且本场景限定非负。 |
| 116 | + if parsed < 0 || parsed > MaxSatoshis { |
| 117 | + return Asset{}, errors.Errorf( |
| 118 | + "asset amount %d out of range [0, %d]", parsed, MaxSatoshis, |
| 119 | + ) |
| 120 | + } |
| 121 | + |
| 122 | + return Asset{Amount: parsed, Symbol: symbol}, nil |
| 123 | +} |
| 124 | + |
| 125 | +// Precision 返回该 asset symbol 的小数位数。 |
| 126 | +// |
| 127 | +// 前置条件:Symbol 必须是经 ParseAsset 校验过的合法 symbol。直接用 Asset{} |
| 128 | +// 字面量构造非法 symbol 属于编程错误,对未知 symbol 直接 panic 而非静默退化 |
| 129 | +// (静默返回 0 会让 String() 输出丢精度且无报错,难排查)。 |
| 130 | +func (a Asset) Precision() int { |
| 131 | + prec, err := symbolPrecision(a.Symbol) |
| 132 | + if err != nil { |
| 133 | + panic(err) |
| 134 | + } |
| 135 | + return prec |
| 136 | +} |
| 137 | + |
| 138 | +// String 从 int64 重建 "1.500 STEEM" 字符串。 |
| 139 | +// 纯整数除法 + 取模重建小数点,避免 strconv.FormatFloat 的 round-half-even。 |
| 140 | +// 不经任何 float 中转,与 ParseAsset 严格互逆(round-trip)。 |
| 141 | +func (a Asset) String() string { |
| 142 | + prec := a.Precision() |
| 143 | + amount := a.Amount |
| 144 | + negative := amount < 0 |
| 145 | + if negative { |
| 146 | + amount = -amount |
| 147 | + } |
| 148 | + |
| 149 | + // 整数部分与小数(原子)部分。 |
| 150 | + var intPart, fracPart string |
| 151 | + if prec == 0 { |
| 152 | + intPart = strconv.FormatInt(amount, 10) |
| 153 | + } else { |
| 154 | + divisor := int64(1) |
| 155 | + for i := 0; i < prec; i++ { |
| 156 | + divisor *= 10 |
| 157 | + } |
| 158 | + intVal := amount / divisor |
| 159 | + fracVal := amount % divisor |
| 160 | + intPart = strconv.FormatInt(intVal, 10) |
| 161 | + // 小数部分左侧补零到 prec 位。 |
| 162 | + fracPart = strconv.FormatInt(fracVal, 10) |
| 163 | + if len(fracPart) < prec { |
| 164 | + fracPart = strings.Repeat("0", prec-len(fracPart)) + fracPart |
| 165 | + } |
| 166 | + } |
| 167 | + |
| 168 | + out := intPart |
| 169 | + if prec > 0 { |
| 170 | + out = out + "." + fracPart |
| 171 | + } |
| 172 | + if negative { |
| 173 | + out = "-" + out |
| 174 | + } |
| 175 | + return out + " " + a.Symbol |
| 176 | +} |
| 177 | + |
| 178 | +// Add 同 symbol 加法(带溢出检查),对齐 steemd asset::operator+ 的 safe<int64_t> 语义。 |
| 179 | +// 不同 symbol 报错(链端 assert base.asset == addend.asset)。 |
| 180 | +// |
| 181 | +// 用 math/bits.Add64 做符号无关的精确溢出检查:拿到进位标志即可判定,无需 |
| 182 | +// 针对正负组合分别写条件(避免"只在 b>0 时检查"一类遗漏)。 |
| 183 | +func (a Asset) Add(b Asset) (Asset, error) { |
| 184 | + if a.Symbol != b.Symbol { |
| 185 | + return Asset{}, errors.Errorf( |
| 186 | + "cannot add assets of different symbols: %s vs %s", a.Symbol, b.Symbol, |
| 187 | + ) |
| 188 | + } |
| 189 | + sum, carry := bits.Add64(uint64(a.Amount), uint64(b.Amount), 0) |
| 190 | + if carry != 0 || sum > uint64(MaxSatoshis) { |
| 191 | + return Asset{}, errors.Errorf("asset addition overflow: %d + %d", a.Amount, b.Amount) |
| 192 | + } |
| 193 | + return Asset{Amount: int64(sum), Symbol: a.Symbol}, nil |
| 194 | +} |
| 195 | + |
| 196 | +// Sub 同 symbol 减法(带溢出检查),对齐 steemd asset::operator-。 |
| 197 | +// |
| 198 | +// 结果为负即报错:与 ParseAsset 的"非负"不变式保持一致——所有 Asset(无论来源) |
| 199 | +// 始终代表链上合法值 [0, MaxSatoshis]。需要"净流入"等可能为负的场景由调用方在 |
| 200 | +// Sub 前比较大小、自行交换操作数;这样 Asset 的 round-trip 契约(String ↔ ParseAsset) |
| 201 | +// 对所有合法值都严格成立。 |
| 202 | +func (a Asset) Sub(b Asset) (Asset, error) { |
| 203 | + if a.Symbol != b.Symbol { |
| 204 | + return Asset{}, errors.Errorf( |
| 205 | + "cannot subtract assets of different symbols: %s vs %s", a.Symbol, b.Symbol, |
| 206 | + ) |
| 207 | + } |
| 208 | + if b.Amount > a.Amount { |
| 209 | + return Asset{}, errors.Errorf( |
| 210 | + "asset subtraction would be negative: %d - %d", a.Amount, b.Amount, |
| 211 | + ) |
| 212 | + } |
| 213 | + return Asset{Amount: a.Amount - b.Amount, Symbol: a.Symbol}, nil |
| 214 | +} |
0 commit comments