diff --git a/README.md b/README.md index 63836dc..149c15a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,3 @@ # avm-abi -A reference implementation for the Algorand [ARC-4](https://arc.algorand.foundation/ARCs/arc-0004) ABI type system. +An implementation of the Algorand [ARC-4](https://arc.algorand.foundation/ARCs/arc-0004) ABI type system. diff --git a/abi/doc.go b/abi/doc.go new file mode 100644 index 0000000..da680ab --- /dev/null +++ b/abi/doc.go @@ -0,0 +1,14 @@ +/* +Package abi provides an implementation of the Algorand ARC-4 ABI type system. + +See https://arc.algorand.foundation/ARCs/arc-0004 for the corresponding specification. + + +Basic Operations + +This package can parse ABI type names using the `abi.TypeOf()` function. + +`abi.TypeOf()` returns an `abi.Type` struct. The `abi.Type` struct's `Encode` and `Decode` methods +can convert between Go values and encoded ABI byte strings. +*/ +package abi diff --git a/abi/encode.go b/abi/encode.go index 9d1b9d8..ea0f2b0 100644 --- a/abi/encode.go +++ b/abi/encode.go @@ -2,7 +2,6 @@ package abi import ( "encoding/binary" - "encoding/json" "fmt" "math/big" "reflect" @@ -459,78 +458,12 @@ func decodeTuple(encoded []byte, childT []Type) ([]interface{}, error) { return values, nil } -// maxAppArgs is the maximum number of arguments for an application call transaction, in compliance -// with ARC-4. Currently this is the same as the MaxAppArgs consensus parameter, but the -// difference is that the consensus parameter is liable to change in a future consensus upgrade. -// However, the ARC-4 ABI argument encoding **MUST** always remain the same. -const maxAppArgs = 16 - -// The tuple threshold is maxAppArgs, minus 1 for the method selector in the first app arg, -// minus 1 for the final app argument becoming a tuple of the remaining method args -const methodArgsTupleThreshold = maxAppArgs - 2 - -// ParseArgJSONtoByteSlice convert input method arguments to ABI encoded bytes -// it converts funcArgTypes into a tuple type and apply changes over input argument string (in JSON format) -// if there are greater or equal to 15 inputs, then we compact the tailing inputs into one tuple -func ParseArgJSONtoByteSlice(argTypes []string, jsonArgs []string, applicationArgs *[][]byte) error { - abiTypes := make([]Type, len(argTypes)) - for i, typeString := range argTypes { - abiType, err := TypeOf(typeString) - if err != nil { - return err - } - abiTypes[i] = abiType - } - - if len(abiTypes) != len(jsonArgs) { - return fmt.Errorf("input argument number %d != method argument number %d", len(jsonArgs), len(abiTypes)) - } - - // Up to 16 app arguments can be passed to app call. First is reserved for method selector, - // and the rest are for method call arguments. But if more than 15 method call arguments - // are present, then the method arguments after the 14th are placed in a tuple in the last - // app argument slot - if len(abiTypes) > maxAppArgs-1 { - typesForTuple := make([]Type, len(abiTypes)-methodArgsTupleThreshold) - copy(typesForTuple, abiTypes[methodArgsTupleThreshold:]) - - compactedType, err := MakeTupleType(typesForTuple) - if err != nil { - return err - } - - abiTypes = append(abiTypes[:methodArgsTupleThreshold], compactedType) - - tupleValues := make([]json.RawMessage, len(jsonArgs)-methodArgsTupleThreshold) - for i, jsonArg := range jsonArgs[methodArgsTupleThreshold:] { - tupleValues[i] = []byte(jsonArg) - } - - remainingJSON, err := json.Marshal(tupleValues) - if err != nil { - return err - } - - jsonArgs = append(jsonArgs[:methodArgsTupleThreshold], string(remainingJSON)) - } - - // parse JSON value to ABI encoded bytes - for i := 0; i < len(jsonArgs); i++ { - interfaceVal, err := abiTypes[i].UnmarshalFromJSON([]byte(jsonArgs[i])) - if err != nil { - return err - } - abiEncoded, err := abiTypes[i].Encode(interfaceVal) - if err != nil { - return err - } - *applicationArgs = append(*applicationArgs, abiEncoded) - } - return nil -} - // ParseMethodSignature parses a method of format `method(argType1,argType2,...)retType` // into `method` {`argType1`,`argType2`,...} and `retType` +// +// NOTE: This function **DOES NOT** verify that the argument or return type strings represent valid +// ABI types. Consider using `VerifyMethodSignature` prior to calling this function if you wish to +// verify those types. func ParseMethodSignature(methodSig string) (name string, argTypes []string, returnType string, err error) { argsStart := strings.Index(methodSig, "(") if argsStart == -1 { @@ -586,14 +519,14 @@ func VerifyMethodSignature(methodSig string) error { _, err = TypeOf(argType) if err != nil { - return fmt.Errorf("Error parsing argument type at index %d: %s", i, err.Error()) + return fmt.Errorf("Error parsing argument type at index %d: %w", i, err) } } if retType != VoidReturnType { _, err = TypeOf(retType) if err != nil { - return fmt.Errorf("Error parsing return type: %s", err.Error()) + return fmt.Errorf("Error parsing return type: %w", err) } } diff --git a/abi/encode_test.go b/abi/encode_test.go index 9dbaa3c..7cd3d60 100644 --- a/abi/encode_test.go +++ b/abi/encode_test.go @@ -3,7 +3,6 @@ package abi import ( "crypto/rand" "encoding/binary" - "fmt" "math/big" "testing" @@ -1030,122 +1029,6 @@ func TestRandomABIEncodeDecodeRoundTrip(t *testing.T) { categorySelfRoundTripTest(t, testValuePool[Tuple]) } -func TestParseArgJSONtoByteSlice(t *testing.T) { - makeRepeatSlice := func(size int, value string) []string { - slice := make([]string, size) - for i := range slice { - slice[i] = value - } - return slice - } - - tests := []struct { - argTypes []string - jsonArgs []string - expectedAppArgs [][]byte - }{ - { - argTypes: []string{}, - jsonArgs: []string{}, - expectedAppArgs: [][]byte{}, - }, - { - argTypes: []string{"uint8"}, - jsonArgs: []string{"100"}, - expectedAppArgs: [][]byte{{100}}, - }, - { - argTypes: []string{"uint8", "uint16"}, - jsonArgs: []string{"100", "65535"}, - expectedAppArgs: [][]byte{{100}, {255, 255}}, - }, - { - argTypes: makeRepeatSlice(15, "string"), - jsonArgs: []string{ - `"a"`, - `"b"`, - `"c"`, - `"d"`, - `"e"`, - `"f"`, - `"g"`, - `"h"`, - `"i"`, - `"j"`, - `"k"`, - `"l"`, - `"m"`, - `"n"`, - `"o"`, - }, - expectedAppArgs: [][]byte{ - {00, 01, 97}, - {00, 01, 98}, - {00, 01, 99}, - {00, 01, 100}, - {00, 01, 101}, - {00, 01, 102}, - {00, 01, 103}, - {00, 01, 104}, - {00, 01, 105}, - {00, 01, 106}, - {00, 01, 107}, - {00, 01, 108}, - {00, 01, 109}, - {00, 01, 110}, - {00, 01, 111}, - }, - }, - { - argTypes: makeRepeatSlice(16, "string"), - jsonArgs: []string{ - `"a"`, - `"b"`, - `"c"`, - `"d"`, - `"e"`, - `"f"`, - `"g"`, - `"h"`, - `"i"`, - `"j"`, - `"k"`, - `"l"`, - `"m"`, - `"n"`, - `"o"`, - `"p"`, - }, - expectedAppArgs: [][]byte{ - {00, 01, 97}, - {00, 01, 98}, - {00, 01, 99}, - {00, 01, 100}, - {00, 01, 101}, - {00, 01, 102}, - {00, 01, 103}, - {00, 01, 104}, - {00, 01, 105}, - {00, 01, 106}, - {00, 01, 107}, - {00, 01, 108}, - {00, 01, 109}, - {00, 01, 110}, - {00, 04, 00, 07, 00, 01, 111, 00, 01, 112}, - }, - }, - } - - for i, test := range tests { - t.Run(fmt.Sprintf("index=%d", i), func(t *testing.T) { - applicationArgs := [][]byte{} - err := ParseArgJSONtoByteSlice(test.argTypes, test.jsonArgs, &applicationArgs) - require.NoError(t, err) - require.Equal(t, test.expectedAppArgs, applicationArgs) - }) - } -} - func TestParseMethodSignature(t *testing.T) { tests := []struct { signature string @@ -1196,6 +1079,56 @@ func TestParseMethodSignature(t *testing.T) { } } +func TestVerifyMethodSignature(t *testing.T) { + tests := []struct { + method string + pass bool + }{ + { + method: "abc(uint64)void", + pass: true, + }, + { + method: "all_special_args(txn,pay,keyreg,acfg,axfer,afrz,appl,account,application,asset)void", + pass: true, + }, + { + method: "abc(uint64)", + pass: false, + }, + { + method: "abc(uint65)void", + pass: false, + }, + { + method: "(uint64)void", + pass: false, + }, + { + method: "abc(uint65,void", + pass: false, + }, + { + method: "abc(uint64))void", + pass: false, + }, + { + method: "abc", + pass: false, + }, + } + + for _, test := range tests { + err := VerifyMethodSignature(test.method) + + if test.pass { + require.NoErrorf(t, err, `Unexpected error from method "%s"`, test.method) + } else { + require.Error(t, err, `Expected an error from method "%s"`, test.method) + } + } +} + func TestInferToSlice(t *testing.T) { var emptySlice []int tests := []struct { diff --git a/abi/json.go b/abi/json.go index 73cf0bc..621d572 100644 --- a/abi/json.go +++ b/abi/json.go @@ -9,9 +9,6 @@ import ( "math/big" ) -// NOTE: discussion about go-algorand-sdk -// https://github.com/algorand/go-algorand/pull/3375#issuecomment-1007536841 - var base32Encoder = base32.StdEncoding.WithPadding(base32.NoPadding) func addressCheckSum(addressBytes [addressByteSize]byte) []byte { diff --git a/abi/type.go b/abi/type.go index 474d7e4..39e6e35 100644 --- a/abi/type.go +++ b/abi/type.go @@ -108,6 +108,9 @@ var ufixedRegexp = regexp.MustCompile(`^ufixed([1-9][\d]*)x([1-9][\d]*)$`) // TypeOf parses an ABI type string. // For example: `TypeOf("(uint64,byte[])")` +// +// Note: this function only supports "basic" ABI types. Reference types and transaction types are +// not supported and will produce an error. func TypeOf(str string) (Type, error) { switch { case strings.HasSuffix(str, "[]"): @@ -447,11 +450,40 @@ func (t Type) ByteLen() (int, error) { // AnyTransactionType is the ABI argument type string for a nonspecific transaction argument const AnyTransactionType = "txn" +// PaymentTransactionType is the ABI argument type string for a payment transaction argument +const PaymentTransactionType = "pay" + +// KeyRegistrationTransactionType is the ABI argument type string for a key registration transaction +// argument +const KeyRegistrationTransactionType = "keyreg" + +// AssetConfigTransactionType is the ABI argument type string for an asset configuration transaction +// argument +const AssetConfigTransactionType = "acfg" + +// AssetTransferTransactionType is the ABI argument type string for an asset transfer transaction +// argument +const AssetTransferTransactionType = "axfer" + +// AssetFreezeTransactionType is the ABI argument type string for an asset freeze transaction +// argument +const AssetFreezeTransactionType = "afrz" + +// ApplicationCallTransactionType is the ABI argument type string for an application call +// transaction argument +const ApplicationCallTransactionType = "appl" + // IsTransactionType checks if a type string represents a transaction type // argument, such as "txn", "pay", "keyreg", etc. func IsTransactionType(s string) bool { switch s { - case AnyTransactionType, "pay", "keyreg", "acfg", "axfer", "afrz", "appl": + case AnyTransactionType, + PaymentTransactionType, + KeyRegistrationTransactionType, + AssetConfigTransactionType, + AssetTransferTransactionType, + AssetFreezeTransactionType, + ApplicationCallTransactionType: return true default: return false