-
Notifications
You must be signed in to change notification settings - Fork 524
Add ABI support to goal #3088
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add ABI support to goal #3088
Changes from all commits
42a56b4
ab9104e
80c1975
f5c0f21
bdd3c10
3f6d738
badcc28
a613273
bf4e035
1e8f62b
c570d80
500ad45
7d563f1
2f3fe42
c87719c
2dd6028
93abb82
c23cad3
ba68ea9
438ccd2
825e98d
43ff514
c757de5
515ef05
da39a1f
1ce16fd
023e10d
1aaf43d
5b60ae6
74f7550
2d0b85c
82555b8
87b9d75
a426d78
8aa055e
aba8514
93a57c8
7f7a49e
33ce8ad
388a50d
6c607bf
02163cd
83c46ba
54f5d3b
e3eccd3
1122a10
4338ef8
19d48a8
4ee6cda
fccf3b7
2943164
d08f5e2
6592d7e
a08025d
47661a3
ba2e4d6
1a4ef0b
081108a
e4dd7cd
5d3e521
ca9c32f
abd37b2
2288a23
9e17bfe
55015ff
74b2525
52ce63d
80d3c16
aa16911
0ad51a1
a3ae50b
c1d95ec
3c1cde8
0b8252c
7bcdbae
5f1355c
e904eb3
6a97d23
e9788a4
c576ea4
88ceeed
1905ac5
3e746ee
e0d5983
9b8735c
1c413cc
687637b
23f4e80
a78cb70
e2019ea
196bbaa
aa1efdc
baa5599
fc0df15
5175e33
01c0b21
79bff00
2d06d9f
3267ff1
435cca9
d1e8ead
fac68f3
78ed7ac
89f3f3a
34fc96a
310180d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -17,6 +17,8 @@ | |
| package main | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "crypto/sha512" | ||
| "encoding/base32" | ||
| "encoding/base64" | ||
| "encoding/binary" | ||
|
|
@@ -28,9 +30,11 @@ import ( | |
| "github.com/spf13/cobra" | ||
|
|
||
| "github.com/algorand/go-algorand/crypto" | ||
| "github.com/algorand/go-algorand/data/abi" | ||
| "github.com/algorand/go-algorand/data/basics" | ||
| "github.com/algorand/go-algorand/data/transactions" | ||
| "github.com/algorand/go-algorand/data/transactions/logic" | ||
| "github.com/algorand/go-algorand/libgoal" | ||
| "github.com/algorand/go-algorand/protocol" | ||
| ) | ||
|
|
||
|
|
@@ -41,6 +45,9 @@ var ( | |
| approvalProgFile string | ||
| clearProgFile string | ||
|
|
||
| method string | ||
| methodArgs []string | ||
|
|
||
| approvalProgRawFile string | ||
| clearProgRawFile string | ||
|
|
||
|
|
@@ -79,9 +86,10 @@ func init() { | |
| appCmd.AddCommand(clearAppCmd) | ||
| appCmd.AddCommand(readStateAppCmd) | ||
| appCmd.AddCommand(infoAppCmd) | ||
| appCmd.AddCommand(methodAppCmd) | ||
|
|
||
| appCmd.PersistentFlags().StringVarP(&walletName, "wallet", "w", "", "Set the wallet to be used for the selected operation") | ||
| 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...'.") | ||
| 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...'.") | ||
| appCmd.PersistentFlags().StringSliceVar(&foreignApps, "foreign-app", nil, "Indexes of other apps whose global state is read in this transaction") | ||
| appCmd.PersistentFlags().StringSliceVar(&foreignAssets, "foreign-asset", nil, "Indexes of assets whose parameters are read in this transaction") | ||
| appCmd.PersistentFlags().StringSliceVar(&appStrAccounts, "app-account", nil, "Accounts that may be accessed from application logic") | ||
|
|
@@ -108,6 +116,10 @@ func init() { | |
| deleteAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to send delete transaction from") | ||
| readStateAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to fetch state from") | ||
| updateAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to send update transaction from") | ||
| methodAppCmd.Flags().StringVarP(&account, "from", "f", "", "Account to call method from") | ||
|
|
||
| methodAppCmd.Flags().StringVar(&method, "method", "", "Method to be called") | ||
| methodAppCmd.Flags().StringArrayVar(&methodArgs, "arg", nil, "Args to pass in for calling a method") | ||
|
|
||
| // Can't use PersistentFlags on the root because for some reason marking | ||
| // a root command as required with MarkPersistentFlagRequired isn't | ||
|
|
@@ -120,6 +132,7 @@ func init() { | |
| readStateAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") | ||
| updateAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") | ||
| infoAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") | ||
| methodAppCmd.Flags().Uint64Var(&appIdx, "app-id", 0, "Application ID") | ||
|
|
||
| // Add common transaction flags to all txn-generating app commands | ||
| addTxnFlags(createAppCmd) | ||
|
|
@@ -129,6 +142,7 @@ func init() { | |
| addTxnFlags(optInAppCmd) | ||
| addTxnFlags(closeOutAppCmd) | ||
| addTxnFlags(clearAppCmd) | ||
| addTxnFlags(methodAppCmd) | ||
|
|
||
| readStateAppCmd.Flags().BoolVar(&fetchLocal, "local", false, "Fetch account-specific state for this application. `--from` address is required when using this flag") | ||
| readStateAppCmd.Flags().BoolVar(&fetchGlobal, "global", false, "Fetch global state for this application.") | ||
|
|
@@ -161,6 +175,13 @@ func init() { | |
| readStateAppCmd.MarkFlagRequired("app-id") | ||
|
|
||
| infoAppCmd.MarkFlagRequired("app-id") | ||
|
|
||
| methodAppCmd.MarkFlagRequired("method") // nolint:errcheck // follow previous required flag format | ||
| methodAppCmd.MarkFlagRequired("app-id") // nolint:errcheck | ||
| methodAppCmd.MarkFlagRequired("from") // nolint:errcheck | ||
| methodAppCmd.Flags().MarkHidden("app-arg") // nolint:errcheck | ||
| methodAppCmd.Flags().MarkHidden("app-input") // nolint:errcheck | ||
| methodAppCmd.Flags().MarkHidden("i") // nolint:errcheck | ||
| } | ||
|
|
||
| type appCallArg struct { | ||
|
|
@@ -229,6 +250,23 @@ func parseAppArg(arg appCallArg) (rawValue []byte, parseErr error) { | |
| return | ||
| } | ||
| rawValue = data | ||
| case "abi": | ||
| typeAndValue := strings.SplitN(arg.Value, ":", 2) | ||
| if len(typeAndValue) != 2 { | ||
| parseErr = fmt.Errorf("Could not decode abi string (%s): should split abi-type and abi-value with colon", arg.Value) | ||
| return | ||
| } | ||
| abiType, err := abi.TypeOf(typeAndValue[0]) | ||
| if err != nil { | ||
| parseErr = fmt.Errorf("Could not decode abi type string (%s): %v", typeAndValue[0], err) | ||
| return | ||
| } | ||
| value, err := abiType.UnmarshalFromJSON([]byte(typeAndValue[1])) | ||
| if err != nil { | ||
| parseErr = fmt.Errorf("Could not decode abi value string (%s):%v ", typeAndValue[1], err) | ||
| return | ||
| } | ||
| return abiType.Encode(value) | ||
| default: | ||
| parseErr = fmt.Errorf("Unknown encoding: %s", arg.Encoding) | ||
| } | ||
|
|
@@ -266,6 +304,20 @@ func processAppInputFile() (args [][]byte, accounts []string, foreignApps []uint | |
| return parseAppInputs(inputs) | ||
| } | ||
|
|
||
| // filterEmptyStrings filters out empty string parsed in by StringArrayVar | ||
| // this function is added to support abi argument parsing | ||
| // since parsing of `appArg` diverted from `StringSliceVar` to `StringArrayVar` | ||
| func filterEmptyStrings(strSlice []string) []string { | ||
| var newStrSlice []string | ||
|
|
||
| for _, str := range strSlice { | ||
| if len(str) > 0 { | ||
| newStrSlice = append(newStrSlice, str) | ||
| } | ||
| } | ||
| return newStrSlice | ||
| } | ||
|
|
||
| func getAppInputs() (args [][]byte, accounts []string, foreignApps []uint64, foreignAssets []uint64) { | ||
| if (appArgs != nil || appStrAccounts != nil || foreignApps != nil) && appInputFilename != "" { | ||
| 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 | |
| } | ||
|
|
||
| var encodedArgs []appCallArg | ||
| for _, arg := range appArgs { | ||
|
|
||
| // we need to filter out empty strings from appArgs first, caused by change to `StringArrayVar` | ||
| newAppArgs := filterEmptyStrings(appArgs) | ||
|
|
||
| for _, arg := range newAppArgs { | ||
| encodingValue := strings.SplitN(arg, ":", 2) | ||
| if len(encodingValue) != 2 { | ||
| reportErrorf("all arguments should be of the form 'encoding:value'") | ||
|
|
@@ -327,6 +383,12 @@ func mustParseOnCompletion(ocString string) (oc transactions.OnCompletion) { | |
| } | ||
| } | ||
|
|
||
| func getDataDirAndClient() (dataDir string, client libgoal.Client) { | ||
| dataDir = ensureSingleDataDir() | ||
| client = ensureFullClient(dataDir) | ||
| return | ||
| } | ||
|
|
||
| func mustParseProgArgs() (approval []byte, clear []byte) { | ||
| // Ensure we don't have ambiguous or all empty args | ||
| if (approvalProgFile == "") == (approvalProgRawFile == "") { | ||
|
|
@@ -357,9 +419,7 @@ var createAppCmd = &cobra.Command{ | |
| Long: `Issue a transaction that creates an application`, | ||
| Args: validateNoPosArgsFn, | ||
| Run: func(cmd *cobra.Command, _ []string) { | ||
|
|
||
| dataDir := ensureSingleDataDir() | ||
| client := ensureFullClient(dataDir) | ||
| dataDir, client := getDataDirAndClient() | ||
|
|
||
| // Construct schemas from args | ||
| localSchema := basics.StateSchema{ | ||
|
|
@@ -451,8 +511,7 @@ var updateAppCmd = &cobra.Command{ | |
| Long: `Issue a transaction that updates an application's ApprovalProgram and ClearStateProgram`, | ||
| Args: validateNoPosArgsFn, | ||
| Run: func(cmd *cobra.Command, _ []string) { | ||
| dataDir := ensureSingleDataDir() | ||
| client := ensureFullClient(dataDir) | ||
| dataDir, client := getDataDirAndClient() | ||
|
|
||
| // Parse transaction parameters | ||
| approvalProg, clearProg := mustParseProgArgs() | ||
|
|
@@ -523,8 +582,7 @@ var optInAppCmd = &cobra.Command{ | |
| Long: `Opt an account in to an application, allocating local state in your account`, | ||
| Args: validateNoPosArgsFn, | ||
| Run: func(cmd *cobra.Command, _ []string) { | ||
| dataDir := ensureSingleDataDir() | ||
| client := ensureFullClient(dataDir) | ||
| dataDir, client := getDataDirAndClient() | ||
|
|
||
| // Parse transaction parameters | ||
| appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() | ||
|
|
@@ -594,8 +652,7 @@ var closeOutAppCmd = &cobra.Command{ | |
| 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'.`, | ||
| Args: validateNoPosArgsFn, | ||
| Run: func(cmd *cobra.Command, _ []string) { | ||
| dataDir := ensureSingleDataDir() | ||
| client := ensureFullClient(dataDir) | ||
| dataDir, client := getDataDirAndClient() | ||
|
|
||
| // Parse transaction parameters | ||
| appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() | ||
|
|
@@ -665,8 +722,7 @@ var clearAppCmd = &cobra.Command{ | |
| Long: `Remove any local state from your account associated with an application. The application does not need to exist anymore.`, | ||
| Args: validateNoPosArgsFn, | ||
| Run: func(cmd *cobra.Command, _ []string) { | ||
| dataDir := ensureSingleDataDir() | ||
| client := ensureFullClient(dataDir) | ||
| dataDir, client := getDataDirAndClient() | ||
|
|
||
| // Parse transaction parameters | ||
| appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() | ||
|
|
@@ -736,8 +792,7 @@ var callAppCmd = &cobra.Command{ | |
| Long: `Call an application, invoking application-specific functionality`, | ||
| Args: validateNoPosArgsFn, | ||
| Run: func(cmd *cobra.Command, _ []string) { | ||
| dataDir := ensureSingleDataDir() | ||
| client := ensureFullClient(dataDir) | ||
| dataDir, client := getDataDirAndClient() | ||
|
|
||
| // Parse transaction parameters | ||
| appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() | ||
|
|
@@ -807,8 +862,7 @@ var deleteAppCmd = &cobra.Command{ | |
| Long: `Delete an application, removing the global state and other application parameters from the creator's account`, | ||
| Args: validateNoPosArgsFn, | ||
| Run: func(cmd *cobra.Command, _ []string) { | ||
| dataDir := ensureSingleDataDir() | ||
| client := ensureFullClient(dataDir) | ||
| dataDir, client := getDataDirAndClient() | ||
|
|
||
| // Parse transaction parameters | ||
| appArgs, appAccounts, foreignApps, foreignAssets := getAppInputs() | ||
|
|
@@ -879,8 +933,7 @@ var readStateAppCmd = &cobra.Command{ | |
| Long: `Read global or local (account-specific) state for an application`, | ||
| Args: validateNoPosArgsFn, | ||
| Run: func(cmd *cobra.Command, _ []string) { | ||
| dataDir := ensureSingleDataDir() | ||
| client := ensureFullClient(dataDir) | ||
| _, client := getDataDirAndClient() | ||
|
|
||
| // Ensure exactly one of --local or --global is specified | ||
| if fetchLocal == fetchGlobal { | ||
|
|
@@ -961,8 +1014,7 @@ var infoAppCmd = &cobra.Command{ | |
| Long: `Look up application information stored on the network, such as program hash.`, | ||
| Args: validateNoPosArgsFn, | ||
| Run: func(cmd *cobra.Command, _ []string) { | ||
| dataDir := ensureSingleDataDir() | ||
| client := ensureFullClient(dataDir) | ||
| _, client := getDataDirAndClient() | ||
|
|
||
| meta, err := client.ApplicationInformation(appIdx) | ||
| if err != nil { | ||
|
|
@@ -995,3 +1047,140 @@ var infoAppCmd = &cobra.Command{ | |
| } | ||
| }, | ||
| } | ||
|
|
||
| var methodAppCmd = &cobra.Command{ | ||
| Use: "method", | ||
| Short: "Invoke a method", | ||
| Long: `Invoke a method in an App (stateful contract) with an application call transaction`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not proposing any changes here, but view this as a learning opportunity for me about Algorand's conventions. Cleary the convention in the file is for this field to use raw string literals. I guess that makes sense, because in theory the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have specific thoughts or idea about it, just following the previous conventions. I just noticed the nearest editing for adding method/updating description is 16 months ago... and I suppose they might want to do something with string literal. |
||
| Args: validateNoPosArgsFn, | ||
| Run: func(cmd *cobra.Command, args []string) { | ||
| dataDir, client := getDataDirAndClient() | ||
|
|
||
| // Parse transaction parameters | ||
| appArgsParsed, appAccounts, foreignApps, foreignAssets := getAppInputs() | ||
| if len(appArgsParsed) > 0 { | ||
| reportErrorf("in goal app method: --arg and --app-arg are mutually exclusive, do not use --app-arg") | ||
| } | ||
|
|
||
| onCompletion := mustParseOnCompletion(createOnCompletion) | ||
|
|
||
| if appIdx == 0 { | ||
| reportErrorf("app id == 0, goal app create not supported in goal app method") | ||
| } | ||
|
|
||
| var approvalProg, clearProg []byte | ||
| if onCompletion == transactions.UpdateApplicationOC { | ||
| approvalProg, clearProg = mustParseProgArgs() | ||
| } | ||
|
|
||
| var applicationArgs [][]byte | ||
|
|
||
| // insert the method selector hash | ||
| hash := sha512.Sum512_256([]byte(method)) | ||
| applicationArgs = append(applicationArgs, hash[0:4]) | ||
|
|
||
| // parse down the ABI type from method signature | ||
| argTupleTypeStr, retTypeStr, err := abi.ParseMethodSignature(method) | ||
| if err != nil { | ||
| reportErrorf("cannot parse method signature: %v", err) | ||
| } | ||
| err = abi.ParseArgJSONtoByteSlice(argTupleTypeStr, methodArgs, &applicationArgs) | ||
| if err != nil { | ||
| reportErrorf("cannot parse arguments to ABI encoding: %v", err) | ||
| } | ||
|
|
||
| tx, err := client.MakeUnsignedApplicationCallTx( | ||
| appIdx, applicationArgs, appAccounts, foreignApps, foreignAssets, | ||
| onCompletion, approvalProg, clearProg, basics.StateSchema{}, basics.StateSchema{}, 0) | ||
|
|
||
| if err != nil { | ||
| reportErrorf("Cannot create application txn: %v", err) | ||
| } | ||
|
|
||
| // Fill in note and lease | ||
| tx.Note = parseNoteField(cmd) | ||
| tx.Lease = parseLease(cmd) | ||
|
|
||
| // Fill in rounds, fee, etc. | ||
| fv, lv, err := client.ComputeValidityRounds(firstValid, lastValid, numValidRounds) | ||
| if err != nil { | ||
| reportErrorf("Cannot determine last valid round: %s", err) | ||
| } | ||
|
|
||
| tx, err = client.FillUnsignedTxTemplate(account, fv, lv, fee, tx) | ||
| if err != nil { | ||
| reportErrorf("Cannot construct transaction: %s", err) | ||
| } | ||
| explicitFee := cmd.Flags().Changed("fee") | ||
| if explicitFee { | ||
| tx.Fee = basics.MicroAlgos{Raw: fee} | ||
| } | ||
|
|
||
| // Broadcast | ||
| wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true) | ||
| signedTxn, err := client.SignTransactionWithWallet(wh, pw, tx) | ||
| if err != nil { | ||
| reportErrorf(errorSigningTX, err) | ||
| } | ||
|
|
||
| txid, err := client.BroadcastTransaction(signedTxn) | ||
| if err != nil { | ||
| reportErrorf(errorBroadcastingTX, err) | ||
| } | ||
|
|
||
| // Report tx details to user | ||
| reportInfof("Issued transaction from account %s, txid %s (fee %d)", tx.Sender, txid, tx.Fee.Raw) | ||
|
|
||
| if !noWaitAfterSend { | ||
| _, err := waitForCommit(client, txid, lv) | ||
| if err != nil { | ||
| reportErrorf(err.Error()) | ||
| } | ||
|
|
||
| resp, err := client.PendingTransactionInformationV2(txid) | ||
| if err != nil { | ||
| reportErrorf(err.Error()) | ||
| } | ||
|
|
||
| if retTypeStr == "void" { | ||
| return | ||
| } | ||
|
|
||
| // specify the return hash prefix | ||
| hashRet := sha512.Sum512_256([]byte("return")) | ||
| hashRetPrefix := hashRet[:4] | ||
|
|
||
| var abiEncodedRet []byte | ||
| foundRet := false | ||
| if resp.Logs != nil { | ||
| for i := len(*resp.Logs) - 1; i >= 0; i-- { | ||
| retLog := (*resp.Logs)[i] | ||
| if bytes.HasPrefix(retLog, hashRetPrefix) { | ||
| abiEncodedRet = retLog[4:] | ||
| foundRet = true | ||
| break | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if !foundRet { | ||
| reportErrorf("cannot find return log for abi type %s", retTypeStr) | ||
| } | ||
|
|
||
| retType, err := abi.TypeOf(retTypeStr) | ||
| if err != nil { | ||
| reportErrorf("cannot cast %s to abi type: %v", retTypeStr, err) | ||
| } | ||
| decoded, err := retType.Decode(abiEncodedRet) | ||
| if err != nil { | ||
| reportErrorf("cannot decode return value %v: %v", abiEncodedRet, err) | ||
| } | ||
|
|
||
| decodedJSON, err := retType.MarshalToJSON(decoded) | ||
| if err != nil { | ||
| reportErrorf("cannot marshal returned bytes %v to JSON: %v", decoded, err) | ||
| } | ||
| fmt.Printf("method %s output: %s", method, string(decodedJSON)) | ||
| } | ||
| }, | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.