Skip to content

feat(api): Asset/Price primitives with zero-float steemd semantics#13

Merged
kuny0707 merged 2 commits into
masterfrom
feat/asset-price-primitives
Jul 13, 2026
Merged

feat(api): Asset/Price primitives with zero-float steemd semantics#13
kuny0707 merged 2 commits into
masterfrom
feat/asset-price-primitives

Conversation

@ety001

@ety001 ety001 commented Jul 13, 2026

Copy link
Copy Markdown
Member

Summary

Adds the lowest-level Asset/Price primitives to protocol/api, filling the gap where wire types only "carry but don't interpret" string-asset fields (e.g. "1.500 STEEM"). Every consumer previously had to split(' ') + parse on its own; now a shared, exact primitive backs steemutil wire types, steemgosdk, and the upcoming conveyor Go rewrite.

Implements the plan in asset-price-plan.md (A1–A4), targeting v0.0.25.

Design decision: align with steemd chain semantics, zero float end-to-end

  • Asset.Amount is int64 (on-chain atomic units); precision is derived from symbol (STEEM/SBD=3, VESTS=6), capped at STEEM_MAX_SATOSHIS (2^62-1).
  • Price math uses math/big.Int (stdlib, covers 128-bit), replicating steemd "multiply-then-divide + overflow check"; Compare cross-multiplies.
  • dsteem's float64 is a JS legacy and is explicitly not followed — conveyor's price index is a fresh Go design; the SDK layer only provides exact integer primitives, and the display layer converts to float as needed.

Changes

Item File Content
A1 protocol/api/asset.go (new) Asset + ParseAsset/String/Add/Sub, int-only parse path (no float transit)
A2 protocol/api/price.go (new) Price + ParsePrice/Convert/Compare/Invert, big.Int math
A3 protocol/api/market.go OrderPrice.ToPrice() / CurrentMedianHistoryPrice.ToPrice() bridges
A4 protocol/api/prices.go (new) ComputePrices pure compute over wire types (conveyor get_prices, int avg)

Acceptance coverage

  • ParseAsset/String round-trip; illegal symbol / wrong-decimal-count / over-MaxSatoshis error
  • 0.1 + 0.2 zero-roundoff equivalent (100 + 200 = 300 atoms)
  • Price.Convert golden vectors (incl. 128-bit intermediate 1000 × 8×10^16 / 1.5×10^14 = 533333), exact integer comparison vs steemd
  • Convert overflow guard (result > MaxSatoshis → error)
  • Compare cross-multiply golden; Invert().Invert() round-trip
  • ComputePrices over go:embed testdata fixtures → exact atom values
  • go 1.18 compatible, zero new third-party deps

Test results

$ go test ./...
ok  	github.com/steemit/steemutil/protocol/api	0.012s
... (all packages pass)
$ go vet ./...   # clean
$ gofmt -l protocol/api/   # clean

Notes

  • STEEMUTIL_TODO.md is a pre-existing untracked file, not part of this change.
  • Integration tests (//go:build integration, real api.steemit.com) are intentionally out of scope for this PR; deterministic unit tests + fixtures are the default suite.

ety001 added 2 commits July 13, 2026 17:08
Add the lowest-level Asset/Price primitives to protocol/api, filling the
gap where wire types only "carry but don't interpret" string-asset fields
(e.g. "1.500 STEEM"). Every consumer previously had to split+parse; now a
shared, exact primitive backs steemutil wire types, steemgosdk, and the
upcoming conveyor Go rewrite.

Design: align with steemd chain semantics, zero float end-to-end.
- Asset.Amount is int64 (on-chain atomic units); precision is derived
  from symbol (STEEM/SBD=3, VESTS=6), capped at STEEM_MAX_SATOSHIS (2^62-1).
- Price math uses math/big.Int (stdlib, covers 128-bit), replicating
  steemd "multiply-then-divide + overflow check"; Compare cross-multiplies.
- dsteem's float64 is a JS legacy and explicitly not followed.

A1. asset.go  : Asset + ParseAsset/String/Add/Sub, int-only parse path.
A2. price.go  : Price + ParsePrice/Convert/Compare/Invert, big.Int math.
A3. market.go : OrderPrice/CurrentMedianHistoryPrice ToPrice() bridges.
A4. prices.go : ComputePrices over wire types (conveyor get_prices, int avg).

Tests: deterministic unit tests + go:embed testdata fixtures covering
round-trip, malformed input, 0.1+0.2 zero-roundoff, Convert golden
vectors (incl. 128-bit intermediate), overflow guards, cross-mult Compare,
Invert round-trip, and ComputePrices exact atoms. go1.18 compatible,
no new third-party deps.
Address four defects found in the self-audit of PR #13, each with a
deterministic regression test:

1. ComputePrices.resultSymbol was hardcoded "SBD". When an order book
   contained a non-STEEM/SBD pair (e.g. STEEM/VESTS), Convert returned
   VESTS atoms but the result was mislabeled "SBD" — silent corruption
   whose String() output was wrong by both symbol and 1000x value
   (VESTS 6dp vs SBD 3dp). Now: infer symbol from the first Convert,
   require all orders to yield a consistent SBD result, and error on
   any non-STEEM/SBD order.

2. Asset arithmetic was asymmetric with ParseAsset. Sub documented
   "results may be negative" yet ParseAsset rejects negatives, breaking
   the String<->ParseAsset round-trip. Add/Sub also only checked
   overflow when b.Amount > 0, missing negative-operand cases.
   Now: Sub rejects negative results (matches ParseAsset's non-negative
   invariant), Add uses bits.Add64 for sign-independent carry check.

3. Asset.Precision() silently returned 0 for unknown symbols, so
   String() emitted e.g. "100 UNKNOWN" with no error. Now: panic on
   unknown symbol — an Asset with an illegal symbol is a programmer
   error (only reachable via Asset{} literal bypassing ParseAsset).

4. Price.Convert documented its precondition: Price must be constructed
   via ParsePrice (guarantees non-zero Base/Quote, valid symbols). Direct
   Price{} literals bypass validation and are undefined behavior.

All tests pass: go test ./... green, go vet clean, gofmt clean.
@kuny0707
kuny0707 merged commit 47ae536 into master Jul 13, 2026
3 checks passed
@ety001
ety001 deleted the feat/asset-price-primitives branch July 13, 2026 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants