Skip to content

Commit 199c617

Browse files
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>
1 parent 7a2eb32 commit 199c617

File tree

4 files changed

+178
-1
lines changed

4 files changed

+178
-1
lines changed

cli/util/cancel.go

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package util
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/nspcc-dev/neo-go/cli/flags"
8+
"github.com/nspcc-dev/neo-go/cli/options"
9+
"github.com/nspcc-dev/neo-go/cli/smartcontract"
10+
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
11+
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
12+
"github.com/nspcc-dev/neo-go/pkg/rpcclient/actor"
13+
"github.com/nspcc-dev/neo-go/pkg/util"
14+
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
15+
"github.com/urfave/cli"
16+
)
17+
18+
func cancelTx(ctx *cli.Context) error {
19+
args := ctx.Args()
20+
if len(args) == 0 {
21+
return cli.NewExitError("transaction hash is missing", 1)
22+
} else if len(args) > 1 {
23+
return cli.NewExitError("only one transaction hash is accepted", 1)
24+
}
25+
26+
txHash, err := util.Uint256DecodeStringLE(strings.TrimPrefix(args[0], "0x"))
27+
if err != nil {
28+
return cli.NewExitError(fmt.Sprintf("invalid tx hash: %s", args[0]), 1)
29+
}
30+
31+
gctx, cancel := options.GetTimeoutContext(ctx)
32+
defer cancel()
33+
34+
c, err := options.GetRPCClient(gctx, ctx)
35+
if err != nil {
36+
return cli.NewExitError(fmt.Errorf("failed to create RPC client: %w", err), 1)
37+
}
38+
39+
mainTx, _ := c.GetRawTransactionVerbose(txHash)
40+
if mainTx != nil && !mainTx.Blockhash.Equals(util.Uint256{}) {
41+
return cli.NewExitError(fmt.Errorf("transaction %s is already accepted at block %s", txHash, mainTx.Blockhash.StringLE()), 1)
42+
}
43+
acc, w, err := smartcontract.GetAccFromContext(ctx)
44+
if err != nil {
45+
return cli.NewExitError(fmt.Errorf("failed to get account from context to sign the conflicting transaction: %w", err), 1)
46+
}
47+
defer w.Close()
48+
49+
if mainTx != nil && !mainTx.HasSigner(acc.ScriptHash()) {
50+
return cli.NewExitError(fmt.Errorf("account %s is not a signer of the conflicting transaction", acc.Address), 1)
51+
}
52+
53+
a, err := actor.NewSimple(c, acc)
54+
if err != nil {
55+
return cli.NewExitError(fmt.Errorf("failed to create Actor: %w", err), 1)
56+
}
57+
58+
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 {
59+
err := actor.DefaultCheckerModifier(r, t)
60+
if err != nil {
61+
return err
62+
}
63+
if mainTx != nil && t.NetworkFee < mainTx.NetworkFee+1 {
64+
t.NetworkFee = mainTx.NetworkFee + 1
65+
}
66+
if mainTx != nil && ctx.IsSet("gas") {
67+
t.NetworkFee += int64(flags.Fixed8FromContext(ctx, "gas"))
68+
}
69+
return nil
70+
})
71+
if err != nil {
72+
return cli.NewExitError(fmt.Errorf("failed to send conflicting transaction: %w", err), 1)
73+
}
74+
fmt.Fprintln(ctx.App.Writer, resHash.StringLE())
75+
return nil
76+
}

