Merge pull request #3223 from nspcc-dev/cancel_command

cli: cancel transaction command
This commit is contained in:
Roman Khimov 2023-11-27 17:32:48 +03:00 committed by GitHub
commit 7179efec35
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 215 additions and 39 deletions

View file

@ -32,6 +32,15 @@ const DefaultTimeout = 10 * time.Second
// check for flag presence in the context. // check for flag presence in the context.
const RPCEndpointFlag = "rpc-endpoint" const RPCEndpointFlag = "rpc-endpoint"
// Wallet is a set of flags used for wallet operations.
var Wallet = []cli.Flag{cli.StringFlag{
Name: "wallet, w",
Usage: "wallet to use to get the key for transaction signing; conflicts with --wallet-config flag",
}, cli.StringFlag{
Name: "wallet-config",
Usage: "path to wallet config to use to get the key for transaction signing; conflicts with --wallet flag"},
}
// Network is a set of flags for choosing the network to operate on // Network is a set of flags for choosing the network to operate on
// (privnet/mainnet/testnet). // (privnet/mainnet/testnet).
var Network = []cli.Flag{ var Network = []cli.Flag{

View file

@ -37,7 +37,7 @@ func manifestAddGroup(ctx *cli.Context) error {
h := state.CreateContractHash(sender, nf.Checksum, m.Name) h := state.CreateContractHash(sender, nf.Checksum, m.Name)
gAcc, w, err := getAccFromContext(ctx) gAcc, w, err := GetAccFromContext(ctx)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("can't get account to sign group with: %w", err), 1) return cli.NewExitError(fmt.Errorf("can't get account to sign group with: %w", err), 1)
} }

View file

@ -48,15 +48,6 @@ var (
errNoScriptHash = errors.New("no smart contract hash was provided, specify one as the first argument") errNoScriptHash = errors.New("no smart contract hash was provided, specify one as the first argument")
errNoSmartContractName = errors.New("no name was provided, specify the '--name or -n' flag") errNoSmartContractName = errors.New("no name was provided, specify the '--name or -n' flag")
errFileExist = errors.New("A file with given smart-contract name already exists") errFileExist = errors.New("A file with given smart-contract name already exists")
walletFlag = cli.StringFlag{
Name: "wallet, w",
Usage: "wallet to use to get the key for transaction signing; conflicts with --wallet-config flag",
}
walletConfigFlag = cli.StringFlag{
Name: "wallet-config",
Usage: "path to wallet config to use to get the key for transaction signing; conflicts with --wallet flag",
}
addressFlag = flags.AddressFlag{ addressFlag = flags.AddressFlag{
Name: addressFlagName, Name: addressFlagName,
Usage: "address to use as transaction signee (and gas source)", Usage: "address to use as transaction signee (and gas source)",
@ -100,14 +91,13 @@ func NewCommands() []cli.Command {
testInvokeFunctionFlags := []cli.Flag{options.Historic} testInvokeFunctionFlags := []cli.Flag{options.Historic}
testInvokeFunctionFlags = append(testInvokeFunctionFlags, options.RPC...) testInvokeFunctionFlags = append(testInvokeFunctionFlags, options.RPC...)
invokeFunctionFlags := []cli.Flag{ invokeFunctionFlags := []cli.Flag{
walletFlag,
walletConfigFlag,
addressFlag, addressFlag,
txctx.GasFlag, txctx.GasFlag,
txctx.SysGasFlag, txctx.SysGasFlag,
txctx.OutFlag, txctx.OutFlag,
txctx.ForceFlag, txctx.ForceFlag,
} }
invokeFunctionFlags = append(invokeFunctionFlags, options.Wallet...)
invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...) invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...)
deployFlags := append(invokeFunctionFlags, []cli.Flag{ deployFlags := append(invokeFunctionFlags, []cli.Flag{
cli.StringFlag{ cli.StringFlag{
@ -119,6 +109,24 @@ func NewCommands() []cli.Command {
Usage: "Manifest input file (*.manifest.json)", Usage: "Manifest input file (*.manifest.json)",
}, },
}...) }...)
manifestAddGroupFlags := append([]cli.Flag{
cli.StringFlag{
Name: "sender, s",
Usage: "deploy transaction sender",
},
flags.AddressFlag{
Name: addressFlagName, // use the same name for handler code unification.
Usage: "account to sign group with",
},
cli.StringFlag{
Name: "nef, n",
Usage: "path to the NEF file",
},
cli.StringFlag{
Name: "manifest, m",
Usage: "path to the manifest",
},
}, options.Wallet...)
return []cli.Command{{ return []cli.Command{{
Name: "contract", Name: "contract",
Usage: "compile - debug - deploy smart contracts", Usage: "compile - debug - deploy smart contracts",
@ -301,26 +309,7 @@ func NewCommands() []cli.Command {
Usage: "adds group to the manifest", Usage: "adds group to the manifest",
UsageText: "neo-go contract manifest add-group -w wallet [--wallet-config path] -n nef -m manifest -a address -s address", UsageText: "neo-go contract manifest add-group -w wallet [--wallet-config path] -n nef -m manifest -a address -s address",
Action: manifestAddGroup, Action: manifestAddGroup,
Flags: []cli.Flag{ Flags: manifestAddGroupFlags,
walletFlag,
walletConfigFlag,
cli.StringFlag{
Name: "sender, s",
Usage: "deploy transaction sender",
},
flags.AddressFlag{
Name: addressFlagName, // use the same name for handler code unification.
Usage: "account to sign group with",
},
cli.StringFlag{
Name: "nef, n",
Usage: "path to the NEF file",
},
cli.StringFlag{
Name: "manifest, m",
Usage: "path to the manifest",
},
},
}, },
}, },
}, },
@ -581,7 +570,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
w *wallet.Wallet w *wallet.Wallet
) )
if signAndPush { if signAndPush {
acc, w, err = getAccFromContext(ctx) acc, w, err = GetAccFromContext(ctx)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
@ -757,7 +746,8 @@ func inspect(ctx *cli.Context) error {
return nil return nil
} }
func getAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error) { // GetAccFromContext returns account and wallet from context. If address is not set, default address is used.
func GetAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error) {
var addr util.Uint160 var addr util.Uint160
wPath := ctx.String("wallet") wPath := ctx.String("wallet")
@ -789,11 +779,13 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error
addr = wall.GetChangeAddress() addr = wall.GetChangeAddress()
} }
acc, err := getUnlockedAccount(wall, addr, pass) acc, err := GetUnlockedAccount(wall, addr, pass)
return acc, wall, err return acc, wall, err
} }
func getUnlockedAccount(wall *wallet.Wallet, addr util.Uint160, pass *string) (*wallet.Account, error) { // GetUnlockedAccount returns account from wallet, address and uses pass to unlock specified account if given.
// If the password is not given, then it is requested from user.
func GetUnlockedAccount(wall *wallet.Wallet, addr util.Uint160, pass *string) (*wallet.Account, error) {
acc := wall.GetAccount(addr) acc := wall.GetAccount(addr)
if acc == nil { if acc == nil {
return nil, fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)) return nil, fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr))
@ -844,7 +836,7 @@ func contractDeploy(ctx *cli.Context) error {
appCallParams = append(appCallParams, data[0]) appCallParams = append(appCallParams, data[0])
} }
acc, w, err := getAccFromContext(ctx) acc, w, err := GetAccFromContext(ctx)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("can't get sender address: %w", err), 1) return cli.NewExitError(fmt.Errorf("can't get sender address: %w", err), 1)
} }

