mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2025-02-08 19:42:38 +00:00
rpc,cli: support multitransfer transactions
Allow to transfer single asset to multiple recepients in a single transaction. It is currently a separate command in CLI and can be merged in future.
This commit is contained in:
parent
8d2e9b68bf
commit
e013477bc9
2 changed files with 131 additions and 17 deletions
|
@ -25,17 +25,25 @@ var (
|
||||||
gasToken = wallet.NewToken(client.GasContractHash, "GAS", "gas", 8)
|
gasToken = wallet.NewToken(client.GasContractHash, "GAS", "gas", 8)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
tokenFlag = cli.StringFlag{
|
||||||
|
Name: "token",
|
||||||
|
Usage: "Token to use",
|
||||||
|
}
|
||||||
|
gasFlag = flags.Fixed8Flag{
|
||||||
|
Name: "gas",
|
||||||
|
Usage: "Amount of GAS to attach to a tx",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func newNEP5Commands() []cli.Command {
|
func newNEP5Commands() []cli.Command {
|
||||||
balanceFlags := []cli.Flag{
|
balanceFlags := []cli.Flag{
|
||||||
walletPathFlag,
|
walletPathFlag,
|
||||||
|
tokenFlag,
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "addr",
|
Name: "addr",
|
||||||
Usage: "Address to use",
|
Usage: "Address to use",
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
|
||||||
Name: "token",
|
|
||||||
Usage: "Token to use",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
balanceFlags = append(balanceFlags, options.RPC...)
|
balanceFlags = append(balanceFlags, options.RPC...)
|
||||||
importFlags := []cli.Flag{
|
importFlags := []cli.Flag{
|
||||||
|
@ -51,20 +59,22 @@ func newNEP5Commands() []cli.Command {
|
||||||
outFlag,
|
outFlag,
|
||||||
fromAddrFlag,
|
fromAddrFlag,
|
||||||
toAddrFlag,
|
toAddrFlag,
|
||||||
cli.StringFlag{
|
tokenFlag,
|
||||||
Name: "token",
|
gasFlag,
|
||||||
Usage: "Token to use",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "amount",
|
Name: "amount",
|
||||||
Usage: "Amount of asset to send",
|
Usage: "Amount of asset to send",
|
||||||
},
|
},
|
||||||
flags.Fixed8Flag{
|
|
||||||
Name: "gas",
|
|
||||||
Usage: "Amount of GAS to attach to a tx",
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
transferFlags = append(transferFlags, options.RPC...)
|
transferFlags = append(transferFlags, options.RPC...)
|
||||||
|
multiTransferFlags := []cli.Flag{
|
||||||
|
walletPathFlag,
|
||||||
|
outFlag,
|
||||||
|
fromAddrFlag,
|
||||||
|
tokenFlag,
|
||||||
|
gasFlag,
|
||||||
|
}
|
||||||
|
multiTransferFlags = append(multiTransferFlags, options.RPC...)
|
||||||
return []cli.Command{
|
return []cli.Command{
|
||||||
{
|
{
|
||||||
Name: "balance",
|
Name: "balance",
|
||||||
|
@ -114,6 +124,14 @@ func newNEP5Commands() []cli.Command {
|
||||||
Action: transferNEP5,
|
Action: transferNEP5,
|
||||||
Flags: transferFlags,
|
Flags: transferFlags,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "multitransfer",
|
||||||
|
Usage: "transfer NEP5 tokens to multiple recepients",
|
||||||
|
UsageText: `multitransfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr>` +
|
||||||
|
` --token <hash> <addr1>:<amount1> [<addr2>:<amount2> [...]]`,
|
||||||
|
Action: multiTransferNEP5,
|
||||||
|
Flags: multiTransferFlags,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -317,6 +335,64 @@ func removeNEP5Token(ctx *cli.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func multiTransferNEP5(ctx *cli.Context) error {
|
||||||
|
wall, err := openWallet(ctx.String("wallet"))
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(err, 1)
|
||||||
|
}
|
||||||
|
defer wall.Close()
|
||||||
|
|
||||||
|
fromFlag := ctx.Generic("from").(*flags.Address)
|
||||||
|
from := fromFlag.Uint160()
|
||||||
|
acc := wall.GetAccount(from)
|
||||||
|
if acc == nil {
|
||||||
|
return cli.NewExitError(fmt.Errorf("can't find account for the address: %s", fromFlag), 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
gctx, cancel := options.GetTimeoutContext(ctx)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
c, err := options.GetRPCClient(gctx, ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
token, err := getMatchingToken(wall, ctx.String("token"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Can't find matching token in the wallet. Querying RPC-node for balances.")
|
||||||
|
token, err = getMatchingTokenRPC(c, from, ctx.String("token"))
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(err, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.NArg() == 0 {
|
||||||
|
return cli.NewExitError("empty recepients list", 1)
|
||||||
|
}
|
||||||
|
var recepients []client.AddrAndAmount
|
||||||
|
for i := 0; i < ctx.NArg(); i++ {
|
||||||
|
arg := ctx.Args().Get(i)
|
||||||
|
ss := strings.SplitN(arg, ":", 2)
|
||||||
|
if len(ss) != 2 {
|
||||||
|
return cli.NewExitError("invalid recepient format", 1)
|
||||||
|
}
|
||||||
|
addr, err := address.StringToUint160(ss[0])
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(fmt.Errorf("invalid address: '%s'", ss[0]), 1)
|
||||||
|
}
|
||||||
|
amount, err := util.FixedNFromString(ss[1], int(token.Decimals))
|
||||||
|
if err != nil {
|
||||||
|
return cli.NewExitError(fmt.Errorf("invalid amount: %v", err), 1)
|
||||||
|
}
|
||||||
|
recepients = append(recepients, client.AddrAndAmount{
|
||||||
|
Address: addr,
|
||||||
|
Amount: amount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return signAndSendTransfer(ctx, c, acc, token, recepients)
|
||||||
|
}
|
||||||
|
|
||||||
func transferNEP5(ctx *cli.Context) error {
|
func transferNEP5(ctx *cli.Context) error {
|
||||||
wall, err := openWallet(ctx.String("wallet"))
|
wall, err := openWallet(ctx.String("wallet"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -355,6 +431,14 @@ func transferNEP5(ctx *cli.Context) error {
|
||||||
return cli.NewExitError(fmt.Errorf("invalid amount: %v", err), 1)
|
return cli.NewExitError(fmt.Errorf("invalid amount: %v", err), 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return signAndSendTransfer(ctx, c, acc, token, []client.AddrAndAmount{{
|
||||||
|
Address: to,
|
||||||
|
Amount: amount,
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
|
||||||
|
func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, token *wallet.Token,
|
||||||
|
recepients []client.AddrAndAmount) error {
|
||||||
gas := flags.Fixed8FromContext(ctx, "gas")
|
gas := flags.Fixed8FromContext(ctx, "gas")
|
||||||
|
|
||||||
if pass, err := readPassword("Password > "); err != nil {
|
if pass, err := readPassword("Password > "); err != nil {
|
||||||
|
@ -363,7 +447,7 @@ func transferNEP5(ctx *cli.Context) error {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
tx, err := c.CreateNEP5TransferTx(acc, to, token.Hash, amount, int64(gas))
|
tx, err := c.CreateNEP5MultiTransferTx(acc, token.Hash, int64(gas), recepients...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cli.NewExitError(err, 1)
|
return cli.NewExitError(err, 1)
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,12 @@ import (
|
||||||
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AddrAndAmount represents target address and token amount for transfer.
|
||||||
|
type AddrAndAmount struct {
|
||||||
|
Address util.Uint160
|
||||||
|
Amount int64
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// NeoContractHash is a hash of the NEO native contract.
|
// NeoContractHash is a hash of the NEO native contract.
|
||||||
NeoContractHash, _ = util.Uint160DecodeStringLE("9bde8f209c88dd0e7ca3bf0af0f476cdd8207789")
|
NeoContractHash, _ = util.Uint160DecodeStringLE("9bde8f209c88dd0e7ca3bf0af0f476cdd8207789")
|
||||||
|
@ -104,15 +110,25 @@ func (c *Client) NEP5TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) {
|
||||||
// (in FixedN format using contract's number of decimals) to given account and
|
// (in FixedN format using contract's number of decimals) to given account and
|
||||||
// returns it. The returned transaction is not signed.
|
// returns it. The returned transaction is not signed.
|
||||||
func (c *Client) CreateNEP5TransferTx(acc *wallet.Account, to util.Uint160, token util.Uint160, amount int64, gas int64) (*transaction.Transaction, error) {
|
func (c *Client) CreateNEP5TransferTx(acc *wallet.Account, to util.Uint160, token util.Uint160, amount int64, gas int64) (*transaction.Transaction, error) {
|
||||||
|
return c.CreateNEP5MultiTransferTx(acc, token, gas, AddrAndAmount{
|
||||||
|
Address: to,
|
||||||
|
Amount: amount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateNEP5MultiTransferTx creates an invocation transaction for performing NEP5 transfers
|
||||||
|
// from a single sender to multiple recepients.
|
||||||
|
func (c *Client) CreateNEP5MultiTransferTx(acc *wallet.Account, token util.Uint160, gas int64, recepients ...AddrAndAmount) (*transaction.Transaction, error) {
|
||||||
from, err := address.StringToUint160(acc.Address)
|
from, err := address.StringToUint160(acc.Address)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("bad account address: %v", err)
|
return nil, fmt.Errorf("bad account address: %v", err)
|
||||||
}
|
}
|
||||||
// Note: we don't use invoke function here because it requires
|
|
||||||
// 2 round trips instead of one.
|
|
||||||
w := io.NewBufBinWriter()
|
w := io.NewBufBinWriter()
|
||||||
emit.AppCallWithOperationAndArgs(w.BinWriter, token, "transfer", from, to, amount)
|
for i := range recepients {
|
||||||
emit.Opcode(w.BinWriter, opcode.ASSERT)
|
emit.AppCallWithOperationAndArgs(w.BinWriter, token, "transfer", from,
|
||||||
|
recepients[i].Address, recepients[i].Amount)
|
||||||
|
emit.Opcode(w.BinWriter, opcode.ASSERT)
|
||||||
|
}
|
||||||
|
|
||||||
script := w.Bytes()
|
script := w.Bytes()
|
||||||
result, err := c.InvokeScript(script, []transaction.Cosigner{
|
result, err := c.InvokeScript(script, []transaction.Cosigner{
|
||||||
|
@ -162,6 +178,20 @@ func (c *Client) TransferNEP5(acc *wallet.Account, to util.Uint160, token util.U
|
||||||
return c.SendRawTransaction(tx)
|
return c.SendRawTransaction(tx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MultiTransferNEP5 is similar to TransferNEP5, buf allows to have multiple recepients.
|
||||||
|
func (c *Client) MultiTransferNEP5(acc *wallet.Account, token util.Uint160, gas int64, recepients ...AddrAndAmount) (util.Uint256, error) {
|
||||||
|
tx, err := c.CreateNEP5MultiTransferTx(acc, token, gas, recepients...)
|
||||||
|
if err != nil {
|
||||||
|
return util.Uint256{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := acc.SignTx(tx); err != nil {
|
||||||
|
return util.Uint256{}, fmt.Errorf("can't sign tx: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SendRawTransaction(tx)
|
||||||
|
}
|
||||||
|
|
||||||
func topIntFromStack(st []smartcontract.Parameter) (int64, error) {
|
func topIntFromStack(st []smartcontract.Parameter) (int64, error) {
|
||||||
index := len(st) - 1 // top stack element is last in the array
|
index := len(st) - 1 // top stack element is last in the array
|
||||||
var decimals int64
|
var decimals int64
|
||||||
|
|
Loading…
Add table
Reference in a new issue