cli/util/convert.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ import (
66
"fmt"
77
"os"
88

9+
"github.com/nspcc-dev/neo-go/cli/flags"
910
"github.com/nspcc-dev/neo-go/cli/options"
11+
"github.com/nspcc-dev/neo-go/cli/txctx"
1012
vmcli "github.com/nspcc-dev/neo-go/cli/vm"
1113
"github.com/nspcc-dev/neo-go/pkg/vm"
1214
"github.com/urfave/cli"
@@ -15,6 +17,14 @@ import (
1517
// NewCommands returns util commands for neo-go CLI.
1618
func NewCommands() []cli.Command {
1719
txDumpFlags := append([]cli.Flag{}, options.RPC...)
20+
txCancelFlags := append([]cli.Flag{
21+
flags.AddressFlag{
22+
Name: "address, a",
23+
Usage: "address to use as conflicting transaction signee (and gas source)",
24+
},
25+
txctx.GasFlag,
26+
}, options.RPC...)
27+
txCancelFlags = append(txCancelFlags, options.Wallet...)
1828
return []cli.Command{
1929
{
2030
Name: "util",
@@ -41,6 +51,24 @@ func NewCommands() []cli.Command {
4151
Action: sendTx,
4252
Flags: txDumpFlags,
4353
},
54+
{
55+
Name: "canceltx",
56+
Usage: "Cancel transaction by sending conflicting transaction",
57+
UsageText: "canceltx <txid> -r <endpoint> --wallet <wallet> [--account <account>] [--wallet-config <path>] [--gas <gas>]",
58+
Description: `Aims to prevent a transaction from being added to the blockchain by dispatching a more
59+
prioritized conflicting transaction to the specified RPC node. The input for this command should
60+
be the transaction hash. If another account is not specified, the conflicting transaction is
61+
automatically generated and signed by the default account in the wallet. If the target transaction
62+
is in the memory pool of the provided RPC node, the NetworkFee value of the conflicting transaction
63+
is set to the target transaction's NetworkFee value plus one (if it's sufficient for the
64+
conflicting transaction itself). If the target transaction is not in the memory pool, standard
65+
NetworkFee calculations are performed based on the calculatenetworkfee RPC request. If the --gas
66+
flag is included, the specified value is added to the resulting conflicting transaction network fee
67+
in both scenarios.
68+
`,
69+
Action: cancelTx,
70+
Flags: txCancelFlags,
71+
},
4472
{
4573
Name: "txdump",
4674
Usage: "Dump transaction stored in file",

cli/util/util_test.go

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ import (
44
"os"
55
"path/filepath"
66
"testing"
7+
"time"
78

89
"github.com/nspcc-dev/neo-go/internal/testcli"
10+
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
911
"github.com/nspcc-dev/neo-go/pkg/util"
12+
"github.com/nspcc-dev/neo-go/pkg/wallet"
1013
"github.com/stretchr/testify/require"
1114
)
1215

@@ -63,3 +66,73 @@ func TestUtilOps(t *testing.T) {
6366
e.Run(t, "neo-go", "util", "ops", "--hex", "--in", tmp) // hex from file
6467
check(t)
6568
}
69+
70+
func TestUtilCancelTx(t *testing.T) {
71+
e := testcli.NewExecutorSuspended(t)
72+
73+
w, err := wallet.NewWalletFromFile("../testdata/testwallet.json")
74+
require.NoError(t, err)
75+
76+
transferArgs := []string{
77+
"neo-go", "wallet", "nep17", "transfer",
78+
"--rpc-endpoint", "http://" + e.RPC.Addresses()[0],
79+
"--wallet", testcli.ValidatorWallet,
80+
"--to", w.Accounts[0].Address,
81+
"--token", "NEO",
82+
"--from", testcli.ValidatorAddr,
83+
"--force",
84+
}
85+
args := []string{"neo-go", "util", "canceltx",
86+
"-r", "http://" + e.RPC.Addresses()[0],
87+
"--wallet", testcli.ValidatorWallet,
88+
"--address", testcli.ValidatorAddr}
89+
90+
e.In.WriteString("one\r")
91+
e.Run(t, append(transferArgs, "--amount", "1")...)
92+
line := e.GetNextLine(t)
93+
txHash, err := util.Uint256DecodeStringLE(line)
94+
require.NoError(t, err)
95+
96+
_, ok := e.Chain.GetMemPool().TryGetValue(txHash)
97+
require.True(t, ok)
98+
99+
t.Run("invalid", func(t *testing.T) {
100+
t.Run("missing tx argument", func(t *testing.T) {
101+
e.RunWithError(t, args...)
102+
})
103+
t.Run("excessive arguments", func(t *testing.T) {
104+
e.RunWithError(t, append(args, txHash.StringLE(), txHash.StringLE())...)
105+
})
106+
t.Run("invalid hash", func(t *testing.T) {
107+
e.RunWithError(t, append(args, "notahash")...)
108+
})
109+
t.Run("not signed by main signer", func(t *testing.T) {
110+
e.In.WriteString("one\r")
111+
e.RunWithError(t, "neo-go", "util", "canceltx",
112+
"-r", "http://"+e.RPC.Addresses()[0],
113+
"--wallet", testcli.ValidatorWallet,
114+
"--address", testcli.MultisigAddr, txHash.StringLE())
115+
})
116+
t.Run("wrong rpc endpoint", func(t *testing.T) {
117+
e.In.WriteString("one\r")
118+
e.RunWithError(t, "neo-go", "util", "canceltx",
119+
"-r", "http://localhost:20331",
120+
"--wallet", testcli.ValidatorWallet, txHash.StringLE())
121+
})
122+
})
123+
124+
e.In.WriteString("one\r")
125+
e.Run(t, append(args, txHash.StringLE())...)
126+
resHash, err := util.Uint256DecodeStringLE(e.GetNextLine(t))
127+
require.NoError(t, err)
128+
129+
_, _, err = e.Chain.GetTransaction(resHash)
130+
require.NoError(t, err)
131+
e.CheckEOF(t)
132+
go e.Chain.Run()
133+
134+
require.Eventually(t, func() bool {
135+
_, aerErr := e.Chain.GetAppExecResults(resHash, trigger.Application)
136+
return aerErr == nil
137+
}, time.Second*2, time.Millisecond*50)
138+
}

docs/rpc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ and we're not accepting issues related to them.
214214

215215
| Method | Reason |
216216
| ------- | ------------|
217-
| `canceltransaction` | Doesn't fit neo-go wallet model |
217+
| `canceltransaction` | Doesn't fit neo-go wallet model, use CLI to do that (`neo-go util canceltx`) |
218218
| `closewallet` | Doesn't fit neo-go wallet model |
219219
| `dumpprivkey` | Shouldn't exist for security reasons, see `closewallet` comment also |
220220
| `getnewaddress` | See `closewallet` comment, use CLI to do that |

0 commit comments

Comments
 (0)