summaryrefslogtreecommitdiff
path: root/cmd/goal/application.go
diff options
context:
space:
mode:
Diffstat (limited to 'cmd/goal/application.go')
-rw-r--r--cmd/goal/application.go243
1 files changed, 222 insertions, 21 deletions
diff --git a/cmd/goal/application.go b/cmd/goal/application.go
index da142e3e0..2ed7eed2d 100644
--- a/cmd/goal/application.go
+++ b/cmd/goal/application.go
@@ -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,11 @@ 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
}
type appCallArg struct {
@@ -229,6 +248,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 +302,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 +325,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 +381,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 +417,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 +509,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 +580,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 +650,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 +720,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 +790,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 +860,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 +931,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 +1012,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 +1045,154 @@ 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`,
+ 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}
+ }
+
+ if outFilename != "" {
+ if dumpForDryrun {
+ err = writeDryrunReqToFile(client, tx, outFilename)
+ } else {
+ // Write transaction to file
+ err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename)
+ }
+
+ if err != nil {
+ reportErrorf(err.Error())
+ }
+ return
+ }
+
+ // 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\n", method, string(decodedJSON))
+ }
+ },
+}