Skip to content

Commit

Permalink
Support transaction arguments for goal app method (#3233)
Browse files Browse the repository at this point in the history
* Implement transactions as arguments

* Fix indexing and dryrun issue

* Add docstring

* Satisfy review dog

* Fix pointer issue

* Fix group command

* Rename e2e test

* Fix filename variable

* Add e2e test

* Use tab
  • Loading branch information
jasonpaulos authored Nov 22, 2021
1 parent 0b0f97b commit 116c06e
Show file tree
Hide file tree
Showing 7 changed files with 281 additions and 101 deletions.
161 changes: 135 additions & 26 deletions cmd/goal/application.go
Original file line number Diff line number Diff line change
Expand Up @@ -1047,6 +1047,43 @@ var infoAppCmd = &cobra.Command{
},
}

// populateMethodCallTxnArgs parses and loads transactions from the files indicated by the values
// slice. An error will occur if the transaction does not matched the expected type, it has a nonzero
// group ID, or if it is signed by a normal signature or Msig signature (but not Lsig signature)
func populateMethodCallTxnArgs(types []string, values []string) ([]transactions.SignedTxn, error) {
loadedTxns := make([]transactions.SignedTxn, len(values))

for i, txFilename := range values {
data, err := readFile(txFilename)
if err != nil {
return nil, fmt.Errorf(fileReadError, txFilename, err)
}

var txn transactions.SignedTxn
err = protocol.Decode(data, &txn)
if err != nil {
return nil, fmt.Errorf(txDecodeError, txFilename, err)
}

if !txn.Sig.Blank() || !txn.Msig.Blank() {
return nil, fmt.Errorf("Transaction from %s has already been signed", txFilename)
}

if !txn.Txn.Group.IsZero() {
return nil, fmt.Errorf("Transaction from %s already has a group ID: %s", txFilename, txn.Txn.Group)
}

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

loadedTxns[i] = txn
}

return loadedTxns, nil
}

