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{