Merge pull request #3265 from nspcc-dev/cli_await

cli: add await flag for operations with transactions
This commit is contained in:
Anna Shaleva 2023-12-29 17:22:23 +05:00 committed by GitHub
commit 1ca7b0bbeb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 492 additions and 78 deletions

View file

@ -328,6 +328,14 @@ func TestNEP11_ND_OwnerOf_BalanceOf_Transfer(t *testing.T) {
e.Run(t, append(cmdCheckBalance, "--token", h.StringLE())...)
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
verifyH := deployVerifyContract(t, e)
cmdTransfer = []string{

View file

@ -223,12 +223,19 @@ func TestNEP17Transfer(t *testing.T) {
"neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://" + e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet,
"--to", address.Uint160ToString(e.Chain.GetNotaryContractScriptHash()),
"--token", "GAS",
"--amount", "1",
"--from", testcli.ValidatorAddr,
"--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) {
e.In.WriteString("one\r")

View file

@ -32,8 +32,14 @@ import (
"gopkg.in/yaml.v3"
)
const (
// DefaultTimeout is the default timeout used for RPC requests.
const DefaultTimeout = 10 * time.Second
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
// check for flag presence in the context.
@ -129,6 +135,9 @@ func GetTimeoutContext(ctx *cli.Context) (context.Context, func()) {
if dur == 0 {
dur = DefaultTimeout
}
if !ctx.IsSet("timeout") && ctx.Bool("await") {
dur = DefaultAwaitableTimeout
}
return context.WithTimeout(context.Background(), dur)
}

View file

@ -338,7 +338,7 @@ func TestContractDeployWithData(t *testing.T) {
"--config", "testdata/deploy/neo-go.yml",
"--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)
cmd := []string{
"neo-go", "contract", "deploy",
@ -348,6 +348,9 @@ func TestContractDeployWithData(t *testing.T) {
"--force",
}
if await {
cmd = append(cmd, "--await")
}
if haveData {
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.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())
if !haveData {
return
@ -396,9 +404,12 @@ func TestContractDeployWithData(t *testing.T) {
require.Equal(t, []byte("take_me_to_church"), res.Stack[0].Value())
}
deployContract(t, true, "")
deployContract(t, false, "Global")
deployContract(t, true, "Global")
deployContract(t, true, "", false)
deployContract(t, false, "Global", false)
deployContract(t, true, "Global", false)
deployContract(t, false, "", true)
deployContract(t, true, "Global", true)
deployContract(t, true, "", true)
}
func TestDeployWithSigners(t *testing.T) {
@ -772,6 +783,12 @@ func TestComlileAndInvokeFunction(t *testing.T) {
e.Run(t, append(cmd, h.StringLE(), "getValue",
"--", 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) {

View file

@ -91,6 +91,7 @@ func NewCommands() []cli.Command {
txctx.SysGasFlag,
txctx.OutFlag,
txctx.ForceFlag,
txctx.AwaitFlag,
}
invokeFunctionFlags = append(invokeFunctionFlags, options.Wallet...)
invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...)
@ -188,10 +189,12 @@ func NewCommands() []cli.Command {
{
Name: "deploy",
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
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,
Flags: deployFlags,
@ -201,13 +204,14 @@ func NewCommands() []cli.Command {
{
Name: "invokefunction",
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,
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,
specify it via signers parameter. See testinvokefunction documentation for
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,
Flags: invokeFunctionFlags,

View file

@ -5,13 +5,16 @@ package txctx
import (
"fmt"
"io"
"time"
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/input"
"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/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/urfave/cli"
)
@ -37,6 +40,11 @@ var (
Name: "force",
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
@ -48,6 +56,7 @@ func SignAndSend(ctx *cli.Context, act *actor.Actor, acc *wallet.Account, tx *tr
gas = flags.Fixed8FromContext(ctx, "gas")
sysgas = flags.Fixed8FromContext(ctx, "sysgas")
ver = act.GetVersion()
aer *state.AppExecResult
)
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.
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 {
return cli.NewExitError(err, 1)
}
fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE())
DumpTransactionInfo(ctx.App.Writer, tx.Hash(), aer)
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
import (
"errors"
"fmt"
"strings"
"github.com/nspcc-dev/neo-go/cli/cmdargs"
"github.com/nspcc-dev/neo-go/cli/flags"
"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/neorpc/result"
"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/vm/opcode"
"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)
}
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)
if err != nil {
return err
@ -64,11 +68,37 @@ func cancelTx(ctx *cli.Context) error {
t.NetworkFee = mainTx.NetworkFee + 1
}
t.NetworkFee += int64(flags.Fixed8FromContext(ctx, "gas"))
if mainTx != nil {
t.ValidUntilBlock = mainTx.ValidUntilBlock
}
return nil
})
if err != nil {
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
}

View file

@ -17,12 +17,14 @@ import (
// NewCommands returns util commands for neo-go CLI.
func NewCommands() []cli.Command {
txDumpFlags := append([]cli.Flag{}, options.RPC...)
txSendFlags := append(txDumpFlags, txctx.AwaitFlag)
txCancelFlags := append([]cli.Flag{
flags.AddressFlag{
Name: "address, a",
Usage: "address to use as conflicting transaction signee (and gas source)",
},
txctx.GasFlag,
txctx.AwaitFlag,
}, options.RPC...)
txCancelFlags = append(txCancelFlags, options.Wallet...)
return []cli.Command{
@ -42,30 +44,32 @@ func NewCommands() []cli.Command {
{
Name: "sendtx",
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
completely signed and ready. This command expects a ContractParametersContext
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,
Flags: txDumpFlags,
Flags: txSendFlags,
},
{
Name: "canceltx",
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
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
automatically generated and signed by the default account in the wallet. If the target transaction
is in the memory pool of the provided RPC node, the NetworkFee value of the conflicting transaction
is set to the target transaction's NetworkFee value plus one (if it's sufficient for the
conflicting transaction itself). If the target transaction is not in the memory pool, standard
conflicting transaction itself), the ValidUntilBlock value of the conflicting transaction is set to the
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
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,
Flags: txCancelFlags,
},
@ -75,6 +79,11 @@ in both scenarios.
UsageText: "txdump [-r <endpoint>] <file.in>",
Action: txDump,
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",

View file

@ -5,6 +5,9 @@ import (
"github.com/nspcc-dev/neo-go/cli/options"
"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"
)
@ -37,6 +40,14 @@ func sendTx(ctx *cli.Context) error {
if err != nil {
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
}

View file

@ -136,3 +136,44 @@ func TestUtilCancelTx(t *testing.T) {
return aerErr == nil
}, 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", "committee", "--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/options"
"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/rpcclient/waiter"
"github.com/urfave/cli"
)
@ -17,6 +20,7 @@ func signStoredTransaction(ctx *cli.Context) error {
out = ctx.String("out")
rpcNode = ctx.String(options.RPCEndpointFlag)
addrFlag = ctx.Generic("address").(*flags.Address)
aer *state.AppExecResult
)
if err := cmdargs.EnsureNone(ctx); err != nil {
return err
@ -84,10 +88,15 @@ func signStoredTransaction(ctx *cli.Context) error {
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to submit transaction to RPC node: %w", err), 1)
}
fmt.Fprintln(ctx.App.Writer, res.StringLE())
return nil
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)
}
}
}
fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE())
txctx.DumpTransactionInfo(ctx.App.Writer, tx.Hash(), aer)
return nil
}

View file

@ -101,7 +101,7 @@ func newNEP11Commands() []cli.Command {
{
Name: "transfer",
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,
Flags: transferFlags,
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
about cosigners syntax. If no cosigners are given then the
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.SysGasFlag,
txctx.ForceFlag,
txctx.AwaitFlag,
cli.StringFlag{
Name: "amount",
Usage: "Amount of asset to send",
@ -83,6 +84,7 @@ var (
txctx.GasFlag,
txctx.SysGasFlag,
txctx.ForceFlag,
txctx.AwaitFlag,
}, options.RPC...)
)
@ -147,20 +149,22 @@ func newNEP17Commands() []cli.Command {
{
Name: "transfer",
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,
Flags: transferFlags,
Description: `Transfers specified NEP-17 token amount with optional 'data' parameter and cosigners
list attached to the transfer. See 'contract testinvokefunction' documentation
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
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",
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> [...]]]`,
Action: multiTransferNEP17,
Flags: multiTransferFlags,

View file

@ -20,7 +20,7 @@ func newValidatorCommands() []cli.Command {
{
Name: "register",
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,
Flags: append([]cli.Flag{
walletPathFlag,
@ -29,6 +29,7 @@ func newValidatorCommands() []cli.Command {
txctx.SysGasFlag,
txctx.OutFlag,
txctx.ForceFlag,
txctx.AwaitFlag,
flags.AddressFlag{
Name: "address, a",
Usage: "Address to register",
@ -38,7 +39,7 @@ func newValidatorCommands() []cli.Command {
{
Name: "unregister",
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,
Flags: append([]cli.Flag{
walletPathFlag,
@ -47,6 +48,7 @@ func newValidatorCommands() []cli.Command {
txctx.SysGasFlag,
txctx.OutFlag,
txctx.ForceFlag,
txctx.AwaitFlag,
flags.AddressFlag{
Name: "address, a",
Usage: "Address to unregister",
@ -56,9 +58,10 @@ func newValidatorCommands() []cli.Command {
{
Name: "vote",
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
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,
Flags: append([]cli.Flag{
@ -68,6 +71,7 @@ func newValidatorCommands() []cli.Command {
txctx.SysGasFlag,
txctx.OutFlag,
txctx.ForceFlag,
txctx.AwaitFlag,
flags.AddressFlag{
Name: "address, a",
Usage: "Address to vote from",

View file

@ -86,6 +86,7 @@ func NewCommands() []cli.Command {
txctx.SysGasFlag,
txctx.OutFlag,
txctx.ForceFlag,
txctx.AwaitFlag,
flags.AddressFlag{
Name: "address, a",
Usage: "Address to claim GAS for",
@ -96,6 +97,7 @@ func NewCommands() []cli.Command {
walletPathFlag,
walletConfigFlag,
txctx.OutFlag,
txctx.AwaitFlag,
inFlag,
flags.AddressFlag{
Name: "address, a",
@ -110,7 +112,7 @@ func NewCommands() []cli.Command {
{
Name: "claim",
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,
Flags: claimFlags,
},
@ -288,13 +290,15 @@ func NewCommands() []cli.Command {
{
Name: "sign",
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
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
(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
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,
Flags: signFlags,

View file

@ -605,6 +605,47 @@ func TestWalletClaimGas(t *testing.T) {
} else {
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) {
@ -822,6 +863,31 @@ func TestOfflineSigning(t *testing.T) {
"--in", txPath)
})
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) {
simpleAddr := w.Accounts[0].Address
args := []string{"neo-go", "wallet", "nep17", "transfer",
@ -853,6 +919,31 @@ func TestOfflineSigning(t *testing.T) {
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
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) {

View file

@ -309,6 +309,13 @@ func (e *Executor) CheckTxPersisted(t *testing.T, prefix ...string) (*transactio
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) {
privs := make([]*keys.PrivateKey, n)
pubs := make(keys.PublicKeys, n)

View file

@ -17,6 +17,7 @@ import (
"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/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
)
@ -72,7 +73,7 @@ type SignerAccount struct {
// and ErrTxNotAccepted is returned if transaction wasn't accepted by this moment.
type Actor struct {
invoker.Invoker
Waiter
waiter.Waiter
client RPCActor
opts Options
@ -126,7 +127,7 @@ func New(ra RPCActor, signers []SignerAccount) (*Actor, error) {
}
return &Actor{
Invoker: *inv,
Waiter: newWaiter(ra, version),
Waiter: waiter.New(ra, version),
client: ra,
opts: NewDefaultOptions(),
signers: signers,

View file

@ -11,9 +11,3 @@ func TestRPCActorRPCClientCompat(t *testing.T) {
_ = actor.RPCActor(&rpcclient.WSClient{})
_ = actor.RPCActor(&rpcclient.Client{})
}
func TestRPCWaiterRPCClientCompat(t *testing.T) {
_ = actor.RPCPollingWaiter(&rpcclient.Client{})
_ = actor.RPCPollingWaiter(&rpcclient.WSClient{})
_ = actor.RPCEventWaiter(&rpcclient.WSClient{})
}

View file

@ -15,6 +15,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/network/payload"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/waiter"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
@ -78,7 +79,7 @@ func (r *RPCClient) GetApplicationLog(hash util.Uint256, trig *trigger.Type) (*r
return r.applog, nil
}
var _ = actor.RPCPollingWaiter(&RPCClient{})
var _ = waiter.RPCPollingWaiter(&RPCClient{})
func TestNewActor(t *testing.T) {
rc := &RPCClient{

View file

@ -1,4 +1,4 @@
package actor
package waiter
import (
"context"
@ -100,10 +100,13 @@ func errIsAlreadyExists(err error) bool {
return strings.Contains(strings.ToLower(err.Error()), "already exists")
}
// newWaiter creates Waiter instance. It can be either websocket-based or
// polling-base, otherwise Waiter stub is returned.
func newWaiter(ra RPCActor, v *result.Version) Waiter {
if eventW, ok := ra.(RPCEventWaiter); ok {
// New creates Waiter instance. It can be either websocket-based or
// polling-base, otherwise Waiter stub is returned. As a first argument
// it accepts RPCEventWaiter implementation, RPCPollingWaiter implementation
// or not an implementation of these two interfaces. It returns websocket-based
// waiter, polling-based waiter or a stub correspondingly.
func New(base any, v *result.Version) Waiter {
if eventW, ok := base.(RPCEventWaiter); ok {
return &EventWaiter{
ws: eventW,
polling: &PollingWaiter{
@ -112,7 +115,7 @@ func newWaiter(ra RPCActor, v *result.Version) Waiter {
},
}
}
if pollW, ok := ra.(RPCPollingWaiter); ok {
if pollW, ok := base.(RPCPollingWaiter); ok {
return &PollingWaiter{
polling: pollW,
version: v,

View file

@ -1,20 +1,82 @@
package actor
package waiter_test
import (
"context"
"errors"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/block"
"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/neorpc"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient"
"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/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
type RPCClient struct {
err error
invRes *result.Invoke
netFee int64
bCount atomic.Uint32
version *result.Version
hash util.Uint256
appLog *result.ApplicationLog
context context.Context
}
func (r *RPCClient) InvokeContractVerify(contract util.Uint160, params []smartcontract.Parameter, signers []transaction.Signer, witnesses ...transaction.Witness) (*result.Invoke, error) {
return r.invRes, r.err
}
func (r *RPCClient) InvokeFunction(contract util.Uint160, operation string, params []smartcontract.Parameter, signers []transaction.Signer) (*result.Invoke, error) {
return r.invRes, r.err
}
func (r *RPCClient) InvokeScript(script []byte, signers []transaction.Signer) (*result.Invoke, error) {
return r.invRes, r.err
}
func (r *RPCClient) CalculateNetworkFee(tx *transaction.Transaction) (int64, error) {
return r.netFee, r.err
}
func (r *RPCClient) GetBlockCount() (uint32, error) {
return r.bCount.Load(), r.err
}
func (r *RPCClient) GetVersion() (*result.Version, error) {
verCopy := *r.version
return &verCopy, r.err
}
func (r *RPCClient) SendRawTransaction(tx *transaction.Transaction) (util.Uint256, error) {
return r.hash, r.err
}
func (r *RPCClient) TerminateSession(sessionID uuid.UUID) (bool, error) {
return false, nil // Just a stub, unused by actor.
}
func (r *RPCClient) TraverseIterator(sessionID, iteratorID uuid.UUID, maxItemsCount int) ([]stackitem.Item, error) {
return nil, nil // Just a stub, unused by actor.
}
func (r *RPCClient) Context() context.Context {
if r.context == nil {
return context.Background()
}
return r.context
}
func (r *RPCClient) GetApplicationLog(hash util.Uint256, trig *trigger.Type) (*result.ApplicationLog, error) {
if r.appLog != nil {
return r.appLog, nil
}
return nil, errors.New("not found")
}
type AwaitableRPCClient struct {
RPCClient
@ -38,16 +100,16 @@ func (c *AwaitableRPCClient) ReceiveExecutions(flt *neorpc.ExecutionFilter, rcvr
func (c *AwaitableRPCClient) Unsubscribe(id string) error { return nil }
func TestNewWaiter(t *testing.T) {
w := newWaiter((RPCActor)(nil), nil)
_, ok := w.(NullWaiter)
w := waiter.New((actor.RPCActor)(nil), nil)
_, ok := w.(waiter.NullWaiter)
require.True(t, ok)
w = newWaiter(&RPCClient{}, &result.Version{})
_, ok = w.(*PollingWaiter)
w = waiter.New(&RPCClient{}, &result.Version{})
_, ok = w.(*waiter.PollingWaiter)
require.True(t, ok)
w = newWaiter(&AwaitableRPCClient{RPCClient: RPCClient{}}, &result.Version{})
_, ok = w.(*EventWaiter)
w = waiter.New(&AwaitableRPCClient{RPCClient: RPCClient{}}, &result.Version{})
_, ok = w.(*waiter.EventWaiter)
require.True(t, ok)
}
@ -58,8 +120,8 @@ func TestPollingWaiter_Wait(t *testing.T) {
expected := &state.AppExecResult{Container: h, Execution: state.Execution{}}
c := &RPCClient{appLog: appLog}
c.bCount.Store(bCount)
w := newWaiter(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time.
_, ok := w.(*PollingWaiter)
w := waiter.New(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time.
_, ok := w.(*waiter.PollingWaiter)
require.True(t, ok)
// Wait with error.
@ -75,7 +137,7 @@ func TestPollingWaiter_Wait(t *testing.T) {
// Missing AER after VUB.
c.appLog = nil
_, err = w.Wait(h, bCount-2, nil)
require.ErrorIs(t, ErrTxNotAccepted, err)
require.ErrorIs(t, waiter.ErrTxNotAccepted, err)
checkErr := func(t *testing.T, trigger func(), target error) {
errCh := make(chan error)
@ -106,14 +168,14 @@ func TestPollingWaiter_Wait(t *testing.T) {
// Tx is accepted before VUB.
c.appLog = nil
c.bCount.Store(bCount)
checkErr(t, func() { c.bCount.Store(bCount + 1) }, ErrTxNotAccepted)
checkErr(t, func() { c.bCount.Store(bCount + 1) }, waiter.ErrTxNotAccepted)
// Context is cancelled.
c.appLog = nil
c.bCount.Store(bCount)
ctx, cancel := context.WithCancel(context.Background())
c.context = ctx
checkErr(t, cancel, ErrContextDone)
checkErr(t, cancel, waiter.ErrContextDone)
}
func TestWSWaiter_Wait(t *testing.T) {
@ -123,8 +185,8 @@ func TestWSWaiter_Wait(t *testing.T) {
expected := &state.AppExecResult{Container: h, Execution: state.Execution{}}
c := &AwaitableRPCClient{RPCClient: RPCClient{appLog: appLog}}
c.bCount.Store(bCount)
w := newWaiter(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time.
_, ok := w.(*EventWaiter)
w := waiter.New(c, &result.Version{Protocol: result.Protocol{MillisecondsPerBlock: 1}}) // reduce testing time.
_, ok := w.(*waiter.EventWaiter)
require.True(t, ok)
// Wait with error.
@ -176,7 +238,7 @@ func TestWSWaiter_Wait(t *testing.T) {
// Missing AER after VUB.
go func() {
_, err = w.Wait(h, bCount-2, nil)
require.ErrorIs(t, err, ErrTxNotAccepted)
require.ErrorIs(t, err, waiter.ErrTxNotAccepted)
doneCh <- struct{}{}
}()
check(t, func() {
@ -185,3 +247,9 @@ func TestWSWaiter_Wait(t *testing.T) {
c.subBlockCh <- &block.Block{}
})
}
func TestRPCWaiterRPCClientCompat(t *testing.T) {
_ = waiter.RPCPollingWaiter(&rpcclient.Client{})
_ = waiter.RPCPollingWaiter(&rpcclient.WSClient{})
_ = waiter.RPCEventWaiter(&rpcclient.WSClient{})
}