cli: add await flag for operations with transactions

New --await flag is an option to synchronize on transaction execution
for CLI commands.

Closes #3244

Signed-off-by: Ekaterina Pavlova <ekt@morphbits.io>
This commit is contained in:
Ekaterina Pavlova 2023-12-28 14:58:38 +03:00
parent 4f5e3f363a
commit 0ffa24932b
18 changed files with 383 additions and 39 deletions

View file

@ -328,6 +328,14 @@ func TestNEP11_ND_OwnerOf_BalanceOf_Transfer(t *testing.T) {
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...) e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, nftOwnerAddr, tokenID1) checkBalanceResult(t, nftOwnerAddr, tokenID1)
// check --await flag
tokenID2 := mint(t)
e.In.WriteString(nftOwnerPass + "\r")
e.Run(t, append(cmdTransfer, "--await", "--id", hex.EncodeToString(tokenID2))...)
e.CheckAwaitableTxPersisted(t)
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
checkBalanceResult(t, nftOwnerAddr, tokenID1)
// transfer: good, to NEP-11-Payable contract, with data // transfer: good, to NEP-11-Payable contract, with data
verifyH := deployVerifyContract(t, e) verifyH := deployVerifyContract(t, e)
cmdTransfer = []string{ cmdTransfer = []string{

View file

@ -223,12 +223,19 @@ func TestNEP17Transfer(t *testing.T) {
"neo-go", "wallet", "nep17", "transfer", "neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://" + e.RPC.Addresses()[0], "--rpc-endpoint", "http://" + e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet, "--wallet", testcli.ValidatorWallet,
"--to", address.Uint160ToString(e.Chain.GetNotaryContractScriptHash()),
"--token", "GAS", "--token", "GAS",
"--amount", "1", "--amount", "1",
"--from", testcli.ValidatorAddr,
"--force", "--force",
"[", testcli.ValidatorAddr, strconv.Itoa(int(validTil)), "]"} "--from", testcli.ValidatorAddr}
t.Run("with await", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, append(cmd, "--to", nftOwnerAddr, "--await")...)
e.CheckAwaitableTxPersisted(t)
})
cmd = append(cmd, "--to", address.Uint160ToString(e.Chain.GetNotaryContractScriptHash()),
"[", testcli.ValidatorAddr, strconv.Itoa(int(validTil)), "]")
t.Run("with data", func(t *testing.T) { t.Run("with data", func(t *testing.T) {
e.In.WriteString("one\r") e.In.WriteString("one\r")

View file

@ -32,8 +32,14 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
// DefaultTimeout is the default timeout used for RPC requests. const (
const DefaultTimeout = 10 * time.Second // DefaultTimeout is the default timeout used for RPC requests.
DefaultTimeout = 10 * time.Second
// DefaultAwaitableTimeout is the default timeout used for RPC requests that
// require transaction awaiting. It is set to the approximate time of three
// Neo N3 mainnet blocks accepting.
DefaultAwaitableTimeout = 3 * 15 * time.Second
)
// RPCEndpointFlag is a long flag name for an RPC endpoint. It can be used to // RPCEndpointFlag is a long flag name for an RPC endpoint. It can be used to
// check for flag presence in the context. // check for flag presence in the context.
@ -129,6 +135,9 @@ func GetTimeoutContext(ctx *cli.Context) (context.Context, func()) {
if dur == 0 { if dur == 0 {
dur = DefaultTimeout dur = DefaultTimeout
} }
if !ctx.IsSet("timeout") && ctx.Bool("await") {
dur = DefaultAwaitableTimeout
}
return context.WithTimeout(context.Background(), dur) return context.WithTimeout(context.Background(), dur)
} }

View file

@ -338,7 +338,7 @@ func TestContractDeployWithData(t *testing.T) {
"--config", "testdata/deploy/neo-go.yml", "--config", "testdata/deploy/neo-go.yml",
"--out", nefName, "--manifest", manifestName) "--out", nefName, "--manifest", manifestName)
deployContract := func(t *testing.T, haveData bool, scope string) { deployContract := func(t *testing.T, haveData bool, scope string, await bool) {
e := testcli.NewExecutor(t, true) e := testcli.NewExecutor(t, true)
cmd := []string{ cmd := []string{
"neo-go", "contract", "deploy", "neo-go", "contract", "deploy",
@ -348,6 +348,9 @@ func TestContractDeployWithData(t *testing.T) {
"--force", "--force",
} }
if await {
cmd = append(cmd, "--await")
}
if haveData { if haveData {
cmd = append(cmd, "[", "key1", "12", "key2", "take_me_to_church", "]") cmd = append(cmd, "[", "key1", "12", "key2", "take_me_to_church", "]")
} }
@ -358,8 +361,13 @@ func TestContractDeployWithData(t *testing.T) {
} }
e.In.WriteString(testcli.ValidatorPass + "\r") e.In.WriteString(testcli.ValidatorPass + "\r")
e.Run(t, cmd...) e.Run(t, cmd...)
var tx *transaction.Transaction
if await {
tx, _ = e.CheckAwaitableTxPersisted(t)
} else {
tx, _ = e.CheckTxPersisted(t)
}
tx, _ := e.CheckTxPersisted(t, "Sent invocation transaction ")
require.Equal(t, scope, tx.Signers[0].Scopes.String()) require.Equal(t, scope, tx.Signers[0].Scopes.String())
if !haveData { if !haveData {
return return
@ -396,9 +404,12 @@ func TestContractDeployWithData(t *testing.T) {
require.Equal(t, []byte("take_me_to_church"), res.Stack[0].Value()) require.Equal(t, []byte("take_me_to_church"), res.Stack[0].Value())
} }
deployContract(t, true, "") deployContract(t, true, "", false)
deployContract(t, false, "Global") deployContract(t, false, "Global", false)
deployContract(t, true, "Global") deployContract(t, true, "Global", false)
deployContract(t, false, "", true)
deployContract(t, true, "Global", true)
deployContract(t, true, "", true)
} }
func TestDeployWithSigners(t *testing.T) { func TestDeployWithSigners(t *testing.T) {
@ -772,6 +783,12 @@ func TestComlileAndInvokeFunction(t *testing.T) {
e.Run(t, append(cmd, h.StringLE(), "getValue", e.Run(t, append(cmd, h.StringLE(), "getValue",
"--", testcli.ValidatorAddr, hVerify.StringLE())...) "--", testcli.ValidatorAddr, hVerify.StringLE())...)
}) })
t.Run("with await", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, append(cmd, "--force", "--await", h.StringLE(), "getValue")...)
e.CheckAwaitableTxPersisted(t)
})
}) })
t.Run("real invoke and save tx", func(t *testing.T) { t.Run("real invoke and save tx", func(t *testing.T) {

View file

@ -91,6 +91,7 @@ func NewCommands() []cli.Command {
txctx.SysGasFlag, txctx.SysGasFlag,
txctx.OutFlag, txctx.OutFlag,
txctx.ForceFlag, txctx.ForceFlag,
txctx.AwaitFlag,
} }
invokeFunctionFlags = append(invokeFunctionFlags, options.Wallet...) invokeFunctionFlags = append(invokeFunctionFlags, options.Wallet...)
invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...) invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...)
@ -188,10 +189,12 @@ func NewCommands() []cli.Command {
{ {
Name: "deploy", Name: "deploy",
Usage: "deploy a smart contract (.nef with description)", Usage: "deploy a smart contract (.nef with description)",
UsageText: "neo-go contract deploy -r endpoint -w wallet [-a address] [-g gas] [-e sysgas] --in contract.nef --manifest contract.manifest.json [--out file] [--force] [data]", UsageText: "neo-go contract deploy -r endpoint -w wallet [-a address] [-g gas] [-e sysgas] --in contract.nef --manifest contract.manifest.json [--out file] [--force] [--await] [data]",
Description: `Deploys given contract into the chain. The gas parameter is for additional Description: `Deploys given contract into the chain. The gas parameter is for additional
gas to be added as a network fee to prioritize the transaction. The data gas to be added as a network fee to prioritize the transaction. The data
parameter is an optional parameter to be passed to '_deploy' method. parameter is an optional parameter to be passed to '_deploy' method. When
--await flag is specified, it waits for the transaction to be included
in a block.
`, `,
Action: contractDeploy, Action: contractDeploy,
Flags: deployFlags, Flags: deployFlags,
@ -201,13 +204,14 @@ func NewCommands() []cli.Command {
{ {
Name: "invokefunction", Name: "invokefunction",
Usage: "invoke deployed contract on the blockchain", Usage: "invoke deployed contract on the blockchain",
UsageText: "neo-go contract invokefunction -r endpoint -w wallet [-a address] [-g gas] [-e sysgas] [--out file] [--force] scripthash [method] [arguments...] [--] [signers...]", UsageText: "neo-go contract invokefunction -r endpoint -w wallet [-a address] [-g gas] [-e sysgas] [--out file] [--force] [--await] scripthash [method] [arguments...] [--] [signers...]",
Description: `Executes given (as a script hash) deployed script with the given method, Description: `Executes given (as a script hash) deployed script with the given method,
arguments and signers. Sender is included in the list of signers by default arguments and signers. Sender is included in the list of signers by default
with None witness scope. If you'd like to change default sender's scope, with None witness scope. If you'd like to change default sender's scope,
specify it via signers parameter. See testinvokefunction documentation for specify it via signers parameter. See testinvokefunction documentation for
the details about parameters. It differs from testinvokefunction in that this the details about parameters. It differs from testinvokefunction in that this
command sends an invocation transaction to the network. command sends an invocation transaction to the network. When --await flag is
specified, it waits for the transaction to be included in a block.
`, `,
Action: invokeFunction, Action: invokeFunction,
Flags: invokeFunctionFlags, Flags: invokeFunctionFlags,

View file

@ -5,13 +5,16 @@ package txctx
import ( import (
"fmt" "fmt"
"io"
"time" "time"
"github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/input"
"github.com/nspcc-dev/neo-go/cli/paramcontext" "github.com/nspcc-dev/neo-go/cli/paramcontext"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -37,6 +40,11 @@ var (
Name: "force", Name: "force",
Usage: "Do not ask for a confirmation (and ignore errors)", Usage: "Do not ask for a confirmation (and ignore errors)",
} }
// AwaitFlag is a flag used to wait for the transaction to be included in a block.
AwaitFlag = cli.BoolFlag{
Name: "await",
Usage: "wait for the transaction to be included in a block",
}
) )
// SignAndSend adds network and system fees to the provided transaction and // SignAndSend adds network and system fees to the provided transaction and
@ -48,6 +56,7 @@ func SignAndSend(ctx *cli.Context, act *actor.Actor, acc *wallet.Account, tx *tr
gas = flags.Fixed8FromContext(ctx, "gas") gas = flags.Fixed8FromContext(ctx, "gas")
sysgas = flags.Fixed8FromContext(ctx, "sysgas") sysgas = flags.Fixed8FromContext(ctx, "sysgas")
ver = act.GetVersion() ver = act.GetVersion()
aer *state.AppExecResult
) )
tx.SystemFee += int64(sysgas) tx.SystemFee += int64(sysgas)
@ -68,12 +77,37 @@ func SignAndSend(ctx *cli.Context, act *actor.Actor, acc *wallet.Account, tx *tr
// Compensate for confirmation waiting. // Compensate for confirmation waiting.
tx.ValidUntilBlock += uint32((waitTime.Milliseconds() / int64(ver.Protocol.MillisecondsPerBlock))) + 1 tx.ValidUntilBlock += uint32((waitTime.Milliseconds() / int64(ver.Protocol.MillisecondsPerBlock))) + 1
} }
_, _, err = act.SignAndSend(tx) var (
resTx util.Uint256
vub uint32
)
resTx, vub, err = act.SignAndSend(tx)
if err != nil {
return cli.NewExitError(err, 1)
}
if ctx.Bool("await") {
aer, err = act.Wait(resTx, vub, err)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to await transaction %s: %w", resTx.StringLE(), err), 1)
}
}
} }
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) DumpTransactionInfo(ctx.App.Writer, tx.Hash(), aer)
return nil return nil
} }
// DumpTransactionInfo prints transaction info to the given writer.
func DumpTransactionInfo(w io.Writer, h util.Uint256, res *state.AppExecResult) {
fmt.Fprintln(w, h.StringLE())
if res != nil {
fmt.Fprintf(w, "OnChain:\t%t\n", res != nil)
fmt.Fprintf(w, "VMState:\t%s\n", res.VMState.String())
if res.FaultException != "" {
fmt.Fprintf(w, "FaultException:\t%s\n", res.FaultException)
}
}
}

