From c034f94a9467a3858717ce45a34e45cf81a4d69b Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Mon, 15 Aug 2022 10:55:13 +0300 Subject: [PATCH 1/6] nep17: provide out of the box multitransfer capability It can't replicate the old multitransfer methods in ability to transfer multiple tokens, but it at the same time can do multiple senders. --- pkg/rpcclient/nep17.go | 8 +++ pkg/rpcclient/nep17/nep17.go | 69 ++++++++++++++++++++------ pkg/rpcclient/nep17/nep17_test.go | 79 ++++++++++++++++++++---------- pkg/services/rpcsrv/client_test.go | 15 ++++++ 4 files changed, 131 insertions(+), 40 deletions(-) diff --git a/pkg/rpcclient/nep17.go b/pkg/rpcclient/nep17.go index 8e86e3f43..3c2be2679 100644 --- a/pkg/rpcclient/nep17.go +++ b/pkg/rpcclient/nep17.go @@ -87,6 +87,10 @@ func (c *Client) CreateNEP17TransferTx(acc *wallet.Account, to util.Uint160, // NEP-17 transfers from a single sender to multiple recipients with the given // data and cosigners. The transaction sender is included with the CalledByEntry // scope by default. +// +// Deprecated: please use nep17 package (when transferring the same token) or +// [smartcontract.Builder] (when transferring multiple tokens), this method will +// be removed in future versions. func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, recipients []TransferTarget, cosigners []SignerAccount) (*transaction.Transaction, error) { from, err := address.StringToUint160(acc.Address) @@ -171,6 +175,10 @@ func (c *Client) TransferNEP17(acc *wallet.Account, to util.Uint160, token util. } // MultiTransferNEP17 is similar to TransferNEP17, buf allows to have multiple recipients. +// +// Deprecated: please use nep17 package (when transferring the same token) or +// [smartcontract.Builder] (when transferring multiple tokens), this method will +// be removed in future versions. func (c *Client) MultiTransferNEP17(acc *wallet.Account, gas int64, recipients []TransferTarget, cosigners []SignerAccount) (util.Uint256, error) { tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients, cosigners) if err != nil { diff --git a/pkg/rpcclient/nep17/nep17.go b/pkg/rpcclient/nep17/nep17.go index d5f3998bd..e19f4ec7f 100644 --- a/pkg/rpcclient/nep17/nep17.go +++ b/pkg/rpcclient/nep17/nep17.go @@ -7,6 +7,7 @@ various methods to perform the only NEP-17 state-changing call, Transfer. package nep17 import ( + "errors" "math/big" "github.com/nspcc-dev/neo-go/pkg/core/transaction" @@ -53,6 +54,14 @@ type TransferEvent struct { Amount *big.Int } +// TransferParameters is a set of parameters for `transfer` method. +type TransferParameters struct { + From util.Uint160 + To util.Uint160 + Amount *big.Int + Data interface{} +} + // NewReader creates an instance of TokenReader for contract with the given hash // using the given Invoker. func NewReader(invoker Invoker, hash util.Uint160) *TokenReader { @@ -75,11 +84,7 @@ func (t *TokenReader) BalanceOf(account util.Uint160) (*big.Int, error) { // transaction if it's not true. The returned values are transaction hash, its // ValidUntilBlock value and an error if any. func (t *Token) Transfer(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (util.Uint256, uint32, error) { - script, err := t.transferScript(from, to, amount, data) - if err != nil { - return util.Uint256{}, 0, err - } - return t.actor.SendRun(script) + return t.MultiTransfer([]TransferParameters{{from, to, amount, data}}) } // TransferTransaction creates a transaction that performs a `transfer` method @@ -87,11 +92,7 @@ func (t *Token) Transfer(from util.Uint160, to util.Uint160, amount *big.Int, da // transaction if it's not true. This transaction is signed, but not sent to the // network, instead it's returned to the caller. func (t *Token) TransferTransaction(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error) { - script, err := t.transferScript(from, to, amount, data) - if err != nil { - return nil, err - } - return t.actor.MakeRun(script) + return t.MultiTransferTransaction([]TransferParameters{{from, to, amount, data}}) } // TransferUnsigned creates a transaction that performs a `transfer` method @@ -99,13 +100,51 @@ func (t *Token) TransferTransaction(from util.Uint160, to util.Uint160, amount * // transaction if it's not true. This transaction is not signed and just returned // to the caller. func (t *Token) TransferUnsigned(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error) { - script, err := t.transferScript(from, to, amount, data) + return t.MultiTransferUnsigned([]TransferParameters{{from, to, amount, data}}) +} + +func (t *Token) multiTransferScript(params []TransferParameters) ([]byte, error) { + if len(params) == 0 { + return nil, errors.New("at least one transfer parameter required") + } + b := smartcontract.NewBuilder() + for i := range params { + b.InvokeWithAssert(t.hash, "transfer", params[i].From, + params[i].To, params[i].Amount, params[i].Data) + } + return b.Script() +} + +// MultiTransfer is not a real NEP-17 method, but rather a convenient way to +// perform multiple transfers (usually from a single account) in one transaction. +// It accepts a set of parameters, creates a script that calls `transfer` as +// many times as needed (with ASSERTs added, so if any of these transfers fail +// whole transaction (with all transfers) fails). The values returned are the +// same as in Transfer. +func (t *Token) MultiTransfer(params []TransferParameters) (util.Uint256, uint32, error) { + script, err := t.multiTransferScript(params) + if err != nil { + return util.Uint256{}, 0, err + } + return t.actor.SendRun(script) +} + +// MultiTransferTransaction is similar to MultiTransfer, but returns the same values +// as TransferTransaction (signed transaction that is not yet sent). +func (t *Token) MultiTransferTransaction(params []TransferParameters) (*transaction.Transaction, error) { + script, err := t.multiTransferScript(params) + if err != nil { + return nil, err + } + return t.actor.MakeRun(script) +} + +// MultiTransferUnsigned is similar to MultiTransfer, but returns the same values +// as TransferUnsigned (not yet signed transaction). +func (t *Token) MultiTransferUnsigned(params []TransferParameters) (*transaction.Transaction, error) { + script, err := t.multiTransferScript(params) if err != nil { return nil, err } return t.actor.MakeUnsignedRun(script, nil) } - -func (t *Token) transferScript(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) ([]byte, error) { - return smartcontract.CreateCallWithAssertScript(t.hash, "transfer", from, to, amount, data) -} diff --git a/pkg/rpcclient/nep17/nep17_test.go b/pkg/rpcclient/nep17/nep17_test.go index 2bcf45d53..b14e84dda 100644 --- a/pkg/rpcclient/nep17/nep17_test.go +++ b/pkg/rpcclient/nep17/nep17_test.go @@ -66,19 +66,32 @@ func TestTokenTransfer(t *testing.T) { ta := new(testAct) tok := New(ta, util.Uint160{1, 2, 3}) - ta.err = errors.New("") - _, _, err := tok.Transfer(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) + for name, fun := range map[string]func(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (util.Uint256, uint32, error){ + "Tranfer": tok.Transfer, + "MultiTransfer": func(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (util.Uint256, uint32, error) { + return tok.MultiTransfer([]TransferParameters{{from, to, amount, data}, {from, to, amount, data}}) + }, + } { + t.Run(name, func(t *testing.T) { + ta.err = errors.New("") + _, _, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) + require.Error(t, err) + + ta.err = nil + ta.txh = util.Uint256{1, 2, 3} + ta.vub = 42 + h, vub, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) + require.NoError(t, err) + require.Equal(t, ta.txh, h) + require.Equal(t, ta.vub, vub) + + _, _, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewMap()) + require.Error(t, err) + }) + } + _, _, err := tok.MultiTransfer(nil) require.Error(t, err) - - ta.err = nil - ta.txh = util.Uint256{1, 2, 3} - ta.vub = 42 - h, vub, err := tok.Transfer(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) - require.NoError(t, err) - require.Equal(t, ta.txh, h) - require.Equal(t, ta.vub, vub) - - _, _, err = tok.Transfer(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewMap()) + _, _, err = tok.MultiTransfer([]TransferParameters{}) require.Error(t, err) } @@ -86,21 +99,37 @@ func TestTokenTransferTransaction(t *testing.T) { ta := new(testAct) tok := New(ta, util.Uint160{1, 2, 3}) - for _, fun := range []func(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error){ - tok.TransferTransaction, - tok.TransferUnsigned, + for name, fun := range map[string]func(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error){ + "TransferTransaction": tok.TransferTransaction, + "TransferUnsigned": tok.TransferUnsigned, + "MultiTransferTransaction": func(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error) { + return tok.MultiTransferTransaction([]TransferParameters{{from, to, amount, data}, {from, to, amount, data}}) + }, + "MultiTransferUnsigned": func(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error) { + return tok.MultiTransferUnsigned([]TransferParameters{{from, to, amount, data}, {from, to, amount, data}}) + }, } { - ta.err = errors.New("") - _, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) - require.Error(t, err) + t.Run(name, func(t *testing.T) { + ta.err = errors.New("") + _, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) + require.Error(t, err) - ta.err = nil - ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} - tx, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) - require.NoError(t, err) - require.Equal(t, ta.tx, tx) + ta.err = nil + ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} + tx, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) + require.NoError(t, err) + require.Equal(t, ta.tx, tx) - _, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewMap()) - require.Error(t, err) + _, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewMap()) + require.Error(t, err) + }) } + _, err := tok.MultiTransferTransaction(nil) + require.Error(t, err) + _, err = tok.MultiTransferTransaction([]TransferParameters{}) + require.Error(t, err) + _, err = tok.MultiTransferUnsigned(nil) + require.Error(t, err) + _, err = tok.MultiTransferUnsigned([]TransferParameters{}) + require.Error(t, err) } diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go index c0681a0e8..92fd99d38 100644 --- a/pkg/services/rpcsrv/client_test.go +++ b/pkg/services/rpcsrv/client_test.go @@ -1071,6 +1071,21 @@ func TestCreateNEP17TransferTx(t *testing.T) { ic.VM.LoadScriptWithFlags(tx.Script, callflag.All) require.NoError(t, ic.VM.Run()) }) + t.Run("default scope, multitransfer", func(t *testing.T) { + act, err := actor.NewSimple(c, acc) + require.NoError(t, err) + gazprom := gas.New(act) + tx, err := gazprom.MultiTransferTransaction([]nep17.TransferParameters{ + {From: addr, To: util.Uint160{3, 2, 1}, Amount: big.NewInt(1000), Data: nil}, + {From: addr, To: util.Uint160{1, 2, 3}, Amount: big.NewInt(1000), Data: nil}, + }) + require.NoError(t, err) + require.NoError(t, chain.VerifyTx(tx)) + ic := chain.GetTestVM(trigger.Application, tx, nil) + ic.VM.LoadScriptWithFlags(tx.Script, callflag.All) + require.NoError(t, ic.VM.Run()) + require.Equal(t, 2, len(ic.Notifications)) + }) t.Run("none scope", func(t *testing.T) { act, err := actor.New(c, []actor.SignerAccount{{ Signer: transaction.Signer{ From b2524a3ba918ff81ff89eb0788d9739084045c35 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 19 Aug 2022 16:45:16 +0300 Subject: [PATCH 2/6] actor: add Sender helper method --- pkg/rpcclient/actor/actor.go | 6 ++++++ pkg/rpcclient/actor/actor_test.go | 11 +++++++++++ 2 files changed, 17 insertions(+) diff --git a/pkg/rpcclient/actor/actor.go b/pkg/rpcclient/actor/actor.go index abf79530f..07b436227 100644 --- a/pkg/rpcclient/actor/actor.go +++ b/pkg/rpcclient/actor/actor.go @@ -202,3 +202,9 @@ func (a *Actor) SendTunedRun(script []byte, attrs []transaction.Attribute, txHoo func (a *Actor) SendUncheckedRun(script []byte, sysfee int64, attrs []transaction.Attribute, txHook TransactionModifier) (util.Uint256, uint32, error) { return a.sendWrapper(a.MakeUncheckedRun(script, sysfee, attrs, txHook)) } + +// Sender return the sender address that will be used in transactions created +// by Actor. +func (a *Actor) Sender() util.Uint160 { + return a.txSigners[0].Account +} diff --git a/pkg/rpcclient/actor/actor_test.go b/pkg/rpcclient/actor/actor_test.go index 32e9eca00..c87b127bd 100644 --- a/pkg/rpcclient/actor/actor_test.go +++ b/pkg/rpcclient/actor/actor_test.go @@ -7,6 +7,7 @@ import ( "github.com/google/uuid" "github.com/nspcc-dev/neo-go/pkg/config/netmode" "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/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/util" @@ -259,3 +260,13 @@ func TestSenders(t *testing.T) { require.Equal(t, client.hash, h) require.Equal(t, uint32(8), vub) } + +func TestSender(t *testing.T) { + client, acc := testRPCAndAccount(t) + a, err := NewSimple(client, acc) + require.NoError(t, err) + + addr, err := address.StringToUint160(acc.Address) + require.NoError(t, err) + require.Equal(t, addr, a.Sender()) +} From 7e212de41a996427e0260489c8661a9fc54742e1 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 19 Aug 2022 20:53:16 +0300 Subject: [PATCH 3/6] cli: drop the use of deprecated APIs from sc/nep11/nep17 --- cli/cmdargs/parser.go | 33 +++++++++++---- cli/smartcontract/smart_contract.go | 45 +++++--------------- cli/wallet/nep11.go | 47 ++++++++------------- cli/wallet/nep17.go | 64 +++++++++++++++++------------ 4 files changed, 90 insertions(+), 99 deletions(-) diff --git a/cli/cmdargs/parser.go b/cli/cmdargs/parser.go index 1e449f5e3..2661d5831 100644 --- a/cli/cmdargs/parser.go +++ b/cli/cmdargs/parser.go @@ -9,7 +9,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/core/transaction" "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/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" @@ -197,17 +197,32 @@ func ParseParams(args []string, calledFromMain bool) (int, []smartcontract.Param // GetSignersAccounts returns the list of signers combined with the corresponding // accounts from the provided wallet. -func GetSignersAccounts(wall *wallet.Wallet, signers []transaction.Signer) ([]rpcclient.SignerAccount, error) { - signersAccounts := make([]rpcclient.SignerAccount, len(signers)) - for i := range signers { - signerAcc := wall.GetAccount(signers[i].Account) +func GetSignersAccounts(senderAcc *wallet.Account, wall *wallet.Wallet, signers []transaction.Signer, accScope transaction.WitnessScope) ([]actor.SignerAccount, error) { + signersAccounts := make([]actor.SignerAccount, 0, len(signers)+1) + sender, err := address.StringToUint160(senderAcc.Address) + if err != nil { + return nil, err + } + signersAccounts = append(signersAccounts, actor.SignerAccount{ + Signer: transaction.Signer{ + Account: sender, + Scopes: accScope, + }, + Account: senderAcc, + }) + for i, s := range signers { + if s.Account == sender { + signersAccounts[0].Signer = s + continue + } + signerAcc := wall.GetAccount(s.Account) if signerAcc == nil { - return nil, fmt.Errorf("no account was found in the wallet for signer #%d (%s)", i, address.Uint160ToString(signers[i].Account)) + return nil, fmt.Errorf("no account was found in the wallet for signer #%d (%s)", i, address.Uint160ToString(s.Account)) } - signersAccounts[i] = rpcclient.SignerAccount{ - Signer: signers[i], + signersAccounts = append(signersAccounts, actor.SignerAccount{ + Signer: s, Account: signerAcc, - } + }) } return signersAccounts, nil } diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index c7530b541..e260306aa 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -22,7 +22,6 @@ 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/neorpc/result" - "github.com/nspcc-dev/neo-go/pkg/rpcclient" "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/management" "github.com/nspcc-dev/neo-go/pkg/smartcontract" @@ -659,25 +658,22 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error { func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, script util.Uint160, operation string, params []smartcontract.Parameter, cosigners []transaction.Signer) (util.Uint160, error) { var ( - err error - gas, sysgas fixedn.Fixed8 - cosignersAccounts []rpcclient.SignerAccount - resp *result.Invoke - sender util.Uint160 - signAndPush = acc != nil - act *actor.Actor + err error + gas, sysgas fixedn.Fixed8 + signersAccounts []actor.SignerAccount + resp *result.Invoke + sender util.Uint160 + signAndPush = acc != nil + act *actor.Actor ) if signAndPush { gas = flags.Fixed8FromContext(ctx, "gas") sysgas = flags.Fixed8FromContext(ctx, "sysgas") - sender, err = address.StringToUint160(acc.Address) + signersAccounts, err = cmdargs.GetSignersAccounts(acc, wall, cosigners, transaction.None) if err != nil { - return sender, err - } - cosignersAccounts, err = cmdargs.GetSignersAccounts(wall, cosigners) - if err != nil { - return sender, cli.NewExitError(fmt.Errorf("failed to calculate network fee: %w", err), 1) + return sender, cli.NewExitError(fmt.Errorf("invalid signers: %w", err), 1) } + sender = signersAccounts[0].Signer.Account } gctx, cancel := options.GetTimeoutContext(ctx) defer cancel() @@ -687,26 +683,7 @@ func invokeWithArgs(ctx *cli.Context, acc *wallet.Account, wall *wallet.Wallet, return sender, err } if signAndPush { - // This will eventually be handled in cmdargs.GetSignersAccounts. - asa := make([]actor.SignerAccount, 0, len(cosigners)+1) - asa = append(asa, actor.SignerAccount{ - Signer: transaction.Signer{ - Account: sender, - Scopes: transaction.None, - }, - Account: acc, - }) - for _, c := range cosignersAccounts { - if c.Signer.Account == sender { - asa[0].Signer = c.Signer - continue - } - asa = append(asa, actor.SignerAccount{ - Signer: c.Signer, - Account: c.Account, - }) - } - act, err = actor.New(c, asa) + act, err = actor.New(c, signersAccounts) if err != nil { return sender, cli.NewExitError(fmt.Errorf("failed to create RPC actor: %w", err), 1) } diff --git a/cli/wallet/nep11.go b/cli/wallet/nep11.go index 3b81dfc39..d69c27ecd 100644 --- a/cli/wallet/nep11.go +++ b/cli/wallet/nep11.go @@ -16,7 +16,7 @@ import ( "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/encoding/fixedn" - "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" @@ -262,44 +262,31 @@ func transferNEP11(ctx *cli.Context) error { return transferNEP(ctx, manifest.NEP11StandardName) } -func signAndSendNEP11Transfer(ctx *cli.Context, c *rpcclient.Client, acc *wallet.Account, token, to util.Uint160, tokenID []byte, amount *big.Int, data interface{}, cosigners []rpcclient.SignerAccount) error { - gas := flags.Fixed8FromContext(ctx, "gas") - sysgas := flags.Fixed8FromContext(ctx, "sysgas") - +func signAndSendNEP11Transfer(ctx *cli.Context, act *actor.Actor, acc *wallet.Account, token, to util.Uint160, tokenID []byte, amount *big.Int, data interface{}) error { var ( - tx *transaction.Transaction - err error + err error + gas = flags.Fixed8FromContext(ctx, "gas") + sysgas = flags.Fixed8FromContext(ctx, "sysgas") + tx *transaction.Transaction ) if amount != nil { - var from util.Uint160 - - from, err = address.StringToUint160(acc.Address) - if err != nil { - return cli.NewExitError(fmt.Errorf("bad account address: %w", err), 1) - } - tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, from, to, amount, tokenID, data) //nolint:staticcheck // SA1019: c.CreateNEP11TransferTx is deprecated + n11 := nep11.NewDivisible(act, token) + tx, err = n11.TransferDUnsigned(act.Sender(), to, amount, tokenID, data) } else { - tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, to, tokenID, data) //nolint:staticcheck // SA1019: c.CreateNEP11TransferTx is deprecated + n11 := nep11.NewNonDivisible(act, token) + tx, err = n11.TransferUnsigned(to, tokenID, data) } if err != nil { return cli.NewExitError(err, 1) } tx.SystemFee += int64(sysgas) + tx.NetworkFee += int64(gas) if outFile := ctx.String("out"); outFile != "" { - ver, err := c.GetVersion() - if err != nil { - return cli.NewExitError(fmt.Errorf("RPC failure: %w", err), 1) - } + ver := act.GetVersion() // Make a long-lived transaction, it's to be signed manually. tx.ValidUntilBlock += (ver.Protocol.MaxValidUntilBlockIncrement - uint32(ver.Protocol.ValidatorsCount)) - 2 - m, err := c.GetNetwork() - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to save tx: %w", err), 1) - } - if err := paramcontext.InitAndSave(m, tx, acc, outFile); err != nil { - return cli.NewExitError(err, 1) - } + err = paramcontext.InitAndSave(ver.Protocol.Network, tx, acc, outFile) } else { if !ctx.Bool("force") { err := input.ConfirmTx(ctx.App.Writer, tx) @@ -307,10 +294,10 @@ func signAndSendNEP11Transfer(ctx *cli.Context, c *rpcclient.Client, acc *wallet return cli.NewExitError(err, 1) } } - _, err := c.SignAndPushTx(tx, acc, cosigners) //nolint:staticcheck // SA1019: c.SignAndPushTx is deprecated - if err != nil { - return cli.NewExitError(err, 1) - } + _, _, err = act.SignAndSend(tx) + } + if err != nil { + return cli.NewExitError(err, 1) } fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index 5adba25d0..b14d593a6 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -12,13 +12,16 @@ import ( "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/core/transaction" "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/neorpc/result" "github.com/nspcc-dev/neo-go/pkg/rpcclient" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" + "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/wallet" @@ -546,12 +549,16 @@ func multiTransferNEP17(ctx *cli.Context) error { if extErr != nil { return extErr } - cosignersAccounts, err := cmdargs.GetSignersAccounts(wall, cosigners) + signersAccounts, err := cmdargs.GetSignersAccounts(acc, wall, cosigners, transaction.CalledByEntry) if err != nil { - return cli.NewExitError(fmt.Errorf("failed to create NEP-17 multitransfer transaction: %w", err), 1) + return cli.NewExitError(fmt.Errorf("invalid signers: %w", err), 1) + } + act, err := actor.New(c, signersAccounts) + if err != nil { + return cli.NewExitError(fmt.Errorf("failed to create RPC actor: %w", err), 1) } - return signAndSendNEP17Transfer(ctx, c, acc, recipients, cosignersAccounts) + return signAndSendNEP17Transfer(ctx, act, acc, recipients) } func transferNEP17(ctx *cli.Context) error { @@ -604,9 +611,13 @@ func transferNEP(ctx *cli.Context, standard string) error { if extErr != nil { return extErr } - cosignersAccounts, err := cmdargs.GetSignersAccounts(wall, cosigners) + signersAccounts, err := cmdargs.GetSignersAccounts(acc, wall, cosigners, transaction.CalledByEntry) if err != nil { - return cli.NewExitError(fmt.Errorf("failed to create NEP-17 transfer transaction: %w", err), 1) + return cli.NewExitError(fmt.Errorf("invalid signers: %w", err), 1) + } + act, err := actor.New(c, signersAccounts) + if err != nil { + return cli.NewExitError(fmt.Errorf("failed to create RPC actor: %w", err), 1) } amountArg := ctx.String("amount") @@ -616,12 +627,12 @@ func transferNEP(ctx *cli.Context, standard string) error { if err != nil { return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1) } - return signAndSendNEP17Transfer(ctx, c, acc, []rpcclient.TransferTarget{{ + return signAndSendNEP17Transfer(ctx, act, acc, []rpcclient.TransferTarget{{ Token: token.Hash, Address: to, Amount: amount.Int64(), Data: data, - }}, cosignersAccounts) + }}) case manifest.NEP11StandardName: tokenID := ctx.String("id") if tokenID == "" { @@ -632,42 +643,43 @@ func transferNEP(ctx *cli.Context, standard string) error { return cli.NewExitError(fmt.Errorf("invalid token ID: %w", err), 1) } if amountArg == "" { - return signAndSendNEP11Transfer(ctx, c, acc, token.Hash, to, tokenIDBytes, nil, data, cosignersAccounts) + return signAndSendNEP11Transfer(ctx, act, acc, token.Hash, to, tokenIDBytes, nil, data) } amount, err := fixedn.FromString(amountArg, int(token.Decimals)) if err != nil { return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1) } - return signAndSendNEP11Transfer(ctx, c, acc, token.Hash, to, tokenIDBytes, amount, data, cosignersAccounts) + return signAndSendNEP11Transfer(ctx, act, acc, token.Hash, to, tokenIDBytes, amount, data) default: return cli.NewExitError(fmt.Errorf("unsupported token standard %s", standard), 1) } } -func signAndSendNEP17Transfer(ctx *cli.Context, c *rpcclient.Client, acc *wallet.Account, recipients []rpcclient.TransferTarget, cosigners []rpcclient.SignerAccount) error { +func signAndSendNEP17Transfer(ctx *cli.Context, act *actor.Actor, acc *wallet.Account, recipients []rpcclient.TransferTarget) error { gas := flags.Fixed8FromContext(ctx, "gas") sysgas := flags.Fixed8FromContext(ctx, "sysgas") - tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients, cosigners) + scr := smartcontract.NewBuilder() + for i := range recipients { + scr.InvokeWithAssert(recipients[i].Token, "transfer", act.Sender(), + recipients[i].Address, recipients[i].Amount, recipients[i].Data) + } + script, err := scr.Script() + if err != nil { + return err + } + tx, err := act.MakeUnsignedRun(script, nil) if err != nil { return cli.NewExitError(err, 1) } tx.SystemFee += int64(sysgas) + tx.NetworkFee += int64(gas) if outFile := ctx.String("out"); outFile != "" { - ver, err := c.GetVersion() - if err != nil { - return cli.NewExitError(fmt.Errorf("RPC failure: %w", err), 1) - } + ver := act.GetVersion() // Make a long-lived transaction, it's to be signed manually. tx.ValidUntilBlock += (ver.Protocol.MaxValidUntilBlockIncrement - uint32(ver.Protocol.ValidatorsCount)) - 2 - m, err := c.GetNetwork() - if err != nil { - return cli.NewExitError(fmt.Errorf("failed to save tx: %w", err), 1) - } - if err := paramcontext.InitAndSave(m, tx, acc, outFile); err != nil { - return cli.NewExitError(err, 1) - } + err = paramcontext.InitAndSave(ver.Protocol.Network, tx, acc, outFile) } else { if !ctx.Bool("force") { err := input.ConfirmTx(ctx.App.Writer, tx) @@ -675,10 +687,10 @@ func signAndSendNEP17Transfer(ctx *cli.Context, c *rpcclient.Client, acc *wallet return cli.NewExitError(err, 1) } } - _, err := c.SignAndPushTx(tx, acc, cosigners) //nolint:staticcheck // SA1019: c.SignAndPushTx is deprecated - if err != nil { - return cli.NewExitError(err, 1) - } + _, _, err = act.SignAndSend(tx) + } + if err != nil { + return cli.NewExitError(err, 1) } fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) From 3402b870c8fb2732957149c024c62376e7eef674 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 19 Aug 2022 21:34:39 +0300 Subject: [PATCH 4/6] fixedn: add a test for empty string Ensure it fails. --- pkg/encoding/fixedn/decimal_test.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/encoding/fixedn/decimal_test.go b/pkg/encoding/fixedn/decimal_test.go index be676e85f..582d6ddfc 100644 --- a/pkg/encoding/fixedn/decimal_test.go +++ b/pkg/encoding/fixedn/decimal_test.go @@ -38,6 +38,8 @@ func TestDecimalFromStringBad(t *testing.T) { s string prec int }{ + {"", 0}, + {"", 1}, {"12A", 1}, {"12.345", 2}, {"12.3A", 2}, From f60fa02a961318138fddae959fb577c2d06b095c Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 19 Aug 2022 21:35:00 +0300 Subject: [PATCH 5/6] cli/wallet: deduplicate some transfer code --- cli/wallet/nep11.go | 47 ------------------------------ cli/wallet/nep17.go | 70 ++++++++++++++++++++++++++------------------- 2 files changed, 41 insertions(+), 76 deletions(-) diff --git a/cli/wallet/nep11.go b/cli/wallet/nep11.go index d69c27ecd..044619b51 100644 --- a/cli/wallet/nep11.go +++ b/cli/wallet/nep11.go @@ -9,18 +9,13 @@ import ( "github.com/nspcc-dev/neo-go/cli/cmdargs" "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/config" - "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/encoding/fixedn" - "github.com/nspcc-dev/neo-go/pkg/rpcclient/actor" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" - "github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/wallet" "github.com/urfave/cli" @@ -262,48 +257,6 @@ func transferNEP11(ctx *cli.Context) error { return transferNEP(ctx, manifest.NEP11StandardName) } -func signAndSendNEP11Transfer(ctx *cli.Context, act *actor.Actor, acc *wallet.Account, token, to util.Uint160, tokenID []byte, amount *big.Int, data interface{}) error { - var ( - err error - gas = flags.Fixed8FromContext(ctx, "gas") - sysgas = flags.Fixed8FromContext(ctx, "sysgas") - tx *transaction.Transaction - ) - if amount != nil { - n11 := nep11.NewDivisible(act, token) - tx, err = n11.TransferDUnsigned(act.Sender(), to, amount, tokenID, data) - } else { - n11 := nep11.NewNonDivisible(act, token) - tx, err = n11.TransferUnsigned(to, tokenID, data) - } - if err != nil { - return cli.NewExitError(err, 1) - } - tx.SystemFee += int64(sysgas) - tx.NetworkFee += int64(gas) - - if outFile := ctx.String("out"); outFile != "" { - ver := act.GetVersion() - // Make a long-lived transaction, it's to be signed manually. - tx.ValidUntilBlock += (ver.Protocol.MaxValidUntilBlockIncrement - uint32(ver.Protocol.ValidatorsCount)) - 2 - err = paramcontext.InitAndSave(ver.Protocol.Network, tx, acc, outFile) - } else { - if !ctx.Bool("force") { - err := input.ConfirmTx(ctx.App.Writer, tx) - if err != nil { - return cli.NewExitError(err, 1) - } - } - _, _, err = act.SignAndSend(tx) - } - if err != nil { - return cli.NewExitError(err, 1) - } - - fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) - return nil -} - func printNEP11NDOwner(ctx *cli.Context) error { return printNEP11Owner(ctx, false) } diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index b14d593a6..97a99bddc 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -21,6 +21,8 @@ import ( "github.com/nspcc-dev/neo-go/pkg/rpcclient/gas" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep11" + "github.com/nspcc-dev/neo-go/pkg/rpcclient/nep17" "github.com/nspcc-dev/neo-go/pkg/smartcontract" "github.com/nspcc-dev/neo-go/pkg/smartcontract/manifest" "github.com/nspcc-dev/neo-go/pkg/util" @@ -558,7 +560,11 @@ func multiTransferNEP17(ctx *cli.Context) error { return cli.NewExitError(fmt.Errorf("failed to create RPC actor: %w", err), 1) } - return signAndSendNEP17Transfer(ctx, act, acc, recipients) + tx, err := makeMultiTransferNEP17(act, recipients) + if err != nil { + return cli.NewExitError(fmt.Errorf("can't make transaction: %w", err), 1) + } + return signAndSendSomeTransaction(ctx, act, acc, tx) } func transferNEP17(ctx *cli.Context) error { @@ -566,6 +572,8 @@ func transferNEP17(ctx *cli.Context) error { } func transferNEP(ctx *cli.Context, standard string) error { + var tx *transaction.Transaction + wall, pass, err := readWallet(ctx) if err != nil { return cli.NewExitError(err, 1) @@ -621,44 +629,42 @@ func transferNEP(ctx *cli.Context, standard string) error { } amountArg := ctx.String("amount") + amount, err := fixedn.FromString(amountArg, int(token.Decimals)) + // It's OK for NEP-11 transfer to not have amount set. + if err != nil && (standard == manifest.NEP17StandardName || amountArg != "") { + return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1) + } switch standard { case manifest.NEP17StandardName: - amount, err := fixedn.FromString(amountArg, int(token.Decimals)) - if err != nil { - return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1) - } - return signAndSendNEP17Transfer(ctx, act, acc, []rpcclient.TransferTarget{{ - Token: token.Hash, - Address: to, - Amount: amount.Int64(), - Data: data, - }}) + n17 := nep17.New(act, token.Hash) + tx, err = n17.TransferUnsigned(act.Sender(), to, amount, data) case manifest.NEP11StandardName: tokenID := ctx.String("id") if tokenID == "" { return cli.NewExitError(errors.New("token ID should be specified"), 1) } - tokenIDBytes, err := hex.DecodeString(tokenID) - if err != nil { - return cli.NewExitError(fmt.Errorf("invalid token ID: %w", err), 1) + tokenIDBytes, terr := hex.DecodeString(tokenID) + if terr != nil { + return cli.NewExitError(fmt.Errorf("invalid token ID: %w", terr), 1) } if amountArg == "" { - return signAndSendNEP11Transfer(ctx, act, acc, token.Hash, to, tokenIDBytes, nil, data) + n11 := nep11.NewNonDivisible(act, token.Hash) + tx, err = n11.TransferUnsigned(to, tokenIDBytes, data) + } else { + n11 := nep11.NewDivisible(act, token.Hash) + tx, err = n11.TransferDUnsigned(act.Sender(), to, amount, tokenIDBytes, data) } - amount, err := fixedn.FromString(amountArg, int(token.Decimals)) - if err != nil { - return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1) - } - return signAndSendNEP11Transfer(ctx, act, acc, token.Hash, to, tokenIDBytes, amount, data) default: return cli.NewExitError(fmt.Errorf("unsupported token standard %s", standard), 1) } + if err != nil { + return cli.NewExitError(fmt.Errorf("can't make transaction: %w", err), 1) + } + + return signAndSendSomeTransaction(ctx, act, acc, tx) } -func signAndSendNEP17Transfer(ctx *cli.Context, act *actor.Actor, acc *wallet.Account, recipients []rpcclient.TransferTarget) error { - gas := flags.Fixed8FromContext(ctx, "gas") - sysgas := flags.Fixed8FromContext(ctx, "sysgas") - +func makeMultiTransferNEP17(act *actor.Actor, recipients []rpcclient.TransferTarget) (*transaction.Transaction, error) { scr := smartcontract.NewBuilder() for i := range recipients { scr.InvokeWithAssert(recipients[i].Token, "transfer", act.Sender(), @@ -666,12 +672,18 @@ func signAndSendNEP17Transfer(ctx *cli.Context, act *actor.Actor, acc *wallet.Ac } script, err := scr.Script() if err != nil { - return err - } - tx, err := act.MakeUnsignedRun(script, nil) - if err != nil { - return cli.NewExitError(err, 1) + return nil, err } + return act.MakeUnsignedRun(script, nil) +} + +func signAndSendSomeTransaction(ctx *cli.Context, act *actor.Actor, acc *wallet.Account, tx *transaction.Transaction) error { + var ( + err error + gas = flags.Fixed8FromContext(ctx, "gas") + sysgas = flags.Fixed8FromContext(ctx, "sysgas") + ) + tx.SystemFee += int64(sysgas) tx.NetworkFee += int64(gas) From 8005bfcd32c61c9f6618a3101a3b98e0dadfe415 Mon Sep 17 00:00:00 2001 From: Roman Khimov Date: Fri, 19 Aug 2022 21:37:50 +0300 Subject: [PATCH 6/6] cli/wallet: compensate for CLI waiting time Similar to ba2e7063dd6f95d97ccb666849077c590c686919. --- cli/wallet/nep17.go | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cli/wallet/nep17.go b/cli/wallet/nep17.go index 97a99bddc..c31dc8c51 100644 --- a/cli/wallet/nep17.go +++ b/cli/wallet/nep17.go @@ -6,6 +6,7 @@ import ( "fmt" "math/big" "strings" + "time" "github.com/nspcc-dev/neo-go/cli/cmdargs" "github.com/nspcc-dev/neo-go/cli/flags" @@ -687,17 +688,21 @@ func signAndSendSomeTransaction(ctx *cli.Context, act *actor.Actor, acc *wallet. tx.SystemFee += int64(sysgas) tx.NetworkFee += int64(gas) + ver := act.GetVersion() if outFile := ctx.String("out"); outFile != "" { - ver := act.GetVersion() // Make a long-lived transaction, it's to be signed manually. tx.ValidUntilBlock += (ver.Protocol.MaxValidUntilBlockIncrement - uint32(ver.Protocol.ValidatorsCount)) - 2 err = paramcontext.InitAndSave(ver.Protocol.Network, tx, acc, outFile) } else { if !ctx.Bool("force") { + promptTime := time.Now() err := input.ConfirmTx(ctx.App.Writer, tx) if err != nil { return cli.NewExitError(err, 1) } + waitTime := time.Since(promptTime) + // Compensate for confirmation waiting. + tx.ValidUntilBlock += uint32((waitTime.Milliseconds() / int64(ver.Protocol.MillisecondsPerBlock))) + 1 } _, _, err = act.SignAndSend(tx) }