mirror of
https://github.com/luxfi/api.git
synced 2026-09-24 00:24:53 +00:00
feat(types): the JSON-RPC boundary carries 16- and 256-bit numbers too
The set was Uint32, Uint64, Float64. Two widths the chain actually uses were missing, so callers reached past this package for both. Uint16 is what platformvm's API already asks for today. Uint256 is the width the EVM carries. A wei value, a token balance or a supply does not fit in 64 bits — an 18-decimal balance passes 2^64 at about 18.4 tokens — so every such field arriving here had to become a bare string or a hand-rolled big.Int. It is uint256.Int rather than big.Int because a fixed-size value cannot be nil and does not allocate; the module was already in the graph. Every numeric here is a QUOTED decimal, which is the point of the package: a JSON number is a float64, and a uint64 above 2^53 does not survive one. The test asserts Uint64 REFUSES a value it cannot hold, which is the failure Uint256 exists to prevent. Unquoting was written out once per type and is now read in one place. Adding two more types would have made five copies of it.
This commit is contained in:
@@ -3,6 +3,7 @@ module github.com/luxfi/api
|
||||
go 1.26.4
|
||||
|
||||
require (
|
||||
github.com/holiman/uint256 v1.3.2
|
||||
github.com/luxfi/consensus v1.36.63
|
||||
github.com/luxfi/formatting v1.1.1
|
||||
github.com/luxfi/ids v1.3.2
|
||||
@@ -16,7 +17,6 @@ require (
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
|
||||
github.com/gorilla/rpc v1.2.1 // indirect
|
||||
github.com/grandcat/zeroconf v1.0.0 // indirect
|
||||
github.com/holiman/uint256 v1.3.2 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/luxfi/accel v1.2.4 // indirect
|
||||
github.com/luxfi/cache v1.3.1 // indirect
|
||||
|
||||
+75
-28
@@ -3,27 +3,60 @@
|
||||
|
||||
package types
|
||||
|
||||
import "strconv"
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
const Null = "null"
|
||||
|
||||
// payload strips the quotes JSON-RPC numerics are carried in and says whether
|
||||
// the value was null. Every type below reads a quoted decimal, so the reading
|
||||
// is stated once; each type differs only in how it parses what comes out.
|
||||
func payload(b []byte) (string, bool) {
|
||||
s := string(b)
|
||||
if s == Null {
|
||||
return "", true
|
||||
}
|
||||
if len(s) >= 2 {
|
||||
if last := len(s) - 1; s[0] == '"' && s[last] == '"' {
|
||||
s = s[1:last]
|
||||
}
|
||||
}
|
||||
return s, false
|
||||
}
|
||||
|
||||
func quote(s string) []byte { return []byte(`"` + s + `"`) }
|
||||
|
||||
type Uint16 uint16
|
||||
|
||||
func (u Uint16) MarshalJSON() ([]byte, error) {
|
||||
return quote(strconv.FormatUint(uint64(u), 10)), nil
|
||||
}
|
||||
|
||||
func (u *Uint16) UnmarshalJSON(b []byte) error {
|
||||
s, null := payload(b)
|
||||
if null {
|
||||
return nil
|
||||
}
|
||||
val, err := strconv.ParseUint(s, 10, 16)
|
||||
*u = Uint16(val)
|
||||
return err
|
||||
}
|
||||
|
||||
type Uint32 uint32
|
||||
|
||||
func (u Uint32) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + strconv.FormatUint(uint64(u), 10) + `"`), nil
|
||||
return quote(strconv.FormatUint(uint64(u), 10)), nil
|
||||
}
|
||||
|
||||
func (u *Uint32) UnmarshalJSON(b []byte) error {
|
||||
str := string(b)
|
||||
if str == Null {
|
||||
s, null := payload(b)
|
||||
if null {
|
||||
return nil
|
||||
}
|
||||
if len(str) >= 2 {
|
||||
if lastIndex := len(str) - 1; str[0] == '"' && str[lastIndex] == '"' {
|
||||
str = str[1:lastIndex]
|
||||
}
|
||||
}
|
||||
val, err := strconv.ParseUint(str, 10, 32)
|
||||
val, err := strconv.ParseUint(s, 10, 32)
|
||||
*u = Uint32(val)
|
||||
return err
|
||||
}
|
||||
@@ -31,41 +64,55 @@ func (u *Uint32) UnmarshalJSON(b []byte) error {
|
||||
type Uint64 uint64
|
||||
|
||||
func (u Uint64) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + strconv.FormatUint(uint64(u), 10) + `"`), nil
|
||||
return quote(strconv.FormatUint(uint64(u), 10)), nil
|
||||
}
|
||||
|
||||
func (u *Uint64) UnmarshalJSON(b []byte) error {
|
||||
str := string(b)
|
||||
if str == Null {
|
||||
s, null := payload(b)
|
||||
if null {
|
||||
return nil
|
||||
}
|
||||
if len(str) >= 2 {
|
||||
if lastIndex := len(str) - 1; str[0] == '"' && str[lastIndex] == '"' {
|
||||
str = str[1:lastIndex]
|
||||
}
|
||||
}
|
||||
val, err := strconv.ParseUint(str, 10, 64)
|
||||
val, err := strconv.ParseUint(s, 10, 64)
|
||||
*u = Uint64(val)
|
||||
return err
|
||||
}
|
||||
|
||||
// Uint256 is the width the EVM actually carries — a balance, a wei value or a
|
||||
// token amount does not fit in 64 bits, and every such field reaching this
|
||||
// boundary had to be a string or a big.Int by hand. It is fixed-size rather
|
||||
// than big.Int so it cannot be nil and cannot allocate on the hot path.
|
||||
type Uint256 uint256.Int
|
||||
|
||||
func (u Uint256) MarshalJSON() ([]byte, error) {
|
||||
v := uint256.Int(u)
|
||||
return quote(v.Dec()), nil
|
||||
}
|
||||
|
||||
func (u *Uint256) UnmarshalJSON(b []byte) error {
|
||||
s, null := payload(b)
|
||||
if null {
|
||||
return nil
|
||||
}
|
||||
v, err := uint256.FromDecimal(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
*u = Uint256(*v)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Float64 float64
|
||||
|
||||
func (f Float64) MarshalJSON() ([]byte, error) {
|
||||
return []byte(`"` + strconv.FormatFloat(float64(f), 'f', 4, 64) + `"`), nil
|
||||
return quote(strconv.FormatFloat(float64(f), 'f', 4, 64)), nil
|
||||
}
|
||||
|
||||
func (f *Float64) UnmarshalJSON(b []byte) error {
|
||||
str := string(b)
|
||||
if str == Null {
|
||||
s, null := payload(b)
|
||||
if null {
|
||||
return nil
|
||||
}
|
||||
if len(str) >= 2 {
|
||||
if lastIndex := len(str) - 1; str[0] == '"' && str[lastIndex] == '"' {
|
||||
str = str[1:lastIndex]
|
||||
}
|
||||
}
|
||||
val, err := strconv.ParseFloat(str, 64)
|
||||
val, err := strconv.ParseFloat(s, 64)
|
||||
*f = Float64(val)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// Copyright (C) 2019-2025, Lux Industries Inc. All rights reserved.
|
||||
// See the file LICENSE for licensing terms.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
// Every numeric at this boundary is carried as a QUOTED decimal, because
|
||||
// JSON numbers are float64 and a uint64 above 2^53 does not survive one.
|
||||
func TestQuotedDecimalRoundTrip(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
in any
|
||||
want string
|
||||
}{
|
||||
{"uint16 max", Uint16(65535), `"65535"`},
|
||||
{"uint32 max", Uint32(4294967295), `"4294967295"`},
|
||||
{"uint64 max", Uint64(18446744073709551615), `"18446744073709551615"`},
|
||||
{"uint256 max", Uint256(*uint256.MustFromDecimal("115792089237316195423570985008687907853269984665640564039457584007913129639935")),
|
||||
`"115792089237316195423570985008687907853269984665640564039457584007913129639935"`},
|
||||
{"uint256 one wei over uint64", Uint256(*uint256.MustFromDecimal("18446744073709551616")), `"18446744073709551616"`},
|
||||
} {
|
||||
got, err := json.Marshal(tc.in)
|
||||
if err != nil {
|
||||
t.Fatalf("%s: marshal: %v", tc.name, err)
|
||||
}
|
||||
if string(got) != tc.want {
|
||||
t.Errorf("%s: got %s want %s", tc.name, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestUint256SurvivesWhatUint64Cannot(t *testing.T) {
|
||||
// A token balance with 18 decimals passes 2^64 at ~18.4 tokens.
|
||||
const wei = "1000000000000000000000" // 1000 tokens
|
||||
var u Uint256
|
||||
if err := json.Unmarshal([]byte(`"`+wei+`"`), &u); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
if v := uint256.Int(u); v.Dec() != wei {
|
||||
t.Errorf("got %s want %s", v.Dec(), wei)
|
||||
}
|
||||
var small Uint64
|
||||
if err := json.Unmarshal([]byte(`"`+wei+`"`), &small); err == nil {
|
||||
t.Error("Uint64 accepted a value it cannot hold; that is the bug Uint256 exists for")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNullLeavesTheValueAlone(t *testing.T) {
|
||||
u16, u32, u64, f := Uint16(7), Uint32(7), Uint64(7), Float64(7)
|
||||
u256 := Uint256(*uint256.NewInt(7))
|
||||
for _, p := range []json.Unmarshaler{&u16, &u32, &u64, &u256, &f} {
|
||||
if err := p.UnmarshalJSON([]byte(Null)); err != nil {
|
||||
t.Fatalf("null: %v", err)
|
||||
}
|
||||
}
|
||||
if u16 != 7 || u32 != 7 || u64 != 7 || f != 7 {
|
||||
t.Error("null overwrote a value")
|
||||
}
|
||||
if v := uint256.Int(u256); v.Dec() != "7" {
|
||||
t.Error("null overwrote the 256-bit value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnquotedIsAlsoAccepted(t *testing.T) {
|
||||
var u Uint64
|
||||
if err := json.Unmarshal([]byte(`12345`), &u); err != nil || u != 12345 {
|
||||
t.Errorf("bare number: got %d err %v", u, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user