View file

@ -1,15 +1,19 @@
package util package util
import ( import (
"errors"
"fmt" "fmt"
"strings" "strings"
"github.com/nspcc-dev/neo-go/cli/cmdargs" "github.com/nspcc-dev/neo-go/cli/cmdargs"
"github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/cli/txctx"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode" "github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -55,7 +59,7 @@ func cancelTx(ctx *cli.Context) error {
return cli.NewExitError(fmt.Errorf("account %s is not a signer of the conflicting transaction", acc.Address), 1) return cli.NewExitError(fmt.Errorf("account %s is not a signer of the conflicting transaction", acc.Address), 1)
} }
resHash, _, err := a.SendTunedRun([]byte{byte(opcode.RET)}, []transaction.Attribute{{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: txHash}}}, func(r *result.Invoke, t *transaction.Transaction) error { resHash, resVub, err := a.SendTunedRun([]byte{byte(opcode.RET)}, []transaction.Attribute{{Type: transaction.ConflictsT, Value: &transaction.Conflicts{Hash: txHash}}}, func(r *result.Invoke, t *transaction.Transaction) error {
err := actor.DefaultCheckerModifier(r, t) err := actor.DefaultCheckerModifier(r, t)
if err != nil { if err != nil {
return err return err
@ -72,6 +76,29 @@ func cancelTx(ctx *cli.Context) error {
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("failed to send conflicting transaction: %w", err), 1) return cli.NewExitError(fmt.Errorf("failed to send conflicting transaction: %w", err), 1)
} }
fmt.Fprintln(ctx.App.Writer, resHash.StringLE()) var (
acceptedH = resHash
res *state.AppExecResult
)
if ctx.Bool("await") {
res, err = a.WaitAny(gctx, resVub, txHash, resHash)
if err != nil {
if errors.Is(err, waiter.ErrTxNotAccepted) {
if mainTx == nil {
return cli.NewExitError(fmt.Errorf("neither target nor conflicting transaction is accepted before the current height %d (ValidUntilBlock value of conlicting transaction). Main transaction is unknown to the provided RPC node, thus still has chances to be accepted, you may try cancellation again", resVub), 1)
}
fmt.Fprintf(ctx.App.Writer, "Neither target nor conflicting transaction is accepted before the current height %d (ValidUntilBlock value of both target and conflicting transactions). Main transaction is not valid anymore, cancellation is successful\n", resVub)
return nil
}
return cli.NewExitError(fmt.Errorf("failed to await target/ conflicting transaction %s/ %s: %w", txHash.StringLE(), resHash.StringLE(), err), 1)
}
if txHash.Equals(res.Container) {
fmt.Fprintln(ctx.App.Writer, "Target transaction accepted")
acceptedH = txHash
} else {
fmt.Fprintln(ctx.App.Writer, "Conflicting transaction accepted")
}
}
txctx.DumpTransactionInfo(ctx.App.Writer, acceptedH, res)
return nil return nil
} }

View file

@ -17,12 +17,14 @@ import (
// NewCommands returns util commands for neo-go CLI. // NewCommands returns util commands for neo-go CLI.
func NewCommands() []cli.Command { func NewCommands() []cli.Command {
txDumpFlags := append([]cli.Flag{}, options.RPC...) txDumpFlags := append([]cli.Flag{}, options.RPC...)
txSendFlags := append(txDumpFlags, txctx.AwaitFlag)
txCancelFlags := append([]cli.Flag{ txCancelFlags := append([]cli.Flag{
flags.AddressFlag{ flags.AddressFlag{
Name: "address, a", Name: "address, a",
Usage: "address to use as conflicting transaction signee (and gas source)", Usage: "address to use as conflicting transaction signee (and gas source)",
}, },
txctx.GasFlag, txctx.GasFlag,
txctx.AwaitFlag,
}, options.RPC...) }, options.RPC...)
txCancelFlags = append(txCancelFlags, options.Wallet...) txCancelFlags = append(txCancelFlags, options.Wallet...)
return []cli.Command{ return []cli.Command{
@ -42,19 +44,20 @@ func NewCommands() []cli.Command {
{ {
Name: "sendtx", Name: "sendtx",
Usage: "Send complete transaction stored in a context file", Usage: "Send complete transaction stored in a context file",
UsageText: "sendtx [-r <endpoint>] <file.in>", UsageText: "sendtx [-r <endpoint>] <file.in> [--await]",
Description: `Sends the transaction from the given context file to the given RPC node if it's Description: `Sends the transaction from the given context file to the given RPC node if it's
completely signed and ready. This command expects a ContractParametersContext completely signed and ready. This command expects a ContractParametersContext
JSON file for input, it can't handle binary (or hex- or base64-encoded) JSON file for input, it can't handle binary (or hex- or base64-encoded)
transactions. transactions. If the --await flag is included, the command waits for the
transaction to be included in a block before exiting.
`, `,
Action: sendTx, Action: sendTx,
Flags: txDumpFlags, Flags: txSendFlags,
}, },
{ {
Name: "canceltx", Name: "canceltx",
Usage: "Cancel transaction by sending conflicting transaction", Usage: "Cancel transaction by sending conflicting transaction",
UsageText: "canceltx <txid> -r <endpoint> --wallet <wallet> [--account <account>] [--wallet-config <path>] [--gas <gas>]", UsageText: "canceltx <txid> -r <endpoint> --wallet <wallet> [--account <account>] [--wallet-config <path>] [--gas <gas>] [--await]",
Description: `Aims to prevent a transaction from being added to the blockchain by dispatching a more Description: `Aims to prevent a transaction from being added to the blockchain by dispatching a more
prioritized conflicting transaction to the specified RPC node. The input for this command should prioritized conflicting transaction to the specified RPC node. The input for this command should
be the transaction hash. If another account is not specified, the conflicting transaction is be the transaction hash. If another account is not specified, the conflicting transaction is
@ -65,7 +68,8 @@ func NewCommands() []cli.Command {
target transaction's ValidUntilBlock value. If the target transaction is not in the memory pool, standard target transaction's ValidUntilBlock value. If the target transaction is not in the memory pool, standard
NetworkFee calculations are performed based on the calculatenetworkfee RPC request. If the --gas NetworkFee calculations are performed based on the calculatenetworkfee RPC request. If the --gas
flag is included, the specified value is added to the resulting conflicting transaction network fee flag is included, the specified value is added to the resulting conflicting transaction network fee
in both scenarios.`, in both scenarios. When the --await flag is included, the command waits for one of the conflicting
or target transactions to be included in a block.`,
Action: cancelTx, Action: cancelTx,
Flags: txCancelFlags, Flags: txCancelFlags,
}, },
@ -75,6 +79,11 @@ func NewCommands() []cli.Command {
UsageText: "txdump [-r <endpoint>] <file.in>", UsageText: "txdump [-r <endpoint>] <file.in>",
Action: txDump, Action: txDump,
Flags: txDumpFlags, Flags: txDumpFlags,
Description: `Dumps the transaction from the given parameter context file to
the output. This command expects a ContractParametersContext JSON file for input, it can't handle
binary (or hex- or base64-encoded) transactions. If --rpc-endpoint flag is specified the result
of the given script after running it true the VM will be printed. Otherwise only transaction will
be printed.`,
}, },
{ {
Name: "ops", Name: "ops",

View file

@ -5,6 +5,9 @@ import (
"github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/cli/paramcontext" "github.com/nspcc-dev/neo-go/cli/paramcontext"
"github.com/nspcc-dev/neo-go/cli/txctx"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -37,6 +40,14 @@ func sendTx(ctx *cli.Context) error {
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("failed to submit transaction to RPC node: %w", err), 1) return cli.NewExitError(fmt.Errorf("failed to submit transaction to RPC node: %w", err), 1)
} }
fmt.Fprintln(ctx.App.Writer, res.StringLE()) var aer *state.AppExecResult
if ctx.Bool("await") {
version, err := c.GetVersion()
aer, err = waiter.New(c, version).Wait(res, tx.ValidUntilBlock, err)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to await transaction %s: %w", res.StringLE(), err), 1)
}
}
txctx.DumpTransactionInfo(ctx.App.Writer, res, aer)
return nil return nil
} }

View file

@ -136,3 +136,44 @@ func TestUtilCancelTx(t *testing.T) {
return aerErr == nil return aerErr == nil
}, time.Second*2, time.Millisecond*50) }, time.Second*2, time.Millisecond*50)
} }
func TestAwaitUtilCancelTx(t *testing.T) {
e := testcli.NewExecutor(t, true)
w, err := wallet.NewWalletFromFile("../testdata/testwallet.json")
require.NoError(t, err)
transferArgs := []string{
"neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://" + e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet,
"--to", w.Accounts[0].Address,
"--token", "NEO",
"--from", testcli.ValidatorAddr,
"--force",
}
args := []string{"neo-go", "util", "canceltx",
"-r", "http://" + e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet,
"--address", testcli.ValidatorAddr,
"--await"}
e.In.WriteString("one\r")
e.Run(t, append(transferArgs, "--amount", "1")...)
line := e.GetNextLine(t)
txHash, err := util.Uint256DecodeStringLE(line)
require.NoError(t, err)
_, ok := e.Chain.GetMemPool().TryGetValue(txHash)
require.True(t, ok)
e.In.WriteString("one\r")
e.Run(t, append(args, txHash.StringLE())...)
e.CheckNextLine(t, "Conflicting transaction accepted")
resHash, _ := e.CheckAwaitableTxPersisted(t)
require.Eventually(t, func() bool {
_, aerErr := e.Chain.GetAppExecResults(resHash.Hash(), trigger.Application)
return aerErr == nil
}, time.Second*2, time.Millisecond*50)
}

View file

@ -159,4 +159,61 @@ func TestRegisterCandidate(t *testing.T) {
e.RunWithError(t, "neo-go", "query", "voter", "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], validatorAddress, validatorAddress) e.RunWithError(t, "neo-go", "query", "voter", "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], validatorAddress, validatorAddress)
e.RunWithError(t, "neo-go", "query", "committee", "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], "something") e.RunWithError(t, "neo-go", "query", "committee", "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], "something")
e.RunWithError(t, "neo-go", "query", "candidates", "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], "something") e.RunWithError(t, "neo-go", "query", "candidates", "--rpc-endpoint", "http://"+e.RPC.Addresses()[0], "something")
t.Run("VoteUnvote await", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, "neo-go", "wallet", "candidate", "register",
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet,
"--address", validatorAddress,
"--force", "--await")
e.CheckAwaitableTxPersisted(t)
e.In.WriteString("one\r")
e.Run(t, "neo-go", "wallet", "candidate", "vote",
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet,
"--address", validatorAddress,
"--candidate", validatorHex,
"--force",
"--await")
e.CheckAwaitableTxPersisted(t)
b, _ := e.Chain.GetGoverningTokenBalance(testcli.ValidatorPriv.GetScriptHash())
// unvote
e.In.WriteString("one\r")
e.Run(t, "neo-go", "wallet", "candidate", "vote",
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet,
"--address", validatorAddress,
"--force", "--await")
_, index := e.CheckAwaitableTxPersisted(t)
vs, err = e.Chain.GetEnrollments()
require.Equal(t, 1, len(vs))
require.Equal(t, validatorPublic, vs[0].Key)
require.Equal(t, big.NewInt(0), vs[0].Votes)
// check state
e.Run(t, "neo-go", "query", "voter",
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
validatorAddress)
e.CheckNextLine(t, "^\\s*Voted:\\s+"+"null") // no vote.
e.CheckNextLine(t, "^\\s*Amount\\s*:\\s*"+b.String()+"$")
e.CheckNextLine(t, "^\\s*Block\\s*:\\s*"+strconv.FormatUint(uint64(index), 10))
e.CheckEOF(t)
})
e.In.WriteString("one\r")
e.Run(t, "neo-go", "wallet", "candidate", "unregister",
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet,
"--address", validatorAddress,
"--force",
"--await")
e.CheckAwaitableTxPersisted(t)
vs, err = e.Chain.GetEnrollments()
require.Equal(t, 0, len(vs))
} }

