Skip to content

Commit

Permalink
Merge pull request #3223 from nspcc-dev/cancel_command
Browse files Browse the repository at this point in the history
cli: cancel transaction command
  • Loading branch information
roman-khimov authored Nov 27, 2023
2 parents 7a1bf77 + fc77754 commit 7179efe
Show file tree
Hide file tree
Showing 7 changed files with 215 additions and 39 deletions.
9 changes: 9 additions & 0 deletions cli/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
2 changes: 1 addition & 1 deletion cli/smartcontract/manifest.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
66 changes: 29 additions & 37 deletions cli/smartcontract/smart_contract.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)",
}
Expand Down Expand Up @@ -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{
Expand All @@ -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",
Expand Down Expand Up @@ -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,
},
},
},
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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))
Expand Down Expand Up @@ -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)
}
Expand Down
74 changes: 74 additions & 0 deletions cli/util/cancel.go
Original file line number Diff line number Diff line change
@@ -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
}
28 changes: 28 additions & 0 deletions cli/util/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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",
Expand All @@ -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",
Expand Down
73 changes: 73 additions & 0 deletions cli/util/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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)
}
2 changes: 1 addition & 1 deletion docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down

0 comments on commit 7179efe

Please sign in to comment.