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.
This commit is contained in:
Roman Khimov 2022-08-15 10:55:13 +03:00
parent 823c4b38fc
commit c034f94a94
4 changed files with 131 additions and 40 deletions

View file

@ -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 // NEP-17 transfers from a single sender to multiple recipients with the given
// data and cosigners. The transaction sender is included with the CalledByEntry // data and cosigners. The transaction sender is included with the CalledByEntry
// scope by default. // 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, func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64,
recipients []TransferTarget, cosigners []SignerAccount) (*transaction.Transaction, error) { recipients []TransferTarget, cosigners []SignerAccount) (*transaction.Transaction, error) {
from, err := address.StringToUint160(acc.Address) 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. // 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) { func (c *Client) MultiTransferNEP17(acc *wallet.Account, gas int64, recipients []TransferTarget, cosigners []SignerAccount) (util.Uint256, error) {
tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients, cosigners) tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients, cosigners)
if err != nil { if err != nil {

View file

@ -7,6 +7,7 @@ various methods to perform the only NEP-17 state-changing call, Transfer.
package nep17 package nep17
import ( import (
"errors"
"math/big" "math/big"
"github.com/nspcc-dev/neo-go/pkg/core/transaction" "github.com/nspcc-dev/neo-go/pkg/core/transaction"
@ -53,6 +54,14 @@ type TransferEvent struct {
Amount *big.Int 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 // NewReader creates an instance of TokenReader for contract with the given hash
// using the given Invoker. // using the given Invoker.
func NewReader(invoker Invoker, hash util.Uint160) *TokenReader { 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 // transaction if it's not true. The returned values are transaction hash, its
// ValidUntilBlock value and an error if any. // 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) { 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) return t.MultiTransfer([]TransferParameters{{from, to, amount, data}})
if err != nil {
return util.Uint256{}, 0, err
}
return t.actor.SendRun(script)
} }
// TransferTransaction creates a transaction that performs a `transfer` method // 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 // transaction if it's not true. This transaction is signed, but not sent to the
// network, instead it's returned to the caller. // 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) { 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) return t.MultiTransferTransaction([]TransferParameters{{from, to, amount, data}})
if err != nil {
return nil, err
}
return t.actor.MakeRun(script)
} }
// TransferUnsigned creates a transaction that performs a `transfer` method // 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 // transaction if it's not true. This transaction is not signed and just returned
// to the caller. // to the caller.
func (t *Token) TransferUnsigned(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error) { 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 { if err != nil {
return nil, err return nil, err
} }
return t.actor.MakeUnsignedRun(script, nil) 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)
}

View file

@ -66,19 +66,32 @@ func TestTokenTransfer(t *testing.T) {
ta := new(testAct) ta := new(testAct)
tok := New(ta, util.Uint160{1, 2, 3}) tok := New(ta, util.Uint160{1, 2, 3})
ta.err = errors.New("") for name, fun := range map[string]func(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (util.Uint256, uint32, error){
_, _, err := tok.Transfer(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) "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) require.Error(t, err)
_, _, err = tok.MultiTransfer([]TransferParameters{})
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())
require.Error(t, err) require.Error(t, err)
} }
@ -86,21 +99,37 @@ func TestTokenTransferTransaction(t *testing.T) {
ta := new(testAct) ta := new(testAct)
tok := New(ta, util.Uint160{1, 2, 3}) 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){ for name, fun := range map[string]func(from util.Uint160, to util.Uint160, amount *big.Int, data interface{}) (*transaction.Transaction, error){
tok.TransferTransaction, "TransferTransaction": tok.TransferTransaction,
tok.TransferUnsigned, "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("") t.Run(name, func(t *testing.T) {
_, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil) ta.err = errors.New("")
require.Error(t, err) _, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil)
require.Error(t, err)
ta.err = nil ta.err = nil
ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42} 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) tx, err := fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), nil)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, ta.tx, tx) require.Equal(t, ta.tx, tx)
_, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewMap()) _, err = fun(util.Uint160{3, 2, 1}, util.Uint160{3, 2, 1}, big.NewInt(1), stackitem.NewMap())
require.Error(t, err) 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)
} }

View file

@ -1071,6 +1071,21 @@ func TestCreateNEP17TransferTx(t *testing.T) {
ic.VM.LoadScriptWithFlags(tx.Script, callflag.All) ic.VM.LoadScriptWithFlags(tx.Script, callflag.All)
require.NoError(t, ic.VM.Run()) 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) { t.Run("none scope", func(t *testing.T) {
act, err := actor.New(c, []actor.SignerAccount{{ act, err := actor.New(c, []actor.SignerAccount{{
Signer: transaction.Signer{ Signer: transaction.Signer{