cli: cancel transaction command
CLI side method for canceling not yet accepted transaction. It's alternative to unsupported `canceltransaction` RPC method. Close #3151. Signed-off-by: Ekaterina Pavlova <ekt@morphbits.io>
This commit is contained in:
parent
7a2eb32c42
commit
fc77754098
4 changed files with 176 additions and 1 deletions
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"
|
"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",
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
|
@ -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 |
|
||||||
|
|
Loading…
Reference in a new issue