View file

@ -8,7 +8,10 @@ import (
"github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/cli/paramcontext" "github.com/nspcc-dev/neo-go/cli/paramcontext"
"github.com/nspcc-dev/neo-go/cli/txctx"
"github.com/nspcc-dev/neo-go/pkg/core/state"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -17,6 +20,7 @@ func signStoredTransaction(ctx *cli.Context) error {
out = ctx.String("out") out = ctx.String("out")
rpcNode = ctx.String(options.RPCEndpointFlag) rpcNode = ctx.String(options.RPCEndpointFlag)
addrFlag = ctx.Generic("address").(*flags.Address) addrFlag = ctx.Generic("address").(*flags.Address)
aer *state.AppExecResult
) )
if err := cmdargs.EnsureNone(ctx); err != nil { if err := cmdargs.EnsureNone(ctx); err != nil {
return err return err
@ -84,10 +88,15 @@ func signStoredTransaction(ctx *cli.Context) error {
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("failed to submit transaction to RPC node: %w", err), 1) return cli.NewExitError(fmt.Errorf("failed to submit transaction to RPC node: %w", err), 1)
} }
fmt.Fprintln(ctx.App.Writer, res.StringLE()) if ctx.Bool("await") {
return nil version, err := c.GetVersion()
aer, err = waiter.New(c, version).Wait(res, tx.ValidUntilBlock, err)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to await transaction %s: %w", res.StringLE(), err), 1)
}
}
} }
fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) txctx.DumpTransactionInfo(ctx.App.Writer, tx.Hash(), aer)
return nil return nil
} }