74
cli/util/cancel.go Normal file
View file

@ -0,0 +1,74 @@
package util
import (
"fmt"
"strings"
"github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/cli/smartcontract"
"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/util"
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
"github.com/urfave/cli"
)
func cancelTx(ctx *cli.Context) error {
args := ctx.Args()
if len(args) == 0 {
return cli.NewExitError("transaction hash is missing", 1)
} else if len(args) > 1 {
return cli.NewExitError("only one transaction hash is accepted", 1)
}
txHash, err := util.Uint256DecodeStringLE(strings.TrimPrefix(args[0], "0x"))
if err != nil {
return cli.NewExitError(fmt.Sprintf("invalid tx hash: %s", args[0]), 1)
}
gctx, cancel := options.GetTimeoutContext(ctx)
defer cancel()
c, err := options.GetRPCClient(gctx, ctx)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to create RPC client: %w", err), 1)
}
mainTx, _ := c.GetRawTransactionVerbose(txHash)
if mainTx != nil && !mainTx.Blockhash.Equals(util.Uint256{}) {
return cli.NewExitError(fmt.Errorf("transaction %s is already accepted at block %s", txHash, mainTx.Blockhash.StringLE()), 1)
}
acc, w, err := smartcontract.GetAccFromContext(ctx)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to get account from context to sign the conflicting transaction: %w", err), 1)
}
defer w.Close()
if mainTx != nil && !mainTx.HasSigner(acc.ScriptHash()) {
return cli.NewExitError(fmt.Errorf("account %s is not a signer of the conflicting transaction", acc.Address), 1)
}
a, err := actor.NewSimple(c, acc)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to create Actor: %w", err), 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 {
err := actor.DefaultCheckerModifier(r, t)
if err != nil {
return err
}
if mainTx != nil && t.NetworkFee < mainTx.NetworkFee+1 {
t.NetworkFee = mainTx.NetworkFee + 1
}
t.NetworkFee += int64(flags.Fixed8FromContext(ctx, "gas"))
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())
return nil
}

