Skip to content

Commit 29a10da

Browse files
authored
Support reference types in goal app method (#3275)
* Fix method signature parse bug * Support reference types * Review dog fixes * Fix comments
1 parent 5318545 commit 29a10da

File tree

6 files changed

+249
-19
lines changed

6 files changed

+249
-19
lines changed

cmd/goal/application.go

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1074,7 +1074,7 @@ func populateMethodCallTxnArgs(types []string, values []string) ([]transactions.
10741074
}
10751075

10761076
expectedType := types[i]
1077-
if expectedType != "txn" && txn.Txn.Type != protocol.TxType(expectedType) {
1077+
if expectedType != abi.AnyTransactionType && txn.Txn.Type != protocol.TxType(expectedType) {
10781078
return nil, fmt.Errorf("Transaction from %s does not match method argument type. Expected %s, got %s", txFilename, expectedType, txn.Txn.Type)
10791079
}
10801080

@@ -1084,6 +1084,82 @@ func populateMethodCallTxnArgs(types []string, values []string) ([]transactions.
10841084
return loadedTxns, nil
10851085
}
10861086

1087+
// populateMethodCallReferenceArgs parses reference argument types and resolves them to an index
1088+
// into the appropriate foreign array. Their placement will be as compact as possible, which means
1089+
// values will be deduplicated and any value that is the sender or the current app will not be added
1090+
// to the foreign array.
1091+
func populateMethodCallReferenceArgs(sender string, currentApp uint64, types []string, values []string, accounts *[]string, apps *[]uint64, assets *[]uint64) ([]int, error) {
1092+
resolvedIndexes := make([]int, len(types))
1093+
1094+
for i, value := range values {
1095+
var resolved int
1096+
1097+
switch types[i] {
1098+
case abi.AccountReferenceType:
1099+
if value == sender {
1100+
resolved = 0
1101+
} else {
1102+
duplicate := false
1103+
for j, account := range *accounts {
1104+
if value == account {
1105+
resolved = j + 1 // + 1 because 0 is the sender
1106+
duplicate = true
1107+
break
1108+
}
1109+
}
1110+
if !duplicate {
1111+
resolved = len(*accounts) + 1
1112+
*accounts = append(*accounts, value)
1113+
}
1114+
}
1115+
case abi.ApplicationReferenceType:
1116+
appID, err := strconv.ParseUint(value, 10, 64)
1117+
if err != nil {
1118+
return nil, fmt.Errorf("Unable to parse application ID '%s': %s", value, err)
1119+
}
1120+
if appID == currentApp {
1121+
resolved = 0
1122+
} else {
1123+
duplicate := false
1124+
for j, app := range *apps {
1125+
if appID == app {
1126+
resolved = j + 1 // + 1 because 0 is the current app
1127+
duplicate = true
1128+
break
1129+
}
1130+
}
1131+
if !duplicate {
1132+
resolved = len(*apps) + 1
1133+
*apps = append(*apps, appID)
1134+
}
1135+
}
1136+
case abi.AssetReferenceType:
1137+
assetID, err := strconv.ParseUint(value, 10, 64)
1138+
if err != nil {
1139+
return nil, fmt.Errorf("Unable to parse asset ID '%s': %s", value, err)
1140+
}
1141+
duplicate := false
1142+
for j, asset := range *assets {
1143+
if assetID == asset {
1144+
resolved = j
1145+
duplicate = true
1146+
break
1147+
}
1148+
}
1149+
if !duplicate {
1150+
resolved = len(*assets)
1151+
*assets = append(*assets, assetID)
1152+
}
1153+
default:
1154+
return nil, fmt.Errorf("Unknown reference type: %s", types[i])
1155+
}
1156+
1157+
resolvedIndexes[i] = resolved
1158+
}
1159+
1160+
return resolvedIndexes, nil
1161+
}
1162+
10871163
var methodAppCmd = &cobra.Command{
10881164
Use: "method",
10891165
Short: "Invoke a method",
@@ -1138,17 +1214,37 @@ var methodAppCmd = &cobra.Command{
11381214
var txnArgValues []string
11391215
var basicArgTypes []string
11401216
var basicArgValues []string
1217+
var refArgTypes []string
1218+
var refArgValues []string
1219+
refArgIndexToBasicArgIndex := make(map[int]int)
11411220
for i, argType := range argTypes {
11421221
argValue := methodArgs[i]
11431222
if abi.IsTransactionType(argType) {
11441223
txnArgTypes = append(txnArgTypes, argType)
11451224
txnArgValues = append(txnArgValues, argValue)
11461225
} else {
1226+
if abi.IsReferenceType(argType) {
1227+
refArgIndexToBasicArgIndex[len(refArgTypes)] = len(basicArgTypes)
1228+
refArgTypes = append(refArgTypes, argType)
1229+
refArgValues = append(refArgValues, argValue)
1230+
// treat the reference as a uint8 for encoding purposes
1231+
argType = "uint8"
1232+
}
11471233
basicArgTypes = append(basicArgTypes, argType)
11481234
basicArgValues = append(basicArgValues, argValue)
11491235
}
11501236
}
11511237

1238+
refArgsResolved, err := populateMethodCallReferenceArgs(account, appIdx, refArgTypes, refArgValues, &appAccounts, &foreignApps, &foreignAssets)
1239+
if err != nil {
1240+
reportErrorf("error populating reference arguments: %v", err)
1241+
}
1242+
for i, resolved := range refArgsResolved {
1243+
basicArgIndex := refArgIndexToBasicArgIndex[i]
1244+
// use the foreign array index as the encoded argument value
1245+
basicArgValues[basicArgIndex] = strconv.Itoa(resolved)
1246+
}
1247+
11521248
err = abi.ParseArgJSONtoByteSlice(basicArgTypes, basicArgValues, &applicationArgs)
11531249
if err != nil {
11541250
reportErrorf("cannot parse arguments to ABI encoding: %v", err)

data/abi/abi_encode.go

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -534,10 +534,9 @@ func ParseMethodSignature(methodSig string) (name string, argTypes []string, ret
534534
argsEnd := -1
535535
depth := 0
536536
for index, char := range methodSig {
537-
switch char {
538-
case '(':
537+
if char == '(' {
539538
depth++
540-
case ')':
539+
} else if char == ')' {
541540
if depth == 0 {
542541
err = fmt.Errorf("Unpaired parenthesis in method signature: %s", methodSig)
543542
return

data/abi/abi_encode_test.go

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1001,3 +1001,55 @@ func TestRandomABIEncodeDecodeRoundTrip(t *testing.T) {
10011001
addTupleRandomValues(t, Tuple, &testValuePool)
10021002
categorySelfRoundTripTest(t, testValuePool[Tuple])
10031003
}
1004+
1005+
func TestParseMethodSignature(t *testing.T) {
1006+
partitiontest.PartitionTest(t)
1007+
1008+
tests := []struct {
1009+
signature string
1010+
name string
1011+
argTypes []string
1012+
returnType string
1013+
}{
1014+
{
1015+
signature: "add(uint8,uint16,pay,account,txn)uint32",
1016+
name: "add",
1017+
argTypes: []string{"uint8", "uint16", "pay", "account", "txn"},
1018+
returnType: "uint32",
1019+
},
1020+
{
1021+
signature: "nothing()void",
1022+
name: "nothing",
1023+
argTypes: []string{},
1024+
returnType: "void",
1025+
},
1026+
{
1027+
signature: "tupleArgs((uint8,uint128),account,(string,(bool,bool)))bool",
1028+
name: "tupleArgs",
1029+
argTypes: []string{"(uint8,uint128)", "account", "(string,(bool,bool))"},
1030+
returnType: "bool",
1031+
},
1032+
{
1033+
signature: "tupleReturn(uint64)(bool,bool,bool)",
1034+
name: "tupleReturn",
1035+
argTypes: []string{"uint64"},
1036+
returnType: "(bool,bool,bool)",
1037+
},
1038+
{
1039+
signature: "tupleArgsAndReturn((uint8,uint128),account,(string,(bool,bool)))(bool,bool,bool)",
1040+
name: "tupleArgsAndReturn",
1041+
argTypes: []string{"(uint8,uint128)", "account", "(string,(bool,bool))"},
1042+
returnType: "(bool,bool,bool)",
1043+
},
1044+
}
1045+
1046+
for _, test := range tests {
1047+
t.Run(test.signature, func(t *testing.T) {
1048+
name, argTypes, returnType, err := ParseMethodSignature(test.signature)
1049+
require.NoError(t, err)
1050+
require.Equal(t, test.name, name)
1051+
require.Equal(t, test.argTypes, argTypes)
1052+
require.Equal(t, test.returnType, returnType)
1053+
})
1054+
}
1055+
}

data/abi/abi_type.go

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -458,11 +458,34 @@ func (t Type) ByteLen() (int, error) {
458458
}
459459
}
460460

461+
// AnyTransactionType is the ABI argument type string for a nonspecific transaction argument
462+
const AnyTransactionType = "txn"
463+
461464
// IsTransactionType checks if a type string represents a transaction type
462465
// argument, such as "txn", "pay", "keyreg", etc.
463466
func IsTransactionType(s string) bool {
464467
switch s {
465-
case "txn", "pay", "keyreg", "acfg", "axfer", "afrz", "appl":
468+
case AnyTransactionType, "pay", "keyreg", "acfg", "axfer", "afrz", "appl":
469+
return true
470+
default:
471+
return false
472+
}
473+
}
474+
475+
// AccountReferenceType is the ABI argument type string for account references
476+
const AccountReferenceType = "account"
477+
478+
// AssetReferenceType is the ABI argument type string for asset references
479+
const AssetReferenceType = "asset"
480+
481+
// ApplicationReferenceType is the ABI argument type string for application references
482+
const ApplicationReferenceType = "application"
483+
484+
// IsReferenceType checks if a type string represents a reference type argument,
485+
// such as "account", "asset", or "application".
486+
func IsReferenceType(s string) bool {
487+
switch s {
488+
case AccountReferenceType, AssetReferenceType, ApplicationReferenceType:
466489
return true
467490
default:
468491
return false

test/scripts/e2e_subs/e2e-app-abi-method.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,14 @@ if [[ $RES != *"${EXPECTED}"* ]]; then
6262
false
6363
fi
6464

65+
# Foreign reference test
66+
RES=$(${gcmd} app method --method "referenceTest(account,application,account,asset,account,asset,asset,application,application)uint8[9]" --arg KGTOR3F3Q74JP4LB5M3SOCSJ4BOPOKZ2GPSLMLLGCWYWRXZJNN4LYQJXXU --arg $APPID --arg $ACCOUNT --arg 10 --arg KGTOR3F3Q74JP4LB5M3SOCSJ4BOPOKZ2GPSLMLLGCWYWRXZJNN4LYQJXXU --arg 11 --arg 10 --arg 20 --arg 21 --app-account 2R5LMPTYLVMWYEG4RPI26PJAM7ARTGUB7LZSONQPGLUWTPOP6LQCJTQZVE --foreign-app 21 --foreign-asset 10 --app-id $APPID --from $ACCOUNT 2>&1 || true)
67+
EXPECTED="method referenceTest(account,application,account,asset,account,asset,asset,application,application)uint8[9] succeeded with output: [2,0,2,0,2,1,0,1,0]"
68+
if [[ $RES != *"${EXPECTED}"* ]]; then
69+
date '+app-abi-method-test FAIL the method call to referenceTest(account,application,account,asset,account,asset,asset,application,application)uint8[9] should not fail %Y%m%d_%H%M%S'
70+
false
71+
fi
72+
6573
# Close out
6674
RES=$(${gcmd} app method --method "closeOut()string" --on-completion closeout --app-id $APPID --from $ACCOUNT 2>&1 || true)
6775
EXPECTED="method closeOut()string succeeded with output: \"goodbye Algorand Fan\""

0 commit comments

Comments
 (0)