Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 49 additions & 2 deletions sdk/tii/encode.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package tii

import (
"encoding/hex"
"fmt"
"sort"
)
Expand Down Expand Up @@ -95,9 +96,14 @@ func marshal(param ParamType, value interface{}, nested bool) (interface{}, erro
switch value.(type) {
case string, map[string]interface{}:
return leaf("bytes", value, nested), nil
default:
return nil, wrongShape("bytes", "hex string or bytes envelope", value)
}
// A native byte array (`[]byte`, or an integer array — the JSON shape
// other SDKs' native byte arrays serialize to) canonicalizes to
// 0x-prefixed hex, the wire form the resolver coerces (SDK spec §3.9).
if raw, ok := asByteArray(value); ok {
return leaf("bytes", "0x"+hex.EncodeToString(raw), nested), nil
}
return nil, wrongShape("bytes", "hex string, bytes envelope, or byte array", value)
case KindAddress:
switch value.(type) {
case string:
Expand Down Expand Up @@ -192,6 +198,47 @@ func marshal(param ParamType, value interface{}, nested bool) (interface{}, erro
}
}

// asByteArray interprets a value as a raw byte array: a `[]byte`, or an array
// whose every element is an integer in 0..=255. The second return is false if
// it is neither.
func asByteArray(value interface{}) ([]byte, bool) {
switch v := value.(type) {
case []byte:
return v, true
case []interface{}:
out := make([]byte, len(v))
for i, item := range v {
b, ok := asByteValue(item)
if !ok {
return nil, false
}
out[i] = b
}
return out, true
default:
return nil, false
}
}

// asByteValue interprets one array element as a byte (an integer in 0..=255).
func asByteValue(item interface{}) (byte, bool) {
switch n := item.(type) {
case float64:
if n == float64(int64(n)) && n >= 0 && n <= 255 {
return byte(n), true
}
case int:
if n >= 0 && n <= 255 {
return byte(n), true
}
case int64:
if n >= 0 && n <= 255 {
return byte(n), true
}
}
return 0, false
}

// leaf renders a scalar leaf: bare at the top level (the resolver knows the
// param's flat type), tagged when nested inside an aggregate (it doesn't).
func leaf(tag string, value interface{}, nested bool) interface{} {
Expand Down
120 changes: 120 additions & 0 deletions sdk/tii/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,3 +194,123 @@ func TestNestedScalarIsTagged(t *testing.T) {
t.Errorf("nested int: got %s, want tagged {\"list\":[{\"int\":5}]}", gotJSON)
}
}

const bytesSchema = `{"$ref":"https://tx3.land/specs/v1beta0/tii#/$defs/Bytes"}`
const listOfBytesSchema = `{"type":"array","items":{"$ref":"https://tx3.land/specs/v1beta0/tii#/$defs/Bytes"}}`

// TestNativeByteArraysCanonicalizeToHex verifies that a native byte array
// (`[]byte`, or an integer array — the JSON shape other SDKs' native byte
// arrays serialize to) canonicalizes to 0x-prefixed hex, per SDK spec §3.9
// (regression: TRP `(-32005) value is not bytes: [1,1]`).
func TestNativeByteArraysCanonicalizeToHex(t *testing.T) {
bytesParam := ParamTypeFromSchema(parse(t, bytesSchema), nil)

got, err := Encode(bytesParam, []byte{1, 1})
if err != nil {
t.Fatalf("encode []byte failed: %v", err)
}
if !jsonEqual(t, got, "0x0101") {
t.Errorf("[]byte: got %#v, want \"0x0101\"", got)
}

got, err = Encode(bytesParam, []interface{}{float64(1), float64(1)})
if err != nil {
t.Fatalf("encode integer array failed: %v", err)
}
if !jsonEqual(t, got, "0x0101") {
t.Errorf("integer array: got %#v, want \"0x0101\"", got)
}

listParam := ParamTypeFromSchema(parse(t, listOfBytesSchema), nil)
got, err = Encode(listParam, []interface{}{[]byte{1, 2}})
if err != nil {
t.Fatalf("encode list of []byte failed: %v", err)
}
want := map[string]interface{}{"list": []interface{}{
map[string]interface{}{"bytes": "0x0102"},
}}
if !jsonEqual(t, got, want) {
gotJSON, _ := json.Marshal(got)
t.Errorf("nested []byte: got %s, want {\"list\":[{\"bytes\":\"0x0102\"}]}", gotJSON)
}
}

// TestRejectsNonByteArraysForBytes pins the reject pass for byte params.
func TestRejectsNonByteArraysForBytes(t *testing.T) {
bytesParam := ParamTypeFromSchema(parse(t, bytesSchema), nil)
for _, bad := range []interface{}{
[]interface{}{float64(1), float64(256)},
[]interface{}{float64(1), float64(-1)},
[]interface{}{"aa", float64(1)},
[]interface{}{true},
true,
} {
if _, err := Encode(bytesParam, bad); err == nil {
t.Errorf("value %#v should have been rejected", bad)
}
}
}

// TestHydraInitArgShapes covers the Hydra `init` argument shapes:
// `participants` / `parties` are `List<Bytes>`, `head_id` is `Bytes`
// (regression: `(-32005) target type not supported: List` /
// `value is not bytes: [1,2]`).
func TestHydraInitArgShapes(t *testing.T) {
listParam := ParamTypeFromSchema(parse(t, listOfBytesSchema), nil)

got, err := Encode(listParam, []interface{}{"0102", "0304"})
if err != nil {
t.Fatalf("encode hex strings failed: %v", err)
}
want := map[string]interface{}{"list": []interface{}{
map[string]interface{}{"bytes": "0102"},
map[string]interface{}{"bytes": "0304"},
}}
if !jsonEqual(t, got, want) {
gotJSON, _ := json.Marshal(got)
t.Errorf("participants (hex): got %s, want tagged list of bytes", gotJSON)
}

got, err = Encode(listParam, []interface{}{[]byte{1, 2}})
if err != nil {
t.Fatalf("encode native byte arrays failed: %v", err)
}
want = map[string]interface{}{"list": []interface{}{
map[string]interface{}{"bytes": "0x0102"},
}}
if !jsonEqual(t, got, want) {
gotJSON, _ := json.Marshal(got)
t.Errorf("participants (native): got %s, want tagged list of bytes", gotJSON)
}

bytesParam := ParamTypeFromSchema(parse(t, bytesSchema), nil)
got, err = Encode(bytesParam, "abcd0123")
if err != nil {
t.Fatalf("encode head_id failed: %v", err)
}
if !jsonEqual(t, got, "abcd0123") {
t.Errorf("head_id: got %#v, want bare \"abcd0123\"", got)
}
}

// TestAsteriaNameArgShapes covers the Asteria `create_ship` `ship_name` /
// `pilot_name` `Bytes` params (regression: `(-32005) value is not bytes: [1,1]`).
func TestAsteriaNameArgShapes(t *testing.T) {
bytesParam := ParamTypeFromSchema(parse(t, bytesSchema), nil)

got, err := Encode(bytesParam, "53484950313233")
if err != nil {
t.Fatalf("encode hex name failed: %v", err)
}
if !jsonEqual(t, got, "53484950313233") {
t.Errorf("hex name: got %#v, want bare passthrough", got)
}

got, err = Encode(bytesParam, []byte("SHIP"))
if err != nil {
t.Fatalf("encode native name failed: %v", err)
}
if !jsonEqual(t, got, "0x53484950") {
t.Errorf("native name: got %#v, want \"0x53484950\"", got)
}
}
Loading