Skip to content

Commit 47ae536

Browse files
authored
Merge pull request #13 from steemit/feat/asset-price-primitives
feat(api): Asset/Price primitives with zero-float steemd semantics
2 parents 6a3bc7c + 9d481a4 commit 47ae536

10 files changed

Lines changed: 1243 additions & 0 deletions

File tree

‎protocol/api/asset.go‎

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
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

Comments
 (0)