Merge pull request #3223 from nspcc-dev/cancel_command
cli: cancel transaction command
This commit is contained in:
commit
7179efec35
7 changed files with 215 additions and 39 deletions
|
@ -32,6 +32,15 @@ const DefaultTimeout = 10 * time.Second
|
|||
// check for flag presence in the context.
|
||||
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
|
||||
// (privnet/mainnet/testnet).
|
||||
var Network = []cli.Flag{
|
||||
|
|
|
@ -37,7 +37,7 @@ func manifestAddGroup(ctx *cli.Context) error {
|
|||
|
||||
h := state.CreateContractHash(sender, nf.Checksum, m.Name)
|
||||
|
||||
gAcc, w, err := getAccFromContext(ctx)
|
||||
gAcc, w, err := GetAccFromContext(ctx)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Errorf("can't get account to sign group with: %w", err), 1)
|
||||
}
|
||||
|
|
|
@ -48,16 +48,7 @@ var (
|
|||
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")
|
||||
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,
|
||||
Usage: "address to use as transaction signee (and gas source)",
|
||||
}
|
||||
|
@ -100,14 +91,13 @@ func NewCommands() []cli.Command {
|
|||
testInvokeFunctionFlags := []cli.Flag{options.Historic}
|
||||
testInvokeFunctionFlags = append(testInvokeFunctionFlags, options.RPC...)
|
||||
invokeFunctionFlags := []cli.Flag{
|
||||
walletFlag,
|
||||
walletConfigFlag,
|
||||
addressFlag,
|
||||
txctx.GasFlag,
|
||||
txctx.SysGasFlag,
|
||||
txctx.OutFlag,
|
||||
txctx.ForceFlag,
|
||||
}
|
||||
invokeFunctionFlags = append(invokeFunctionFlags, options.Wallet...)
|
||||
invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...)
|
||||
deployFlags := append(invokeFunctionFlags, []cli.Flag{
|
||||
cli.StringFlag{
|
||||
|
@ -119,6 +109,24 @@ func NewCommands() []cli.Command {
|
|||
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{{
|
||||
Name: "contract",
|
||||
Usage: "compile - debug - deploy smart contracts",
|
||||
|
@ -301,26 +309,7 @@ func NewCommands() []cli.Command {
|
|||
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",
|
||||
Action: manifestAddGroup,
|
||||
Flags: []cli.Flag{
|
||||
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",
|
||||
},
|
||||
},
|
||||
Flags: manifestAddGroupFlags,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -581,7 +570,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
|
|||
w *wallet.Wallet
|
||||
)
|
||||
if signAndPush {
|
||||
acc, w, err = getAccFromContext(ctx)
|
||||
acc, w, err = GetAccFromContext(ctx)
|
||||
if err != nil {
|
||||
return cli.NewExitError(err, 1)
|
||||
}
|
||||
|
@ -757,7 +746,8 @@ func inspect(ctx *cli.Context) error {
|
|||
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
|
||||
|
||||
wPath := ctx.String("wallet")
|
||||
|
@ -789,11 +779,13 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error
|
|||
addr = wall.GetChangeAddress()
|
||||
}
|
||||
|
||||
acc, err := getUnlockedAccount(wall, addr, pass)
|
||||
acc, err := GetUnlockedAccount(wall, addr, pass)
|
||||
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)
|
||||
if acc == nil {
|
||||
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])
|
||||
}
|
||||
|
||||
acc, w, err := getAccFromContext(ctx)
|
||||
acc, w, err := GetAccFromContext(ctx)
|
||||
if err != nil {
|
||||
return cli.NewExitError(fmt.Errorf("can't get sender address: %w", err), 1)
|
||||
}
|
||||
|
|
74
cli/util/cancel.go
Normal file
74
cli/util/cancel.go
Normal 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
|
||||
}
|
|
@ -6,7 +6,9 @@ import (
|
|||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/nspcc-dev/neo-go/cli/flags"
|
||||
"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"
|
||||
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||
"github.com/urfave/cli"
|
||||
|
@ -15,6 +17,14 @@ import (
|
|||
// NewCommands returns util commands for neo-go CLI.
|
||||
func NewCommands() []cli.Command {
|
||||
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{
|
||||
{
|
||||
Name: "util",
|
||||
|
@ -41,6 +51,24 @@ func NewCommands() []cli.Command {
|
|||
Action: sendTx,
|
||||
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",
|
||||
Usage: "Dump transaction stored in file",
|
||||
|
|
|
@ -4,9 +4,12 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/wallet"
|
||||
"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
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -214,7 +214,7 @@ and we're not accepting issues related to them.
|
|||
|
||||
| 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 |
|
||||
| `dumpprivkey` | Shouldn't exist for security reasons, see `closewallet` comment also |
|
||||
| `getnewaddress` | See `closewallet` comment, use CLI to do that |
|
||||
|
|
Loading…
Reference in a new issue