From 897c9198f85ae212abfb645b1ae9b718e47b2890 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Fri, 2 Oct 2020 16:13:17 +0300 Subject: [PATCH] cli: allow to send multisig deploy/invoke tx It allows to invoke native contracts as committee from CLI in privnet, e.g. to set new oracle nodes. Also don't require `out` flag in `multisig sign` if tx is to be pushed. --- cli/multisig_test.go | 27 +++++++++++++++ cli/paramcontext/context.go | 53 +++++++++++++++++++++++++++++ cli/smartcontract/smart_contract.go | 23 ++++++++++++- cli/wallet/multisig.go | 30 ++-------------- cli/wallet/nep5.go | 21 ++---------- 5 files changed, 108 insertions(+), 46 deletions(-) create mode 100644 cli/paramcontext/context.go diff --git a/cli/multisig_test.go b/cli/multisig_test.go index 24e9f9762..1e3e87093 100644 --- a/cli/multisig_test.go +++ b/cli/multisig_test.go @@ -5,11 +5,13 @@ import ( "math/big" "os" "path" + "strings" "testing" "github.com/nspcc-dev/neo-go/pkg/crypto/hash" "github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/encoding/address" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/stretchr/testify/require" ) @@ -82,4 +84,29 @@ func TestSignMultisigTx(t *testing.T) { require.Equal(t, big.NewInt(1), b) b, _ = e.Chain.GetGoverningTokenBalance(multisigHash) require.Equal(t, big.NewInt(3), b) + + t.Run("via invokefunction", func(t *testing.T) { + e.In.WriteString("pass\r") + e.Run(t, "neo-go", "contract", "invokefunction", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", wallet1Path, "--address", multisigAddr, + "--out", txPath, + client.NeoContractHash.StringLE(), "transfer", + "bytes:"+multisigHash.StringBE(), + "bytes:"+priv.GetScriptHash().StringBE(), + "int:1", + "--", strings.Join([]string{multisigHash.StringLE(), ":", "Global"}, "")) + + e.In.WriteString("pass\r") + e.Run(t, "neo-go", "wallet", "multisig", "sign", + "--unittest", "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", wallet2Path, "--address", multisigAddr, + "--in", txPath, "--out", txPath) + e.checkTxPersisted(t) + + b, _ := e.Chain.GetGoverningTokenBalance(priv.GetScriptHash()) + require.Equal(t, big.NewInt(2), b) + b, _ = e.Chain.GetGoverningTokenBalance(multisigHash) + require.Equal(t, big.NewInt(2), b) + }) } diff --git a/cli/paramcontext/context.go b/cli/paramcontext/context.go new file mode 100644 index 000000000..f1bc55e9c --- /dev/null +++ b/cli/paramcontext/context.go @@ -0,0 +1,53 @@ +package paramcontext + +import ( + "encoding/json" + "fmt" + "io/ioutil" + + "github.com/nspcc-dev/neo-go/pkg/core/transaction" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/context" + "github.com/nspcc-dev/neo-go/pkg/wallet" +) + +// validUntilBlockIncrement is the number of extra blocks to add to an exported transaction +const validUntilBlockIncrement = 50 + +// InitAndSave creates incompletely signed transaction which can used +// as input to `multisig sign`. +func InitAndSave(tx *transaction.Transaction, acc *wallet.Account, filename string) error { + // avoid fast transaction expiration + tx.ValidUntilBlock += validUntilBlockIncrement + priv := acc.PrivateKey() + pub := priv.PublicKey() + sign := priv.Sign(tx.GetSignedPart()) + scCtx := context.NewParameterContext("Neo.Core.ContractTransaction", tx) + if err := scCtx.AddSignature(acc.Contract, pub, sign); err != nil { + return fmt.Errorf("can't add signature: %w", err) + } + return Save(scCtx, filename) +} + +// Read reads parameter context from file. +func Read(filename string) (*context.ParameterContext, error) { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, fmt.Errorf("can't read input file: %w", err) + } + + c := new(context.ParameterContext) + if err := json.Unmarshal(data, c); err != nil { + return nil, fmt.Errorf("can't parse transaction: %w", err) + } + return c, nil +} + +// Save writes parameter context to file. +func Save(c *context.ParameterContext, filename string) error { + if data, err := json.Marshal(c); err != nil { + return fmt.Errorf("can't marshal transaction: %w", err) + } else if err := ioutil.WriteFile(filename, data, 0644); err != nil { + return fmt.Errorf("can't write transaction to file: %w", err) + } + return nil +} diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index f9b6c577f..dcc4cf6d1 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -13,6 +13,7 @@ import ( "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/options" + "github.com/nspcc-dev/neo-go/cli/paramcontext" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" @@ -50,6 +51,10 @@ var ( Name: "gas, g", Usage: "gas to add to the transaction", } + outFlag = cli.StringFlag{ + Name: "out", + Usage: "file to put JSON transaction to", + } ) const ( @@ -104,6 +109,7 @@ func NewCommands() []cli.Command { walletFlag, addressFlag, gasFlag, + outFlag, } invokeFunctionFlags = append(invokeFunctionFlags, options.RPC...) return []cli.Command{{ @@ -156,7 +162,7 @@ func NewCommands() []cli.Command { { Name: "invokefunction", Usage: "invoke deployed contract on the blockchain", - UsageText: "neo-go contract invokefunction -r endpoint -w wallet [-a address] [-g gas] scripthash [method] [arguments...] [--] [signers...]", + UsageText: "neo-go contract invokefunction -r endpoint -w wallet [-a address] [-g gas] [--out file] scripthash [method] [arguments...] [--] [signers...]", Description: `Executes given (as a script hash) deployed script with the given method, arguments and signers. Sender is included in the list of signers by default with None witness scope. If you'd like to change default sender's scope, @@ -481,6 +487,21 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { if err != nil { return cli.NewExitError(err, 1) } + if out := ctx.String("out"); out != "" { + script, err := hex.DecodeString(resp.Script) + if err != nil { + return cli.NewExitError(fmt.Errorf("bad script returned from the RPC node: %w", err), 1) + } + tx, err := c.CreateTxFromScript(script, acc, resp.GasConsumed, int64(gas), cosigners...) + if err != nil { + return cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1) + } + if err := paramcontext.InitAndSave(tx, acc, out); err != nil { + return cli.NewExitError(err, 1) + } + fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) + return nil + } if signAndPush { if len(resp.Script) == 0 { return cli.NewExitError(errors.New("no script returned from the RPC node"), 1) diff --git a/cli/wallet/multisig.go b/cli/wallet/multisig.go index 165f3079e..bda83a331 100644 --- a/cli/wallet/multisig.go +++ b/cli/wallet/multisig.go @@ -1,14 +1,12 @@ package wallet import ( - "encoding/json" "fmt" - "io/ioutil" "github.com/nspcc-dev/neo-go/cli/options" + "github.com/nspcc-dev/neo-go/cli/paramcontext" "github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/encoding/address" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/context" "github.com/urfave/cli" ) @@ -41,7 +39,7 @@ func signMultisig(ctx *cli.Context) error { } defer wall.Close() - c, err := readParameterContext(ctx.String("in")) + c, err := paramcontext.Read(ctx.String("in")) if err != nil { return cli.NewExitError(err, 1) } @@ -66,7 +64,7 @@ func signMultisig(ctx *cli.Context) error { return cli.NewExitError(fmt.Errorf("can't add signature: %w", err), 1) } if out := ctx.String("out"); out != "" { - if err := writeParameterContext(c, out); err != nil { + if err := paramcontext.Save(c, out); err != nil { return cli.NewExitError(err, 1) } } @@ -95,25 +93,3 @@ func signMultisig(ctx *cli.Context) error { fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) return nil } - -func readParameterContext(filename string) (*context.ParameterContext, error) { - data, err := ioutil.ReadFile(filename) - if err != nil { - return nil, fmt.Errorf("can't read input file: %w", err) - } - - c := new(context.ParameterContext) - if err := json.Unmarshal(data, c); err != nil { - return nil, fmt.Errorf("can't parse transaction: %w", err) - } - return c, nil -} - -func writeParameterContext(c *context.ParameterContext, filename string) error { - if data, err := json.Marshal(c); err != nil { - return fmt.Errorf("can't marshal transaction: %w", err) - } else if err := ioutil.WriteFile(filename, data, 0644); err != nil { - return fmt.Errorf("can't write transaction to file: %w", err) - } - return nil -} diff --git a/cli/wallet/nep5.go b/cli/wallet/nep5.go index 3a61994f3..961286c8e 100644 --- a/cli/wallet/nep5.go +++ b/cli/wallet/nep5.go @@ -1,25 +1,20 @@ package wallet import ( - "encoding/json" "errors" "fmt" - "io/ioutil" "strings" "github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/options" + "github.com/nspcc-dev/neo-go/cli/paramcontext" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/rpc/client" - "github.com/nspcc-dev/neo-go/pkg/smartcontract/context" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" ) -// validUntilBlockIncrement is the number of extra blocks to add to an exported transaction -const validUntilBlockIncrement = 50 - var ( neoToken = wallet.NewToken(client.NeoContractHash, "NEO", "neo", 0) gasToken = wallet.NewToken(client.GasContractHash, "GAS", "gas", 8) @@ -474,18 +469,8 @@ func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account } if outFile := ctx.String("out"); outFile != "" { - // avoid fast transaction expiration - tx.ValidUntilBlock += validUntilBlockIncrement - priv := acc.PrivateKey() - pub := priv.PublicKey() - sign := priv.Sign(tx.GetSignedPart()) - scCtx := context.NewParameterContext("Neo.Core.ContractTransaction", tx) - if err := scCtx.AddSignature(acc.Contract, pub, sign); err != nil { - return cli.NewExitError(fmt.Errorf("can't add signature: %w", err), 1) - } else if data, err := json.Marshal(scCtx); err != nil { - return cli.NewExitError(fmt.Errorf("can't marshal tx to JSON: %w", err), 1) - } else if err := ioutil.WriteFile(outFile, data, 0644); err != nil { - return cli.NewExitError(fmt.Errorf("can't write tx to file: %w", err), 1) + if err := paramcontext.InitAndSave(tx, acc, outFile); err != nil { + return cli.NewExitError(err, 1) } } else { _ = acc.SignTx(tx)