diff --git a/cmd/generate-bindings/solana/anchor-go/generator/constants.go b/cmd/generate-bindings/solana/anchor-go/generator/constants.go index 591c4556..d05100f6 100644 --- a/cmd/generate-bindings/solana/anchor-go/generator/constants.go +++ b/cmd/generate-bindings/solana/anchor-go/generator/constants.go @@ -217,7 +217,9 @@ func (g *Generator) gen_constants() (*OutputFile, error) { _ = ty // "type":{"array":["u8",23]},"value":"[115, 101, 110, 100, 95, 119, 105, 116, 104, 95, 115, 119, 97, 112, 95, 100, 101, 108, 101, 103, 97, 116, 101]" var b []any - err := json.Unmarshal([]byte(co.Value), &b) + dec := json.NewDecoder(strings.NewReader(co.Value)) + dec.UseNumber() + err := dec.Decode(&b) if err != nil { return nil, fmt.Errorf("failed to unmarshal array constants[%d] %s: %w", coi, spew.Sdump(co), err) } @@ -260,36 +262,66 @@ func (g *Generator) gen_constants() (*OutputFile, error) { }).Op("{").ListFunc(func(byteGroup *Group) { for _, val := range b[:] { switch ty.Type.(type) { - case *idltype.U8: - byteGroup.Lit(byte(val.(float64))) - case *idltype.I8: - byteGroup.Lit(int8(val.(float64))) - case *idltype.U16: - byteGroup.Lit(uint16(val.(float64))) - case *idltype.I16: - byteGroup.Lit(int16(val.(float64))) - case *idltype.U32: - byteGroup.Lit(uint32(val.(float64))) - case *idltype.I32: - byteGroup.Lit(int32(val.(float64))) - case *idltype.U64: - byteGroup.Lit(uint64(val.(float64))) - case *idltype.I64: - byteGroup.Lit(int64(val.(float64))) - case *idltype.F32: - // TODO: is this correct? Are they encoded as strings? - v, err := strconv.ParseFloat(val.(string), 32) - if err != nil { - panic(fmt.Errorf("failed to parse f32 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) - } - byteGroup.Lit(float32(v)) - case *idltype.F64: - // TODO: is this correct? Are they encoded as strings? - v, err := strconv.ParseFloat(val.(string), 64) - if err != nil { - panic(fmt.Errorf("failed to parse f64 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) - } - byteGroup.Lit(v) + case *idltype.U8: + v, err := strconv.ParseUint(val.(json.Number).String(), 10, 8) + if err != nil { + panic(fmt.Errorf("failed to parse u8 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) + } + byteGroup.Lit(byte(v)) + case *idltype.I8: + v, err := strconv.ParseInt(val.(json.Number).String(), 10, 8) + if err != nil { + panic(fmt.Errorf("failed to parse i8 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) + } + byteGroup.Lit(int8(v)) + case *idltype.U16: + v, err := strconv.ParseUint(val.(json.Number).String(), 10, 16) + if err != nil { + panic(fmt.Errorf("failed to parse u16 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) + } + byteGroup.Lit(uint16(v)) + case *idltype.I16: + v, err := strconv.ParseInt(val.(json.Number).String(), 10, 16) + if err != nil { + panic(fmt.Errorf("failed to parse i16 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) + } + byteGroup.Lit(int16(v)) + case *idltype.U32: + v, err := strconv.ParseUint(val.(json.Number).String(), 10, 32) + if err != nil { + panic(fmt.Errorf("failed to parse u32 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) + } + byteGroup.Lit(uint32(v)) + case *idltype.I32: + v, err := strconv.ParseInt(val.(json.Number).String(), 10, 32) + if err != nil { + panic(fmt.Errorf("failed to parse i32 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) + } + byteGroup.Lit(int32(v)) + case *idltype.U64: + v, err := strconv.ParseUint(val.(json.Number).String(), 10, 64) + if err != nil { + panic(fmt.Errorf("failed to parse u64 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) + } + byteGroup.Lit(uint64(v)) + case *idltype.I64: + v, err := strconv.ParseInt(val.(json.Number).String(), 10, 64) + if err != nil { + panic(fmt.Errorf("failed to parse i64 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) + } + byteGroup.Lit(int64(v)) + case *idltype.F32: + v, err := strconv.ParseFloat(val.(json.Number).String(), 32) + if err != nil { + panic(fmt.Errorf("failed to parse f32 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) + } + byteGroup.Lit(float32(v)) + case *idltype.F64: + v, err := strconv.ParseFloat(val.(json.Number).String(), 64) + if err != nil { + panic(fmt.Errorf("failed to parse f64 in constants[%d] %s: %w", coi, spew.Sdump(co), err)) + } + byteGroup.Lit(v) case *idltype.String: v, err := strconv.Unquote(val.(string)) if err != nil { diff --git a/cmd/generate-bindings/solana/anchor-go/generator/constants_test.go b/cmd/generate-bindings/solana/anchor-go/generator/constants_test.go index 92e3192c..23a516c8 100644 --- a/cmd/generate-bindings/solana/anchor-go/generator/constants_test.go +++ b/cmd/generate-bindings/solana/anchor-go/generator/constants_test.go @@ -715,6 +715,108 @@ func TestGenConstantsErrorCases(t *testing.T) { }) } +// TestGenConstantsLargeU64I64ArrayPrecision verifies that u64 and i64 array +// elements above 2^53 are emitted with full 64-bit precision. json.Unmarshal +// into []any decodes numbers as float64, which silently rounds integers larger +// than 2^53. This test catches that: if the generator still uses float64 casts, +// the expected exact values will not appear in the generated code. +func TestGenConstantsLargeU64I64ArrayPrecision(t *testing.T) { + t.Run("u64 array with values above 2^53", func(t *testing.T) { + constants := []idl.IdlConst{ + { + Name: "LARGE_U64_ARRAY", + Ty: &idltype.Array{ + Type: &idltype.U64{}, + Size: &idltype.IdlArrayLenValue{Value: 4}, + }, + // 2^53 = 9007199254740992 is the last integer float64 represents exactly. + // 2^53+1 and 2^53+3 are NOT representable in float64 and will be rounded + // to 2^53 and 2^53+4 respectively if parsed through float64. + Value: "[9007199254740993, 9007199254740995, 18446744073709551615, 9007199254740992]", + }, + } + + idlData := &idl.Idl{Constants: constants} + gen := &Generator{idl: idlData, options: &GeneratorOptions{Package: "test"}} + + outputFile, err := gen.gen_constants() + require.NoError(t, err) + + generatedCode := outputFile.File.GoString() + + // 2^53+1 = 0x20000000000001 — NOT exactly representable in float64 + assert.Contains(t, generatedCode, "uint64(0x20000000000001)", + "9007199254740993 (2^53+1) was rounded; float64 precision loss in u64 array element") + // 2^53+3 = 0x20000000000003 — NOT exactly representable in float64 + assert.Contains(t, generatedCode, "uint64(0x20000000000003)", + "9007199254740995 (2^53+3) was rounded; float64 precision loss in u64 array element") + // max u64 = 0xffffffffffffffff + assert.Contains(t, generatedCode, "uint64(0xffffffffffffffff)", + "18446744073709551615 (max u64) was rounded; float64 precision loss in u64 array element") + // 2^53 exactly representable — should always work + assert.Contains(t, generatedCode, "uint64(0x20000000000000)", + "9007199254740992 (2^53) should be emitted correctly") + }) + + t.Run("i64 array with values above 2^53", func(t *testing.T) { + constants := []idl.IdlConst{ + { + Name: "LARGE_I64_ARRAY", + Ty: &idltype.Array{ + Type: &idltype.I64{}, + Size: &idltype.IdlArrayLenValue{Value: 4}, + }, + Value: "[9007199254740993, -9007199254740993, 9223372036854775807, -9223372036854775808]", + }, + } + + idlData := &idl.Idl{Constants: constants} + gen := &Generator{idl: idlData, options: &GeneratorOptions{Package: "test"}} + + outputFile, err := gen.gen_constants() + require.NoError(t, err) + + generatedCode := outputFile.File.GoString() + + // 2^53+1 positive + assert.Contains(t, generatedCode, "int64(9007199254740993)", + "9007199254740993 (2^53+1) was rounded; float64 precision loss in i64 array element") + // 2^53+1 negative + assert.Contains(t, generatedCode, "int64(-9007199254740993)", + "-9007199254740993 was rounded; float64 precision loss in i64 array element") + // max i64 + assert.Contains(t, generatedCode, "int64(9223372036854775807)", + "max i64 was rounded; float64 precision loss in i64 array element") + // min i64 + assert.Contains(t, generatedCode, "int64(-9223372036854775808)", + "min i64 was rounded; float64 precision loss in i64 array element") + }) + + t.Run("u32 array is not affected", func(t *testing.T) { + // u32 max = 4294967295 < 2^53, so float64 is fine + constants := []idl.IdlConst{ + { + Name: "U32_ARRAY", + Ty: &idltype.Array{ + Type: &idltype.U32{}, + Size: &idltype.IdlArrayLenValue{Value: 2}, + }, + Value: "[4294967295, 0]", + }, + } + + idlData := &idl.Idl{Constants: constants} + gen := &Generator{idl: idlData, options: &GeneratorOptions{Package: "test"}} + + outputFile, err := gen.gen_constants() + require.NoError(t, err) + + generatedCode := outputFile.File.GoString() + assert.Contains(t, generatedCode, "uint32(0xffffffff)") + assert.Contains(t, generatedCode, "uint32(0x0)") + }) +} + // TestGenConstantsRealWorldExamples 测试真实世界的例子 func TestGenConstantsRealWorldExamples(t *testing.T) { t.Run("Solana program constants", func(t *testing.T) {