View file

@ -6,7 +6,9 @@ import (
"fmt" "fmt"
"os" "os"
"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"
vmcli "github.com/nspcc-dev/neo-go/cli/vm" vmcli "github.com/nspcc-dev/neo-go/cli/vm"
"github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -15,6 +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...)
txCancelFlags := append([]cli.Flag{
flags.AddressFlag{
Name: "address, a",
Usage: "address to use as conflicting transaction signee (and gas source)",
},
txctx.GasFlag,
}, options.RPC...)
txCancelFlags = append(txCancelFlags, options.Wallet...)
return []cli.Command{ return []cli.Command{
{ {
Name: "util", Name: "util",
@ -41,6 +51,24 @@ func NewCommands() []cli.Command {
Action: sendTx, Action: sendTx,
Flags: txDumpFlags, Flags: txDumpFlags,
}, },
{
Name: "canceltx",
Usage: "Cancel transaction by sending conflicting transaction",
UsageText: "canceltx <txid> -r <endpoint> --wallet <wallet> [--account <account>] [--wallet-config <path>] [--gas <gas>]",
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
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.
`,
Action: cancelTx,
Flags: txCancelFlags,
},
{ {
Name: "txdump", Name: "txdump",
Usage: "Dump transaction stored in file", Usage: "Dump transaction stored in file",

View file

@ -4,9 +4,12 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"time"
"github.com/nspcc-dev/neo-go/internal/testcli" "github.com/nspcc-dev/neo-go/internal/testcli"
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -63,3 +66,73 @@ func TestUtilOps(t *testing.T) {
e.Run(t, "neo-go", "util", "ops", "--hex", "--in", tmp) // hex from file e.Run(t, "neo-go", "util", "ops", "--hex", "--in", tmp) // hex from file
check(t) check(t)
} }
func TestUtilCancelTx(t *testing.T) {
e := testcli.NewExecutorSuspended(t)
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}
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)
t.Run("invalid", func(t *testing.T) {
t.Run("missing tx argument", func(t *testing.T) {
e.RunWithError(t, args...)
})
t.Run("excessive arguments", func(t *testing.T) {
e.RunWithError(t, append(args, txHash.StringLE(), txHash.StringLE())...)
})
t.Run("invalid hash", func(t *testing.T) {
e.RunWithError(t, append(args, "notahash")...)
})
t.Run("not signed by main signer", func(t *testing.T) {
e.In.WriteString("one\r")
e.RunWithError(t, "neo-go", "util", "canceltx",
"-r", "http://"+e.RPC.Addresses()[0],
"--wallet", testcli.ValidatorWallet,
"--address", testcli.MultisigAddr, txHash.StringLE())
})
t.Run("wrong rpc endpoint", func(t *testing.T) {
e.In.WriteString("one\r")
e.RunWithError(t, "neo-go", "util", "canceltx",
"-r", "http://localhost:20331",
"--wallet", testcli.ValidatorWallet, txHash.StringLE())
})
})
e.In.WriteString("one\r")
e.Run(t, append(args, txHash.StringLE())...)
resHash, err := util.Uint256DecodeStringLE(e.GetNextLine(t))
require.NoError(t, err)
_, _, err = e.Chain.GetTransaction(resHash)
require.NoError(t, err)
e.CheckEOF(t)
go e.Chain.Run()
require.Eventually(t, func() bool {
_, aerErr := e.Chain.GetAppExecResults(resHash, trigger.Application)
return aerErr == nil
}, time.Second*2, time.Millisecond*50)
}

View file

@ -214,7 +214,7 @@ and we're not accepting issues related to them.
| Method | Reason | | Method | Reason |
| ------- | ------------| | ------- | ------------|
| `canceltransaction` | Doesn't fit neo-go wallet model | | `canceltransaction` | Doesn't fit neo-go wallet model, use CLI to do that (`neo-go util canceltx`) |
| `closewallet` | Doesn't fit neo-go wallet model | | `closewallet` | Doesn't fit neo-go wallet model |
| `dumpprivkey` | Shouldn't exist for security reasons, see `closewallet` comment also | | `dumpprivkey` | Shouldn't exist for security reasons, see `closewallet` comment also |
| `getnewaddress` | See `closewallet` comment, use CLI to do that | | `getnewaddress` | See `closewallet` comment, use CLI to do that |