var methodAppCmd = &cobra.Command{
Use: "method",
Short: "Invoke a method",
Expand Down Expand Up @@ -1079,16 +1116,50 @@ var methodAppCmd = &cobra.Command{
applicationArgs = append(applicationArgs, hash[0:4])

// parse down the ABI type from method signature
argTupleTypeStr, retTypeStr, err := abi.ParseMethodSignature(method)
_, argTypes, retTypeStr, err := abi.ParseMethodSignature(method)
if err != nil {
reportErrorf("cannot parse method signature: %v", err)
}
err = abi.ParseArgJSONtoByteSlice(argTupleTypeStr, methodArgs, &applicationArgs)

var retType *abi.Type
if retTypeStr != "void" {
theRetType, err := abi.TypeOf(retTypeStr)
if err != nil {
reportErrorf("cannot cast %s to abi type: %v", retTypeStr, err)
}
retType = &theRetType
}

if len(methodArgs) != len(argTypes) {
reportErrorf("incorrect number of arguments, method expected %d but got %d", len(argTypes), len(methodArgs))
}

var txnArgTypes []string
var txnArgValues []string
var basicArgTypes []string
var basicArgValues []string
for i, argType := range argTypes {
argValue := methodArgs[i]
if abi.IsTransactionType(argType) {
txnArgTypes = append(txnArgTypes, argType)
txnArgValues = append(txnArgValues, argValue)
} else {
basicArgTypes = append(basicArgTypes, argType)
basicArgValues = append(basicArgValues, argValue)
}
}

err = abi.ParseArgJSONtoByteSlice(basicArgTypes, basicArgValues, &applicationArgs)
if err != nil {
reportErrorf("cannot parse arguments to ABI encoding: %v", err)
}

tx, err := client.MakeUnsignedApplicationCallTx(
txnArgs, err := populateMethodCallTxnArgs(txnArgTypes, txnArgValues)
if err != nil {
reportErrorf("error populating transaction arguments: %v", err)
}

appCallTxn, err := client.MakeUnsignedApplicationCallTx(
appIdx, applicationArgs, appAccounts, foreignApps, foreignAssets,
onCompletionEnum, approvalProg, clearProg, basics.StateSchema{}, basics.StateSchema{}, 0)

Expand All @@ -1097,52 +1168,94 @@ var methodAppCmd = &cobra.Command{
}

// Fill in note and lease
tx.Note = parseNoteField(cmd)
tx.Lease = parseLease(cmd)
appCallTxn.Note = parseNoteField(cmd)
appCallTxn.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)
appCallTxn, err = client.FillUnsignedTxTemplate(account, fv, lv, fee, appCallTxn)
if err != nil {
reportErrorf("Cannot construct transaction: %s", err)
}
explicitFee := cmd.Flags().Changed("fee")
if explicitFee {
tx.Fee = basics.MicroAlgos{Raw: fee}
appCallTxn.Fee = basics.MicroAlgos{Raw: fee}
}

// Compile group
var txnGroup []transactions.Transaction
for i := range txnArgs {
txnGroup = append(txnGroup, txnArgs[i].Txn)
}
txnGroup = append(txnGroup, appCallTxn)
if len(txnGroup) > 1 {
// Only if transaction arguments are present, assign group ID
groupID, err := client.GroupID(txnGroup)
if err != nil {
reportErrorf("Cannot assign transaction group ID: %s", err)
}
for i := range txnGroup {
txnGroup[i].Group = groupID
}
}

// Sign transactions
var signedTxnGroup []transactions.SignedTxn
shouldSign := sign || outFilename == ""
for i, unsignedTxn := range txnGroup {
txnFromArgs := transactions.SignedTxn{}
if i < len(txnArgs) {
txnFromArgs = txnArgs[i]
}

if !txnFromArgs.Lsig.Blank() {
signedTxnGroup = append(signedTxnGroup, transactions.SignedTxn{
Lsig: txnFromArgs.Lsig,
AuthAddr: txnFromArgs.AuthAddr,
Txn: unsignedTxn,
})
continue
}

signedTxn, err := createSignedTransaction(client, shouldSign, dataDir, walletName, unsignedTxn, txnFromArgs.AuthAddr)
if err != nil {
reportErrorf(errorSigningTX, err)
}

signedTxnGroup = append(signedTxnGroup, signedTxn)
}

// Output to file
if outFilename != "" {
if dumpForDryrun {
err = writeDryrunReqToFile(client, tx, outFilename)
err = writeDryrunReqToFile(client, signedTxnGroup, outFilename)
} else {
// Write transaction to file
err = writeTxnToFile(client, sign, dataDir, walletName, tx, outFilename)
err = writeSignedTxnsToFile(signedTxnGroup, 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)
err = client.BroadcastTransactionGroup(signedTxnGroup)
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)
reportInfof("Issued %d transaction(s):", len(signedTxnGroup))
// remember the final txid in this variable
var txid string
for _, stxn := range signedTxnGroup {
txid = stxn.Txn.ID().String()
reportInfof("\tIssued transaction from account %s, txid %s (fee %d)", stxn.Txn.Sender, txid, stxn.Txn.Fee.Raw)
}

if !noWaitAfterSend {
_, err := waitForCommit(client, txid, lv)
Expand All @@ -1155,8 +1268,8 @@ var methodAppCmd = &cobra.Command{
reportErrorf(err.Error())
}

if retTypeStr == "void" {
fmt.Printf("method %s succeeded", method)
if retType == nil {
fmt.Printf("method %s succeeded\n", method)
return
}

Expand All @@ -1181,10 +1294,6 @@ var methodAppCmd = &cobra.Command{
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)
Expand All @@ -1194,7 +1303,7 @@ var methodAppCmd = &cobra.Command{
if err != nil {
reportErrorf("cannot marshal returned bytes %v to JSON: %v", decoded, err)
}
fmt.Printf("method %s succeeded with output: %s", method, string(decodedJSON))
fmt.Printf("method %s succeeded with output: %s\n", method, string(decodedJSON))
}
},
}
50 changes: 30 additions & 20 deletions cmd/goal/clerk.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,34 +194,45 @@ func waitForCommit(client libgoal.Client, txid string, transactionLastValidRound
return
}

func createSignedTransaction(client libgoal.Client, signTx bool, dataDir string, walletName string, tx transactions.Transaction) (stxn transactions.SignedTxn, err error) {
func createSignedTransaction(client libgoal.Client, signTx bool, dataDir string, walletName string, tx transactions.Transaction, signer basics.Address) (stxn transactions.SignedTxn, err error) {
if signTx {
// Sign the transaction
wh, pw := ensureWalletHandleMaybePassword(dataDir, walletName, true)
stxn, err = client.SignTransactionWithWallet(wh, pw, tx)
if err != nil {
return
}
} else {
// Wrap in a transactions.SignedTxn with an empty sig.
// This way protocol.Encode will encode the transaction type
stxn, err = transactions.AssembleSignedTxn(tx, crypto.Signature{}, crypto.MultisigSig{})
if err != nil {
return
if signer.IsZero() {
stxn, err = client.SignTransactionWithWallet(wh, pw, tx)
} else {
stxn, err = client.SignTransactionWithWalletAndSigner(wh, pw, signer.String(), tx)
}
return
}

stxn = populateBlankMultisig(client, dataDir, walletName, stxn)
// Wrap in a transactions.SignedTxn with an empty sig.
// This way protocol.Encode will encode the transaction type
stxn, err = transactions.AssembleSignedTxn(tx, crypto.Signature{}, crypto.MultisigSig{})
if err != nil {
return
}

stxn = populateBlankMultisig(client, dataDir, walletName, stxn)
return
}

func writeSignedTxnsToFile(stxns []transactions.SignedTxn, filename string) error {
var outData []byte
for _, stxn := range stxns {
outData = append(outData, protocol.Encode(&stxn)...)
}

return writeFile(filename, outData, 0600)
}

func writeTxnToFile(client libgoal.Client, signTx bool, dataDir string, walletName string, tx transactions.Transaction, filename string) error {
stxn, err := createSignedTransaction(client, signTx, dataDir, walletName, tx)
stxn, err := createSignedTransaction(client, signTx, dataDir, walletName, tx, basics.Address{})
if err != nil {
return err
}
// Write the SignedTxn to the output file
return writeFile(filename, protocol.Encode(&stxn), 0600)
return writeSignedTxnsToFile([]transactions.SignedTxn{stxn}, filename)
}

func getB64Args(args []string) [][]byte {
Expand Down Expand Up @@ -419,7 +430,7 @@ var sendCmd = &cobra.Command{
}
} else {
signTx := sign || (outFilename == "")
stx, err = createSignedTransaction(client, signTx, dataDir, walletName, payment)
stx, err = createSignedTransaction(client, signTx, dataDir, walletName, payment, basics.Address{})
if err != nil {
reportErrorf(errorSigningTX, err)
}
Expand Down Expand Up @@ -854,13 +865,12 @@ var groupCmd = &cobra.Command{
transactionIdx++
}

var outData []byte
for _, stxn := range stxns {
stxn.Txn.Group = crypto.HashObj(group)
outData = append(outData, protocol.Encode(&stxn)...)
groupHash := crypto.HashObj(group)
for i := range stxns {
stxns[i].Txn.Group = groupHash
}

err = writeFile(outFilename, outData, 0600)
err = writeSignedTxnsToFile(stxns, outFilename)
if err != nil {
reportErrorf(fileWriteError, outFilename, err)
}
Expand Down
Loading

0 comments on commit 116c06e

Please sign in to comment.