View file

@ -101,7 +101,7 @@ func newNEP11Commands() []cli.Command {
{ {
Name: "transfer", Name: "transfer",
Usage: "transfer NEP-11 tokens", Usage: "transfer NEP-11 tokens",
UsageText: "transfer -w wallet [--wallet-config path] --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --id <token-id> [--amount string] [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]", UsageText: "transfer -w wallet [--wallet-config path] --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --id <token-id> [--amount string] [--await] [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
Action: transferNEP11, Action: transferNEP11,
Flags: transferFlags, Flags: transferFlags,
Description: `Transfers specified NEP-11 token with optional cosigners list attached to Description: `Transfers specified NEP-11 token with optional cosigners list attached to
@ -110,7 +110,8 @@ func newNEP11Commands() []cli.Command {
'contract testinvokefunction' documentation for the details 'contract testinvokefunction' documentation for the details
about cosigners syntax. If no cosigners are given then the about cosigners syntax. If no cosigners are given then the
sender with CalledByEntry scope will be used as the only sender with CalledByEntry scope will be used as the only
signer. signer. If --await flag is set then the command will wait
for the transaction to be included in a block.
`, `,
}, },
{ {

View file

@ -70,6 +70,7 @@ var (
txctx.GasFlag, txctx.GasFlag,
txctx.SysGasFlag, txctx.SysGasFlag,
txctx.ForceFlag, txctx.ForceFlag,
txctx.AwaitFlag,
cli.StringFlag{ cli.StringFlag{
Name: "amount", Name: "amount",
Usage: "Amount of asset to send", Usage: "Amount of asset to send",
@ -83,6 +84,7 @@ var (
txctx.GasFlag, txctx.GasFlag,
txctx.SysGasFlag, txctx.SysGasFlag,
txctx.ForceFlag, txctx.ForceFlag,
txctx.AwaitFlag,
}, options.RPC...) }, options.RPC...)
) )
@ -147,20 +149,22 @@ func newNEP17Commands() []cli.Command {
{ {
Name: "transfer", Name: "transfer",
Usage: "transfer NEP-17 tokens", Usage: "transfer NEP-17 tokens",
UsageText: "transfer -w wallet [--wallet-config path] --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --amount string [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]", UsageText: "transfer -w wallet [--wallet-config path] [--await] --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash-or-name> --amount string [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
Action: transferNEP17, Action: transferNEP17,
Flags: transferFlags, Flags: transferFlags,
Description: `Transfers specified NEP-17 token amount with optional 'data' parameter and cosigners Description: `Transfers specified NEP-17 token amount with optional 'data' parameter and cosigners
list attached to the transfer. See 'contract testinvokefunction' documentation list attached to the transfer. See 'contract testinvokefunction' documentation
for the details about 'data' parameter and cosigners syntax. If no 'data' is for the details about 'data' parameter and cosigners syntax. If no 'data' is
given then default nil value will be used. If no cosigners are given then the given then default nil value will be used. If no cosigners are given then the
sender with CalledByEntry scope will be used as the only signer. sender with CalledByEntry scope will be used as the only signer. When --await
flag is used, the command waits for the transaction to be included in a block
before exiting.
`, `,
}, },
{ {
Name: "multitransfer", Name: "multitransfer",
Usage: "transfer NEP-17 tokens to multiple recipients", Usage: "transfer NEP-17 tokens to multiple recipients",
UsageText: `multitransfer -w wallet [--wallet-config path] --rpc-endpoint <node> --timeout <time> --from <addr>` + UsageText: `multitransfer -w wallet [--wallet-config path] [--await] --rpc-endpoint <node> --timeout <time> --from <addr>` +
` <token1>:<addr1>:<amount1> [<token2>:<addr2>:<amount2> [...]] [-- <cosigner1:Scope> [<cosigner2> [...]]]`, ` <token1>:<addr1>:<amount1> [<token2>:<addr2>:<amount2> [...]] [-- <cosigner1:Scope> [<cosigner2> [...]]]`,
Action: multiTransferNEP17, Action: multiTransferNEP17,
Flags: multiTransferFlags, Flags: multiTransferFlags,

View file

@ -20,7 +20,7 @@ func newValidatorCommands() []cli.Command {
{ {
Name: "register", Name: "register",
Usage: "register as a new candidate", Usage: "register as a new candidate",
UsageText: "register -w <path> -r <rpc> -a <addr> [-g gas] [-e sysgas] [--out file] [--force]", UsageText: "register -w <path> -r <rpc> -a <addr> [-g gas] [-e sysgas] [--out file] [--force] [--await]",
Action: handleRegister, Action: handleRegister,
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
walletPathFlag, walletPathFlag,
@ -29,6 +29,7 @@ func newValidatorCommands() []cli.Command {
txctx.SysGasFlag, txctx.SysGasFlag,
txctx.OutFlag, txctx.OutFlag,
txctx.ForceFlag, txctx.ForceFlag,
txctx.AwaitFlag,
flags.AddressFlag{ flags.AddressFlag{
Name: "address, a", Name: "address, a",
Usage: "Address to register", Usage: "Address to register",
@ -38,7 +39,7 @@ func newValidatorCommands() []cli.Command {
{ {
Name: "unregister", Name: "unregister",
Usage: "unregister self as a candidate", Usage: "unregister self as a candidate",
UsageText: "unregister -w <path> -r <rpc> -a <addr> [-g gas] [-e sysgas] [--out file] [--force]", UsageText: "unregister -w <path> -r <rpc> -a <addr> [-g gas] [-e sysgas] [--out file] [--force] [--await]",
Action: handleUnregister, Action: handleUnregister,
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
walletPathFlag, walletPathFlag,
@ -47,6 +48,7 @@ func newValidatorCommands() []cli.Command {
txctx.SysGasFlag, txctx.SysGasFlag,
txctx.OutFlag, txctx.OutFlag,
txctx.ForceFlag, txctx.ForceFlag,
txctx.AwaitFlag,
flags.AddressFlag{ flags.AddressFlag{
Name: "address, a", Name: "address, a",
Usage: "Address to unregister", Usage: "Address to unregister",
@ -56,9 +58,10 @@ func newValidatorCommands() []cli.Command {
{ {
Name: "vote", Name: "vote",
Usage: "vote for a validator", Usage: "vote for a validator",
UsageText: "vote -w <path> -r <rpc> [-s <timeout>] [-g gas] [-e sysgas] -a <addr> [-c <public key>] [--out file] [--force]", UsageText: "vote -w <path> -r <rpc> [-s <timeout>] [-g gas] [-e sysgas] -a <addr> [-c <public key>] [--out file] [--force] [--await]",
Description: `Votes for a validator by calling "vote" method of a NEO native Description: `Votes for a validator by calling "vote" method of a NEO native
contract. Do not provide candidate argument to perform unvoting. contract. Do not provide candidate argument to perform unvoting. If --await flag is
included, the command waits for the transaction to be included in a block before exiting.
`, `,
Action: handleVote, Action: handleVote,
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
@ -68,6 +71,7 @@ func newValidatorCommands() []cli.Command {
txctx.SysGasFlag, txctx.SysGasFlag,
txctx.OutFlag, txctx.OutFlag,
txctx.ForceFlag, txctx.ForceFlag,
txctx.AwaitFlag,
flags.AddressFlag{ flags.AddressFlag{
Name: "address, a", Name: "address, a",
Usage: "Address to vote from", Usage: "Address to vote from",

View file

@ -86,6 +86,7 @@ func NewCommands() []cli.Command {
txctx.SysGasFlag, txctx.SysGasFlag,
txctx.OutFlag, txctx.OutFlag,
txctx.ForceFlag, txctx.ForceFlag,
txctx.AwaitFlag,
flags.AddressFlag{ flags.AddressFlag{
Name: "address, a", Name: "address, a",
Usage: "Address to claim GAS for", Usage: "Address to claim GAS for",
@ -96,6 +97,7 @@ func NewCommands() []cli.Command {
walletPathFlag, walletPathFlag,
walletConfigFlag, walletConfigFlag,
txctx.OutFlag, txctx.OutFlag,
txctx.AwaitFlag,
inFlag, inFlag,
flags.AddressFlag{ flags.AddressFlag{
Name: "address, a", Name: "address, a",
@ -110,7 +112,7 @@ func NewCommands() []cli.Command {
{ {
Name: "claim", Name: "claim",
Usage: "claim GAS", Usage: "claim GAS",
UsageText: "neo-go wallet claim -w wallet [--wallet-config path] [-g gas] [-e sysgas] -a address -r endpoint [-s timeout] [--out file] [--force]", UsageText: "neo-go wallet claim -w wallet [--wallet-config path] [-g gas] [-e sysgas] -a address -r endpoint [-s timeout] [--out file] [--force] [--await]",
Action: claimGas, Action: claimGas,
Flags: claimFlags, Flags: claimFlags,
}, },
@ -288,13 +290,15 @@ func NewCommands() []cli.Command {
{ {
Name: "sign", Name: "sign",
Usage: "cosign transaction with multisig/contract/additional account", Usage: "cosign transaction with multisig/contract/additional account",
UsageText: "sign -w wallet [--wallet-config path] --address <address> --in <file.in> [--out <file.out>] [-r <endpoint>]", UsageText: "sign -w wallet [--wallet-config path] --address <address> --in <file.in> [--out <file.out>] [-r <endpoint>] [--await]",
Description: `Signs the given (in file.in) context (which must be a transaction Description: `Signs the given (in file.in) context (which must be a transaction
signing context) for the given address using the given wallet. This command can signing context) for the given address using the given wallet. This command can
output the resulting JSON (with additional signature added) right to the console output the resulting JSON (with additional signature added) right to the console
(if no file.out and no RPC endpoint specified) or into a file (which can be the (if no file.out and no RPC endpoint specified) or into a file (which can be the
same as input one). If an RPC endpoint is given it'll also try to construct a same as input one). If an RPC endpoint is given it'll also try to construct a
complete transaction and send it via RPC (printing its hash if everything is OK). complete transaction and send it via RPC (printing its hash if everything is OK).
If the --await (with a given RPC endpoint) flag is included, the command waits
for the transaction to be included in a block before exiting.
`, `,
Action: signStoredTransaction, Action: signStoredTransaction,
Flags: signFlags, Flags: signFlags,

View file

@ -605,6 +605,47 @@ func TestWalletClaimGas(t *testing.T) {
} else { } else {
require.Equal(t, 1, balanceAfter.Cmp(balanceBefore)) require.Equal(t, 1, balanceAfter.Cmp(balanceBefore))
} }
t.Run("await", func(t *testing.T) {
args := []string{
"neo-go", "wallet", "nep17", "multitransfer",
"--rpc-endpoint", "http://" + e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet,
"--from", testcli.ValidatorAddr, "--await",
"--force",
"NEO:" + testcli.TestWalletAccount + ":1000",
"GAS:" + testcli.TestWalletAccount + ":1000", // for tx send
}
e.In.WriteString("one\r")
e.Run(t, args...)
e.CheckAwaitableTxPersisted(t)
h, err := address.StringToUint160(testcli.TestWalletAccount)
require.NoError(t, err)
balanceBefore := e.Chain.GetUtilityTokenBalance(h)
claimHeight := e.Chain.BlockHeight() + 1
cl, err := e.Chain.CalculateClaimable(h, claimHeight)
require.NoError(t, err)
require.True(t, cl.Sign() > 0)
e.In.WriteString("testpass\r")
e.Run(t, "neo-go", "wallet", "claim",
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
"--wallet", testcli.TestWalletPath,
"--address", testcli.TestWalletAccount,
"--force", "--await")
tx, height = e.CheckAwaitableTxPersisted(t)
balanceBefore.Sub(balanceBefore, big.NewInt(tx.NetworkFee+tx.SystemFee))
balanceBefore.Add(balanceBefore, cl)
balanceAfter = e.Chain.GetUtilityTokenBalance(h)
// height can be bigger than claimHeight especially when tests are executed with -race.
if height == claimHeight {
require.Equal(t, 0, balanceAfter.Cmp(balanceBefore))
} else {
require.Equal(t, 1, balanceAfter.Cmp(balanceBefore))
}
})
} }
func TestWalletImportDeployed(t *testing.T) { func TestWalletImportDeployed(t *testing.T) {
@ -822,6 +863,31 @@ func TestOfflineSigning(t *testing.T) {
"--in", txPath) "--in", txPath)
}) })
e.CheckTxPersisted(t) e.CheckTxPersisted(t)
t.Run("await 1/1 multisig", func(t *testing.T) {
args := []string{"neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://" + e.RPC.Addresses()[0],
"--wallet", walletPath,
"--from", testcli.ValidatorAddr,
"--to", w.Accounts[0].Address,
"--token", "NEO",
"--amount", "1",
"--force",
}
e.Run(t, append(args, "--out", txPath)...)
e.In.WriteString("one\r")
e.Run(t, "neo-go", "wallet", "sign",
"--wallet", testcli.ValidatorWallet, "--address", testcli.ValidatorAddr,
"--in", txPath, "--out", txPath)
e.Run(t, "neo-go", "wallet", "sign",
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
"--wallet", walletPath, "--address", testcli.ValidatorAddr,
"--in", txPath, "--await")
e.CheckAwaitableTxPersisted(t)
})
t.Run("simple signature", func(t *testing.T) { t.Run("simple signature", func(t *testing.T) {
simpleAddr := w.Accounts[0].Address simpleAddr := w.Accounts[0].Address
args := []string{"neo-go", "wallet", "nep17", "transfer", args := []string{"neo-go", "wallet", "nep17", "transfer",
@ -853,6 +919,31 @@ func TestOfflineSigning(t *testing.T) {
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0], "--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
txPath) txPath)
}) })
t.Run("await simple signature", func(t *testing.T) {
simpleAddr := w.Accounts[0].Address
args := []string{"neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://" + e.RPC.Addresses()[0],
"--wallet", walletPath,
"--from", simpleAddr,
"--to", testcli.ValidatorAddr,
"--token", "NEO",
"--amount", "1",
"--force",
}
e.Run(t, append(args, "--out", txPath)...)
e.In.WriteString("one\r")
e.Run(t, "neo-go", "wallet", "sign",
"--wallet", testcli.ValidatorWallet, "--address", simpleAddr,
"--in", txPath, "--out", txPath)
e.Run(t, "neo-go", "util", "sendtx",
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
txPath, "--await")
e.CheckAwaitableTxPersisted(t)
})
} }
func TestWalletDump(t *testing.T) { func TestWalletDump(t *testing.T) {

View file

@ -309,6 +309,13 @@ func (e *Executor) CheckTxPersisted(t *testing.T, prefix ...string) (*transactio
return tx, height return tx, height
} }
func (e *Executor) CheckAwaitableTxPersisted(t *testing.T, prefix ...string) (*transaction.Transaction, uint32) {
tx, vub := e.CheckTxPersisted(t, prefix...)
e.CheckNextLine(t, "OnChain:\ttrue")
e.CheckNextLine(t, "VMState:\tHALT")
return tx, vub
}
func GenerateKeys(t *testing.T, n int) ([]*keys.PrivateKey, keys.PublicKeys) { func GenerateKeys(t *testing.T, n int) ([]*keys.PrivateKey, keys.PublicKeys) {
privs := make([]*keys.PrivateKey, n) privs := make([]*keys.PrivateKey, n)
pubs := make(keys.PublicKeys, n) pubs := make(keys.PublicKeys, n)