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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -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.
14 changes: 14 additions & 0 deletions abi/doc.go
Original file line number Diff line number Diff line change
@@ -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
79 changes: 6 additions & 73 deletions abi/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package abi

import (
"encoding/binary"
"encoding/json"
"fmt"
"math/big"
"reflect"
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
}
}

Expand Down
167 changes: 50 additions & 117 deletions abi/encode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package abi
import (
"crypto/rand"
"encoding/binary"
"fmt"
"math/big"
"testing"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down
3 changes: 0 additions & 3 deletions abi/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
34 changes: 33 additions & 1 deletion abi/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -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, "[]"):
Expand Down Expand Up @@ -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
Expand Down