Skip to content

Commit d3bbe62

Browse files
authored
Add ABI support to goal (#3088)
* add abi encoding * accord with go-algorand code format * minor modification * add partition test * resolve review, need more testcase rewrite * move from cmd/goal to data, resolve review * rewrite is-dynamic, dynamic array * update dynamic array 0 element support, test needed * minor * minor * minor * add more testcase for abi encode/decode * update comments in encoding test * minor * attempt to split abi.go into 2 files * separate abi files to smaller files * resolve reviews, work on random gen tuple value encode/decode * add random tuple test * remove math-rand, use crypto-rand * minor * minor * some change requested from community * fix for 1 corner case * resolve review comments * resolve review comments * minor * minor * update encode slot capacity * minor * resolve reviews * minor update on bool bytelen calculate * update encode/decode from types * random test remain to be modified * testing variable renaming, encode int support (u)int types * update test scripts and remove value struct * follow golint * partly resolving comments * whoops uint encoding update * update int decode to primitive types method * go fmt * update parseAppArg to accept abi input (attempt) * need to check cmdline arg validity * update unmarshal from JSON in ABI type * unmarshal from json for ABI type * update ABI type unmarshal values from JSON bytes * update ABI methods for string/array/address * update unmarshal from JSON in abi * fix for error in ufixed json unmarshal * fix * update on method sub command * minor * probably better separate abi json to a single file * i just want to add a required flag plz... * minor fix on interface from json * consider some rough test cases * minor * add partition test * update static uint test * update marshal/unmarshal json methods for abi * marshal byte array to b64 string * abi json polish * update golangci lint rules * revert golangci config * update method impl * update method signature return type check * minor * copy-paste code from call app cmd * minor * add method flag to txn flags * minor * update changes * minor * moving helper functions to abi * update comments * update method app call * resolve part in abi impl * add oncomplete support * minor * try to use stringarrayvar * minor * update goal return log handing process * go simple * add a line of e2e test for now * update * minor * minor * minor * go fmt * approval/clear prog nil * discard all changes to e2d-app-cross-round, going to write separately e2e test * update e2d tests * check ret valu * use constant * resolve review partly * resolve review on code reformatting * resolve review on code reformatting, use code chunk for datadir and client * go fmt * export tuple type maker * update comments in e2e test * update filter empty string * resolve issues with JSON abi * minor
1 parent 63e16ed commit d3bbe62

11 files changed

+909
-53
lines changed

cmd/goal/application.go

+210-21
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package main
1818

1919
import (
20+
"bytes"
21+
"crypto/sha512"
2022
"encoding/base32"
2123
"encoding/base64"
2224
"encoding/binary"
@@ -28,9 +30,11 @@ import (
2830
"github.com/spf13/cobra"
2931

3032
"github.com/algorand/go-algorand/crypto"
33+
"github.com/algorand/go-algorand/data/abi"
3134
"github.com/algorand/go-algorand/data/basics"
3235
"github.com/algorand/go-algorand/data/transactions"
3336
"github.com/algorand/go-algorand/data/transactions/logic"
37+
"github.com/algorand/go-algorand/libgoal"
3438
"github.com/algorand/go-algorand/protocol"
3539
)
3640

@@ -41,6 +45,9 @@ var (
4145
approvalProgFile string
4246
clearProgFile string
4347

48+
method string
49+
methodArgs []string
50+
4451
approvalProgRawFile string
4552
clearProgRawFile string
4653

@@ -79,9 +86,10 @@ func init() {
7986
appCmd.AddCommand(clearAppCmd)
8087
appCmd.AddCommand(readStateAppCmd)
8188
appCmd.AddCommand(infoAppCmd)
89+
appCmd.AddCommand(methodAppCmd)
8290

8391
appCmd.PersistentFlags().StringVarP(&walletName, "wallet", "w", "", "Set the wallet to be used for the selected operation")
84-
appCmd.PersistentFlags().StringSliceVar(&appArgs, "app-arg", nil, "Args to encode for application transactions (all will be encoded to a byte slice). For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.")
92+
appCmd.PersistentFlags().StringArrayVar(&appArgs, "app-arg", nil, "Args to encode for application transactions (all will be encoded to a byte slice). For ints, use the form 'int:1234'. For raw bytes, use the form 'b64:A=='. For printable strings, use the form 'str:hello'. For addresses, use the form 'addr:XYZ...'.")
8593
appCmd.PersistentFlags().StringSliceVar(&foreignApps, "foreign-app", nil, "Indexes of other apps whose global state is read in this transaction")
8694
appCmd.PersistentFlags().StringSliceVar(&foreignAssets, "foreign-asset", nil, "Indexes of assets whose parameters are read in this transaction")
8795
appCmd.PersistentFlags().StringSliceVar(&appStrAccounts, "app-account", nil, "Accounts that may be accessed from application logic")
@@ -108,6 +116,10 @@ func init() {
108116
deleteAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to send delete transaction from")
109117
readStateAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to fetch state from")
110118
updateAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to send update transaction from")
119+
methodAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to call method from")
120+
121+
methodAppCmd.Flags().StringVar(&method, "method", "", "Method to be called")
122+
methodAppCmd.Flags().StringArrayVar(&methodArgs, "arg", nil, "Args to pass in for calling a method")
111123

112124
// Can't use PersistentFlags on the root because for some reason marking
113125
// a root command as required with MarkPersistentFlagRequired isn't
@@ -120,6 +132,7 @@ func init() {
120132
readStateAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID")
121133
updateAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID")
122134
infoAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID")
135+
methodAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID")
123136

124137
// Add common transaction flags to all txn-generating app commands
125138
addTxnFlags(createAppCmd)
@@ -129,6 +142,7 @@ func init() {
129142
addTxnFlags(optInAppCmd)
130143
addTxnFlags(closeOutAppCmd)
131144
addTxnFlags(clearAppCmd)
145+
addTxnFlags(methodAppCmd)
132146

133147
readStateAppCmd.Flags().BoolVar(&fetchLocal, "local", false, "Fetch account-specific state for this application. `--from` address is required when using this flag")
134148
readStateAppCmd.Flags().BoolVar(&fetchGlobal, "global", false, "Fetch global state for this application.")
@@ -161,6 +175,13 @@ func init() {
161175
readStateAppCmd.MarkFlagRequired("app-id")
162176

163177
infoAppCmd.MarkFlagRequired("app-id")
178+
179+
methodAppCmd.MarkFlagRequired("method") // nolint:errcheck // follow previous required flag format
180+
methodAppCmd.MarkFlagRequired("app-id") // nolint:errcheck
181+
methodAppCmd.MarkFlagRequired("from") // nolint:errcheck
182+
methodAppCmd.Flags().MarkHidden("app-arg") // nolint:errcheck
183+
methodAppCmd.Flags().MarkHidden("app-input") // nolint:errcheck
184+
methodAppCmd.Flags().MarkHidden("i") // nolint:errcheck
164185
}
165186

166187
type appCallArg struct {
@@ -229,6 +250,23 @@ func parseAppArg(arg appCallArg) (rawValue []byte, parseErr error) {
229250
return
230251
}
231252
rawValue = data
253+
case "abi":
254+
typeAndValue := strings.SplitN(arg.Value, ":", 2)
255+
if len(typeAndValue) != 2 {
256+
parseErr = fmt.Errorf("Could not decode abi string (%s): should split abi-type and abi-value with colon", arg.Value)
257+
return
258+
}
259+
abiType, err := abi.TypeOf(typeAndValue[0])
260+
if err != nil {
261+
parseErr = fmt.Errorf("Could not decode abi type string (%s): %v", typeAndValue[0], err)
262+
return
263+
}
264+
value, err := abiType.UnmarshalFromJSON([]byte(typeAndValue[1]))
265+
if err != nil {
266+
parseErr = fmt.Errorf("Could not decode abi value string (%s):%v ", typeAndValue[1], err)
267+
return
268+
}
269+
return abiType.Encode(value)
232270
default:
233271
parseErr = fmt.Errorf("Unknown encoding: %s", arg.Encoding)
234272
}
@@ -266,6 +304,20 @@ func processAppInputFile() (args [][]byte, accounts []string, foreignApps []uint
266304
return parseAppInputs(inputs)
267305
}
268306

307+
// filterEmptyStrings filters out empty string parsed in by StringArrayVar
308+
// this function is added to support abi argument parsing
309+
// since parsing of `appArg` diverted from `StringSliceVar` to `StringArrayVar`
310+
func filterEmptyStrings(strSlice []string) []string {
311+
var newStrSlice []string
312+
313+
for _, str := range strSlice {
314+
if len(str) > 0 {
315+
newStrSlice = append(newStrSlice, str)
316+
}
317+
}
318+
return newStrSlice
319+
}
320+
269321
func getAppInputs() (args [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) {
270322
if (appArgs != nil || appStrAccounts != nil || foreignApps != nil) && appInputFilename != "" {
271323
reportErrorf("Cannot specify both command-line arguments/accounts and JSON input filename")
@@ -275,7 +327,11 @@ func getAppInputs() (args [][]byte, accounts []string, foreignApps []uint64, for
275327
}
276328

277329
var encodedArgs []appCallArg
278-
for _, arg := range appArgs {
330+
331+
// we need to filter out empty strings from appArgs first, caused by change to `StringArrayVar`
332+
newAppArgs := filterEmptyStrings(appArgs)
333+
334+
for _, arg := range newAppArgs {
279335
encodingValue := strings.SplitN(arg, ":", 2)
280336
if len(encodingValue) != 2 {
281337
reportErrorf("all arguments should be of the form 'encoding:value'")
@@ -327,6 +383,12 @@ func mustParseOnCompletion(ocString string) (oc transactions.OnCompletion) {
327383
}
328384
}
329385

386+
func getDataDirAndClient() (dataDir string, client libgoal.Client) {
387+
dataDir = ensureSingleDataDir()
388+
client = ensureFullClient(dataDir)
389+
return
390+
}
391+
330392
func mustParseProgArgs() (approval []byte, clear []byte) {
331393
// Ensure we don't have ambiguous or all empty args
332394
if (approvalProgFile == "") == (approvalProgRawFile == "") {
@@ -357,9 +419,7 @@ var createAppCmd = &cobra.Command{
357419
Long: `Issue a transaction that creates an application`,
358420
Args: validateNoPosArgsFn,
359421
Run: func(cmd *cobra.Command, _ []string) {
360-
361-
dataDir := ensureSingleDataDir()
362-
client := ensureFullClient(dataDir)
422+
dataDir, client := getDataDirAndClient()
363423

364424
// Construct schemas from args
365425
localSchema := basics.StateSchema{
@@ -451,8 +511,7 @@ var updateAppCmd = &cobra.Command{
451511
Long: `Issue a transaction that updates an application's ApprovalProgram and ClearStateProgram`,
452512
Args: validateNoPosArgsFn,
453513
Run: func(cmd *cobra.Command, _ []string) {
454-
dataDir := ensureSingleDataDir()
455-
client := ensureFullClient(dataDir)
514+
dataDir, client := getDataDirAndClient()
456515

457516
// Parse transaction parameters
458517
approvalProg, clearProg := mustParseProgArgs()
@@ -523,8 +582,7 @@ var optInAppCmd = &cobra.Command{
523582
Long: `Opt an account in to an application, allocating local state in your account`,
524583
Args: validateNoPosArgsFn,
525584
Run: func(cmd *cobra.Command, _ []string) {
526-
dataDir := ensureSingleDataDir()
527-
client := ensureFullClient(dataDir)
585+
dataDir, client := getDataDirAndClient()
528586

529587
// Parse transaction parameters
530588
appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs()
@@ -594,8 +652,7 @@ var closeOutAppCmd = &cobra.Command{
594652
Long: `Close an account out of an application, removing local state from your account. The application must still exist. If it doesn't, use 'goal app clear'.`,
595653
Args: validateNoPosArgsFn,
596654
Run: func(cmd *cobra.Command, _ []string) {
597-
dataDir := ensureSingleDataDir()
598-
client := ensureFullClient(dataDir)
655+
dataDir, client := getDataDirAndClient()
599656

600657
// Parse transaction parameters
601658
appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs()
@@ -665,8 +722,7 @@ var clearAppCmd = &cobra.Command{
665722
Long: `Remove any local state from your account associated with an application. The application does not need to exist anymore.`,
666723
Args: validateNoPosArgsFn,
667724
Run: func(cmd *cobra.Command, _ []string) {
668-
dataDir := ensureSingleDataDir()
669-
client := ensureFullClient(dataDir)
725+
dataDir, client := getDataDirAndClient()
670726

671727
// Parse transaction parameters
672728
appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs()
@@ -736,8 +792,7 @@ var callAppCmd = &cobra.Command{
736792
Long: `Call an application, invoking application-specific functionality`,
737793
Args: validateNoPosArgsFn,
738794
Run: func(cmd *cobra.Command, _ []string) {
739-
dataDir := ensureSingleDataDir()
740-
client := ensureFullClient(dataDir)
795+
dataDir, client := getDataDirAndClient()
741796

742797
// Parse transaction parameters
743798
appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs()
@@ -807,8 +862,7 @@ var deleteAppCmd = &cobra.Command{
807862
Long: `Delete an application, removing the global state and other application parameters from the creator's account`,
808863
Args: validateNoPosArgsFn,
809864
Run: func(cmd *cobra.Command, _ []string) {
810-
dataDir := ensureSingleDataDir()
811-
client := ensureFullClient(dataDir)
865+
dataDir, client := getDataDirAndClient()
812866

813867
// Parse transaction parameters
814868
appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs()
@@ -879,8 +933,7 @@ var readStateAppCmd = &cobra.Command{
879933
Long: `Read global or local (account-specific) state for an application`,
880934
Args: validateNoPosArgsFn,
881935
Run: func(cmd *cobra.Command, _ []string) {
882-
dataDir := ensureSingleDataDir()
883-
client := ensureFullClient(dataDir)
936+
_, client := getDataDirAndClient()
884937

885938
// Ensure exactly one of --local or --global is specified
886939
if fetchLocal == fetchGlobal {
@@ -961,8 +1014,7 @@ var infoAppCmd = &cobra.Command{
9611014
Long: `Look up application information stored on the network, such as program hash.`,
9621015
Args: validateNoPosArgsFn,
9631016
Run: func(cmd *cobra.Command, _ []string) {
964-
dataDir := ensureSingleDataDir()
965-
client := ensureFullClient(dataDir)
1017+
_, client := getDataDirAndClient()
9661018

9671019
meta, err := client.ApplicationInformation(appIdx)
9681020
if err != nil {
@@ -995,3 +1047,140 @@ var infoAppCmd = &cobra.Command{
9951047
}
9961048
},
9971049
}
1050+
1051+
var methodAppCmd = &cobra.Command{
1052+
Use: "method",
1053+
Short: "Invoke a method",
1054+
Long: `Invoke a method in an App (stateful contract) with an application call transaction`,
1055+
Args: validateNoPosArgsFn,
1056+
Run: func(cmd *cobra.Command, args []string) {
1057+
dataDir, client := getDataDirAndClient()
1058+
1059+
// Parse transaction parameters
1060+
appArgsParsed, appAccounts, foreignApps, foreignAssets := getAppInputs()
1061+
if len(appArgsParsed) > 0 {
1062+
reportErrorf("in goal app method: --arg and --app-arg are mutually exclusive, do not use --app-arg")
1063+
}
1064+
1065+
onCompletion := mustParseOnCompletion(createOnCompletion)
1066+
1067+
if appIdx == 0 {
1068+
reportErrorf("app id == 0, goal app create not supported in goal app method")
1069+
}
1070+
1071+
var approvalProg, clearProg []byte
1072+
if onCompletion == transactions.UpdateApplicationOC {
1073+
approvalProg, clearProg = mustParseProgArgs()
1074+
}
1075+
1076+
var applicationArgs [][]byte
1077+
1078+
// insert the method selector hash
1079+
hash := sha512.Sum512_256([]byte(method))
1080+
applicationArgs = append(applicationArgs, hash[0:4])
1081+
1082+
// parse down the ABI type from method signature
1083+
argTupleTypeStr, retTypeStr, err := abi.ParseMethodSignature(method)
1084+
if err != nil {
1085+
reportErrorf("cannot parse method signature: %v", err)
1086+
}
1087+
err = abi.ParseArgJSONtoByteSlice(argTupleTypeStr, methodArgs, &applicationArgs)
1088+
if err != nil {
1089+
reportErrorf("cannot parse arguments to ABI encoding: %v", err)
1090+
}
1091+
1092+
tx, err := client.MakeUnsignedApplicationCallTx(
1093+
appIdx, applicationArgs, appAccounts, foreignApps, foreignAssets,
1094+
onCompletion, approvalProg, clearProg, basics.StateSchema{}, basics.StateSchema{}, 0)
1095+
1096+
if err != nil {
1097+
reportErrorf("Cannot create application txn: %v", err)
1098+
}
1099+
1100+
// Fill in note and lease
1101+
tx.Note = parseNoteField(cmd)
1102+
tx.Lease = parseLease(cmd)
1103+
1104+
// Fill in rounds, fee, etc.
1105+
fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds)
1106+
if err != nil {
1107+
reportErrorf("Cannot determine last valid round: %s", err)
1108+
}
1109+
1110+
tx, err = client.FillUnsignedTxTemplate(account, fv, lv, fee, tx)
1111+
if err != nil {
1112+
reportErrorf("Cannot construct transaction: %s", err)
1113+
}
1114+
explicitFee := cmd.Flags().Changed("fee")
1115+
if explicitFee {
1116+
tx.Fee = basics.MicroAlgos{Raw: fee}
1117+
}
1118+
1119+
// Broadcast
1120+
wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true)
1121+
signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx)
1122+
if err != nil {
1123+
reportErrorf(errorSigningTX, err)
1124+
}
1125+
1126+
txid, err := client.BroadcastTransaction(signedTxn)
1127+
if err != nil {
1128+
reportErrorf(errorBroadcastingTX, err)
1129+
}
1130+
1131+
// Report tx details to user
1132+
reportInfof("Issued transaction from account %s, txid %s (fee %d)", tx.Sender, txid, tx.Fee.Raw)
1133+
1134+
if !noWaitAfterSend {
1135+
_, err := waitForCommit(client, txid, lv)
1136+
if err != nil {
1137+
reportErrorf(err.Error())
1138+
}
1139+
1140+
resp, err := client.PendingTransactionInformationV2(txid)
1141+
if err != nil {
1142+
reportErrorf(err.Error())
1143+
}
1144+
1145+
if retTypeStr == "void" {
1146+
return
1147+
}
1148+
1149+
// specify the return hash prefix
1150+
hashRet := sha512.Sum512_256([]byte("return"))
1151+
hashRetPrefix := hashRet[:4]
1152+
1153+
var abiEncodedRet []byte
1154+
foundRet := false
1155+
if resp.Logs != nil {
1156+
for i := len(*resp.Logs) - 1; i >= 0; i-- {
1157+
retLog := (*resp.Logs)[i]
1158+
if bytes.HasPrefix(retLog, hashRetPrefix) {
1159+
abiEncodedRet = retLog[4:]
1160+
foundRet = true
1161+
break
1162+
}
1163+
}
1164+
}
1165+
1166+
if !foundRet {
1167+
reportErrorf("cannot find return log for abi type %s", retTypeStr)
1168+
}
1169+
1170+
retType, err := abi.TypeOf(retTypeStr)
1171+
if err != nil {
1172+
reportErrorf("cannot cast %s to abi type: %v", retTypeStr, err)
1173+
}
1174+
decoded, err := retType.Decode(abiEncodedRet)
1175+
if err != nil {
1176+
reportErrorf("cannot decode return value %v: %v", abiEncodedRet, err)
1177+
}
1178+
1179+
decodedJSON, err := retType.MarshalToJSON(decoded)
1180+
if err != nil {
1181+
reportErrorf("cannot marshal returned bytes %v to JSON: %v", decoded, err)
1182+
}
1183+
fmt.Printf("method %s output: %s", method, string(decodedJSON))
1184+
}
1185+
},
1186+
}

0 commit comments

Comments
 (0)