diff --git a/cli/contract_test.go b/cli/contract_test.go index 56daf0a03..87fee7919 100644 --- a/cli/contract_test.go +++ b/cli/contract_test.go @@ -14,6 +14,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/interop/storage" "github.com/nspcc-dev/neo-go/pkg/core/state" + "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/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" @@ -22,6 +23,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" + "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/stretchr/testify/require" ) @@ -275,6 +277,78 @@ func TestComlileAndInvokeFunction(t *testing.T) { }) }) + t.Run("real invoke and save tx", func(t *testing.T) { + txout := path.Join(tmpDir, "test_contract_tx.json") + + nefName = path.Join(tmpDir, "verify.nef") + manifestName = path.Join(tmpDir, "verify.manifest.json") + e.Run(t, "neo-go", "contract", "compile", + "--in", "testdata/verify.go", + "--config", "testdata/verify.yml", + "--out", nefName, "--manifest", manifestName) + + e.In.WriteString("one\r") + e.Run(t, "neo-go", "contract", "deploy", + "--rpc-endpoint", "http://"+e.RPC.Addr, + "--wallet", validatorWallet, "--address", validatorAddr, + "--in", nefName, "--manifest", manifestName) + + line, err := e.Out.ReadString('\n') + require.NoError(t, err) + line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: ")) + hVerify, err := util.Uint160DecodeStringLE(line) + require.NoError(t, err) + e.checkTxPersisted(t) + + cmd = []string{"neo-go", "contract", "invokefunction", + "--rpc-endpoint", "http://" + e.RPC.Addr, + "--out", txout, + "--wallet", validatorWallet, "--address", validatorAddr, + } + + t.Run("without cosigner", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, append(cmd, hVerify.StringLE(), "verify")...) + }) + + t.Run("with cosigner", func(t *testing.T) { + t.Run("cosigner is sender", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, append(cmd, hVerify.StringLE(), "verify", "--", validatorAddr+":Global")...) + }) + + acc, err := wallet.NewAccount() + require.NoError(t, err) + pk, err := keys.NewPrivateKey() + require.NoError(t, err) + acc.ConvertMultisig(2, keys.PublicKeys{acc.PrivateKey().PublicKey(), pk.PublicKey()}) + + t.Run("cosigner is multisig account", func(t *testing.T) { + t.Run("missing in the wallet", func(t *testing.T) { + e.In.WriteString("one\r") + e.RunWithError(t, append(cmd, hVerify.StringLE(), "verify", "--", acc.Address)...) + }) + + t.Run("good", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, append(cmd, hVerify.StringLE(), "verify", "--", multisigAddr)...) + }) + }) + + t.Run("cosigner is deployed contract", func(t *testing.T) { + t.Run("missing in the wallet", func(t *testing.T) { + e.In.WriteString("one\r") + e.RunWithError(t, append(cmd, hVerify.StringLE(), "verify", "--", h.StringLE())...) + }) + + t.Run("good", func(t *testing.T) { + e.In.WriteString("one\r") + e.Run(t, append(cmd, hVerify.StringLE(), "verify", "--", hVerify.StringLE())...) + }) + }) + }) + }) + t.Run("test Storage.Find", func(t *testing.T) { cmd := []string{"neo-go", "contract", "testinvokefunction", "--rpc-endpoint", "http://" + e.RPC.Addr, diff --git a/cli/executor_test.go b/cli/executor_test.go index 3c75d4e23..92435afc6 100644 --- a/cli/executor_test.go +++ b/cli/executor_test.go @@ -32,6 +32,7 @@ import ( const ( validatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY" validatorAddr = "NVNvVRW5Q5naSx2k2iZm7xRgtRNGuZppAK" + multisigAddr = "NUVPACMnKFhpuHjsRjhUvXz1XhqfGZYVtY" validatorWallet = "testdata/wallet1_solo.json" ) diff --git a/cli/nep17_test.go b/cli/nep17_test.go index 8948176f1..3341b35a2 100644 --- a/cli/nep17_test.go +++ b/cli/nep17_test.go @@ -83,6 +83,11 @@ func TestNEP17Balance(t *testing.T) { e.checkNextLine(t, "^\\s*Updated\\s*:\\s*"+strconv.FormatUint(uint64(index), 10)) } } + + e.checkNextLine(t, "^\\s*$") + addr4, err := address.StringToUint160("NbxpLNCCSWZ9BkYpCYT8NfN1uoxq9Rfbrn") // deployed verify.go contract + require.NoError(t, err) + e.checkNextLine(t, "^Account "+address.Uint160ToString(addr4)) e.checkEOF(t) }) t.Run("Bad token", func(t *testing.T) { diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index 0feecfcce..a58111162 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -22,6 +22,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/fixedn" "github.com/nspcc-dev/neo-go/pkg/io" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/rpc/response/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" @@ -509,15 +510,17 @@ func invokeFunction(ctx *cli.Context) error { func invokeInternal(ctx *cli.Context, signAndPush bool) error { var ( - err error - gas fixedn.Fixed8 - operation string - params = make([]smartcontract.Parameter, 0) - paramsStart = 1 - cosigners []transaction.Signer - cosignersOffset = 0 - resp *result.Invoke - acc *wallet.Account + err error + gas fixedn.Fixed8 + operation string + params = make([]smartcontract.Parameter, 0) + paramsStart = 1 + cosigners []transaction.Signer + cosignersAccounts []client.SignerAccount + cosignersOffset = 0 + resp *result.Invoke + acc *wallet.Account + wall *wallet.Wallet ) args := ctx.Args() @@ -554,10 +557,20 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { if signAndPush { gas = flags.Fixed8FromContext(ctx, "gas") - acc, err = getAccFromContext(ctx) + acc, wall, err = getAccFromContext(ctx) if err != nil { return err } + for i := range cosigners { + cosignerAcc := wall.GetAccount(cosigners[i].Account) + if cosignerAcc == nil { + return cli.NewExitError(fmt.Errorf("can't calculate network fee: no account was found in the wallet for cosigner #%d", i), 1) + } + cosignersAccounts = append(cosignersAccounts, client.SignerAccount{ + Signer: cosigners[i], + Account: cosignerAcc, + }) + } } gctx, cancel := options.GetTimeoutContext(ctx) defer cancel() @@ -579,7 +592,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { fmt.Fprintln(ctx.App.Writer, errText+". Sending transaction...") } if out := ctx.String("out"); out != "" { - tx, err := c.CreateTxFromScript(resp.Script, acc, resp.GasConsumed, int64(gas), cosigners) + tx, err := c.CreateTxFromScript(resp.Script, acc, resp.GasConsumed, int64(gas), cosignersAccounts) if err != nil { return cli.NewExitError(fmt.Errorf("failed to create tx: %w", err), 1) } @@ -593,7 +606,7 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { if len(resp.Script) == 0 { return cli.NewExitError(errors.New("no script returned from the RPC node"), 1) } - txHash, err := c.SignAndPushInvocationTx(resp.Script, acc, resp.GasConsumed, gas, cosigners) + txHash, err := c.SignAndPushInvocationTx(resp.Script, acc, resp.GasConsumed, gas, cosignersAccounts) if err != nil { return cli.NewExitError(fmt.Errorf("failed to push invocation tx: %w", err), 1) } @@ -747,17 +760,17 @@ func inspect(ctx *cli.Context) error { return nil } -func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) { +func getAccFromContext(ctx *cli.Context) (*wallet.Account, *wallet.Wallet, error) { var addr util.Uint160 wPath := ctx.String("wallet") if len(wPath) == 0 { - return nil, cli.NewExitError(errNoWallet, 1) + return nil, nil, cli.NewExitError(errNoWallet, 1) } wall, err := wallet.NewWalletFromFile(wPath) if err != nil { - return nil, cli.NewExitError(err, 1) + return nil, nil, cli.NewExitError(err, 1) } addrFlag := ctx.Generic("address").(*flags.Address) if addrFlag.IsSet { @@ -767,20 +780,20 @@ func getAccFromContext(ctx *cli.Context) (*wallet.Account, error) { } acc := wall.GetAccount(addr) if acc == nil { - return nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1) + return nil, nil, cli.NewExitError(fmt.Errorf("wallet contains no account for '%s'", address.Uint160ToString(addr)), 1) } rawPass, err := input.ReadPassword( fmt.Sprintf("Enter account %s password > ", address.Uint160ToString(addr))) if err != nil { - return nil, cli.NewExitError(err, 1) + return nil, nil, cli.NewExitError(err, 1) } pass := strings.TrimRight(string(rawPass), "\n") err = acc.Decrypt(pass) if err != nil { - return nil, cli.NewExitError(err, 1) + return nil, nil, cli.NewExitError(err, 1) } - return acc, nil + return acc, wall, nil } // contractDeploy deploys contract. @@ -795,7 +808,7 @@ func contractDeploy(ctx *cli.Context) error { } gas := flags.Fixed8FromContext(ctx, "gas") - acc, err := getAccFromContext(ctx) + acc, _, err := getAccFromContext(ctx) if err != nil { return err } diff --git a/cli/testdata/verify.go b/cli/testdata/verify.go index 4abc927d5..85e433920 100644 --- a/cli/testdata/verify.go +++ b/cli/testdata/verify.go @@ -1,8 +1,10 @@ package testdata +import "github.com/nspcc-dev/neo-go/pkg/interop" + func Verify() bool { return true } -func OnNEP17Payment(from []byte, amount int, data interface{}) { +func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) { } diff --git a/cli/testdata/verify.yml b/cli/testdata/verify.yml new file mode 100644 index 000000000..8b39995ea --- /dev/null +++ b/cli/testdata/verify.yml @@ -0,0 +1 @@ +name: Test verify diff --git a/cli/testdata/wallet1_solo.json b/cli/testdata/wallet1_solo.json index db25b7d3e..764d94cfa 100644 --- a/cli/testdata/wallet1_solo.json +++ b/cli/testdata/wallet1_solo.json @@ -59,6 +59,18 @@ }, "lock": false, "isdefault": false + }, + { + "address" : "NbxpLNCCSWZ9BkYpCYT8NfN1uoxq9Rfbrn", + "key" : "6PYXDze5Ak4HahYKygcNzk6n65ACjWdDCYLSuKgA5KG8vyMJSFboUNSiPD", + "label" : "", + "contract" : { + "script" : "VwEAEUBXAANA", + "deployed" : true, + "parameters" : [] + }, + "lock" : false, + "isdefault" : false } ], "scrypt": { diff --git a/cli/wallet/validator.go b/cli/wallet/validator.go index 7da15f0e7..80e9bbbea 100644 --- a/cli/wallet/validator.go +++ b/cli/wallet/validator.go @@ -11,6 +11,7 @@ import ( "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/io" + "github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/emit" @@ -108,10 +109,14 @@ func handleCandidate(ctx *cli.Context, method string, sysGas int64) error { w := io.NewBufBinWriter() emit.AppCall(w.BinWriter, neoContractHash, method, callflag.States, acc.PrivateKey().PublicKey().Bytes()) emit.Opcodes(w.BinWriter, opcode.ASSERT) - tx, err := c.CreateTxFromScript(w.Bytes(), acc, sysGas, int64(gas), []transaction.Signer{{ - Account: acc.Contract.ScriptHash(), - Scopes: transaction.CalledByEntry, - }}) + tx, err := c.CreateTxFromScript(w.Bytes(), acc, sysGas, int64(gas), []client.SignerAccount{{ + Signer: transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: acc, + }, + }) if err != nil { return cli.NewExitError(err, 1) } else if err = acc.SignTx(tx); err != nil { @@ -171,9 +176,12 @@ func handleVote(ctx *cli.Context) error { emit.AppCall(w.BinWriter, neoContractHash, "vote", callflag.States, addr.BytesBE(), pubArg) emit.Opcodes(w.BinWriter, opcode.ASSERT) - tx, err := c.CreateTxFromScript(w.Bytes(), acc, -1, int64(gas), []transaction.Signer{{ - Account: acc.Contract.ScriptHash(), - Scopes: transaction.CalledByEntry, + tx, err := c.CreateTxFromScript(w.Bytes(), acc, -1, int64(gas), []client.SignerAccount{{ + Signer: transaction.Signer{ + Account: acc.Contract.ScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: acc, }}) if err != nil { return cli.NewExitError(err, 1) diff --git a/pkg/rpc/client/nep17.go b/pkg/rpc/client/nep17.go index 2d9f031a8..62a61b1fe 100644 --- a/pkg/rpc/client/nep17.go +++ b/pkg/rpc/client/nep17.go @@ -24,6 +24,13 @@ type TransferTarget struct { Amount int64 } +// SignerAccount represents combination of the transaction.Signer and the +// corresponding wallet.Account. +type SignerAccount struct { + Signer transaction.Signer + Account *wallet.Account +} + // NEP17Decimals invokes `decimals` NEP17 method on a specified contract. func (c *Client) NEP17Decimals(tokenHash util.Uint160) (int64, error) { result, err := c.InvokeFunction(tokenHash, "decimals", []smartcontract.Parameter{}, nil) @@ -140,9 +147,12 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, reci if err != nil { return nil, fmt.Errorf("bad account address: %v", err) } - return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, []transaction.Signer{{ - Account: accAddr, - Scopes: transaction.CalledByEntry, + return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, []SignerAccount{{ + Signer: transaction.Signer{ + Account: accAddr, + Scopes: transaction.CalledByEntry, + }, + Account: acc, }}) } @@ -150,13 +160,11 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, reci // If sysFee <= 0, it is determined via result of `invokescript` RPC. You should // initialize network magic with Init before calling CreateTxFromScript. func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee, netFee int64, - cosigners []transaction.Signer) (*transaction.Transaction, error) { - from, err := address.StringToUint160(acc.Address) + cosigners []SignerAccount) (*transaction.Transaction, error) { + signers, accounts, err := getSigners(acc, cosigners) if err != nil { - return nil, fmt.Errorf("bad account address: %v", err) + return nil, fmt.Errorf("failed to construct tx signers: %w", err) } - - signers := getSigners(from, cosigners) if sysFee < 0 { result, err := c.InvokeScript(script, signers) if err != nil { @@ -179,7 +187,7 @@ func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee, return nil, fmt.Errorf("failed to add validUntilBlock to transaction: %w", err) } - err = c.AddNetworkFee(tx, netFee, acc) + err = c.AddNetworkFee(tx, netFee, accounts...) if err != nil { return nil, fmt.Errorf("failed to add network fee: %w", err) } diff --git a/pkg/rpc/client/rpc.go b/pkg/rpc/client/rpc.go index 66aaa0c79..1706cda70 100644 --- a/pkg/rpc/client/rpc.go +++ b/pkg/rpc/client/rpc.go @@ -522,7 +522,7 @@ func (c *Client) SubmitRawOracleResponse(ps request.RawParams) error { // SignAndPushInvocationTx signs and pushes given script as an invocation // transaction using given wif to sign it and spending the amount of gas // specified. It returns a hash of the invocation transaction and an error. -func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sysfee int64, netfee fixedn.Fixed8, cosigners []transaction.Signer) (util.Uint256, error) { +func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sysfee int64, netfee fixedn.Fixed8, cosigners []SignerAccount) (util.Uint256, error) { var txHash util.Uint256 var err error @@ -544,25 +544,33 @@ func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sys return txHash, nil } -// getSigners returns an array of transaction signers from given sender and cosigners. -// If cosigners list already contains sender, the sender will be placed at the start of -// the list. -func getSigners(sender util.Uint160, cosigners []transaction.Signer) []transaction.Signer { +// getSigners returns an array of transaction signers and corresponding accounts from +// given sender and cosigners. If cosigners list already contains sender, the sender +// will be placed at the start of the list. +func getSigners(sender *wallet.Account, cosigners []SignerAccount) ([]transaction.Signer, []*wallet.Account, error) { + var ( + signers []transaction.Signer + accounts []*wallet.Account + ) + from, err := address.StringToUint160(sender.Address) + if err != nil { + return nil, nil, fmt.Errorf("bad sender account address: %v", err) + } s := transaction.Signer{ - Account: sender, + Account: from, Scopes: transaction.None, } - for i, c := range cosigners { - if c.Account == sender { - if i == 0 { - return cosigners - } - s.Scopes = c.Scopes - cosigners = append(cosigners[:i], cosigners[i+1:]...) - break + for _, c := range cosigners { + if c.Signer.Account == from { + s.Scopes = c.Signer.Scopes + continue } + signers = append(signers, c.Signer) + accounts = append(accounts, c.Account) } - return append([]transaction.Signer{s}, cosigners...) + signers = append([]transaction.Signer{s}, signers...) + accounts = append([]*wallet.Account{sender}, accounts...) + return signers, accounts, nil } // SignAndPushP2PNotaryRequest creates and pushes P2PNotary request constructed from the main diff --git a/pkg/rpc/server/client_test.go b/pkg/rpc/server/client_test.go index f36da87dd..db4fd154d 100644 --- a/pkg/rpc/server/client_test.go +++ b/pkg/rpc/server/client_test.go @@ -203,10 +203,15 @@ func TestSignAndPushInvocationTx(t *testing.T) { priv := testchain.PrivateKey(0) acc := wallet.NewAccountFromPrivateKey(priv) - h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc, 30, 0, []transaction.Signer{{ - Account: priv.GetScriptHash(), - Scopes: transaction.CalledByEntry, - }}) + h, err := c.SignAndPushInvocationTx([]byte{byte(opcode.PUSH1)}, acc, 30, 0, []client.SignerAccount{ + { + Signer: transaction.Signer{ + Account: priv.GetScriptHash(), + Scopes: transaction.CalledByEntry, + }, + Account: acc, + }, + }) require.NoError(t, err) mp := chain.GetMemPool()