Merge pull request #1914 from nspcc-dev/cli/nep17_with_cosigners

rpc, cli: refactor the way of transactions signing
This commit is contained in:
Roman Khimov 2021-04-23 10:56:18 +03:00 committed by GitHub
commit 402cd2a818
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 708 additions and 600 deletions

150
cli/cmdargs/parser.go Normal file
View file

@ -0,0 +1,150 @@
package cmdargs
import (
"errors"
"fmt"
"strings"
"github.com/nspcc-dev/neo-go/cli/flags"
"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/rpc/client"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/wallet"
"github.com/urfave/cli"
)
const (
// CosignersSeparator marks the start of cosigners cli args.
CosignersSeparator = "--"
// ArrayStartSeparator marks the start of array cli arg.
ArrayStartSeparator = "["
// ArrayEndSeparator marks the end of array cli arg.
ArrayEndSeparator = "]"
)
// GetSignersFromContext returns signers parsed from context args starting
// from the specified offset.
func GetSignersFromContext(ctx *cli.Context, offset int) ([]transaction.Signer, *cli.ExitError) {
args := ctx.Args()
var signers []transaction.Signer
if args.Present() && len(args) > offset {
for i, c := range args[offset:] {
cosigner, err := parseCosigner(c)
if err != nil {
return nil, cli.NewExitError(fmt.Errorf("failed to parse signer #%d: %w", i, err), 1)
}
signers = append(signers, cosigner)
}
}
return signers, nil
}
func parseCosigner(c string) (transaction.Signer, error) {
var (
err error
res = transaction.Signer{
Scopes: transaction.CalledByEntry,
}
)
data := strings.SplitN(c, ":", 2)
s := data[0]
res.Account, err = flags.ParseAddress(s)
if err != nil {
return res, err
}
if len(data) > 1 {
res.Scopes, err = transaction.ScopesFromString(data[1])
if err != nil {
return transaction.Signer{}, err
}
}
return res, nil
}
// GetDataFromContext returns data parameter from context args.
func GetDataFromContext(ctx *cli.Context) (int, interface{}, *cli.ExitError) {
var (
data interface{}
offset int
params []smartcontract.Parameter
err error
)
args := ctx.Args()
if args.Present() {
offset, params, err = ParseParams(args, true)
if err != nil {
return offset, nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1)
}
if len(params) != 1 {
return offset, nil, cli.NewExitError("'data' should be represented as a single parameter", 1)
}
data, err = smartcontract.ExpandParameterToEmitable(params[0])
if err != nil {
return offset, nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1)
}
}
return offset, data, nil
}
// ParseParams extracts array of smartcontract.Parameter from the given args and
// returns the number of handled words, the array itself and an error.
// `calledFromMain` denotes whether the method was called from the outside or
// recursively and used to check if CosignersSeparator and ArrayEndSeparator are
// allowed to be in `args` sequence.
func ParseParams(args []string, calledFromMain bool) (int, []smartcontract.Parameter, error) {
res := []smartcontract.Parameter{}
for k := 0; k < len(args); {
s := args[k]
switch s {
case CosignersSeparator:
if calledFromMain {
return k + 1, res, nil // `1` to convert index to numWordsRead
}
return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
case ArrayStartSeparator:
numWordsRead, array, err := ParseParams(args[k+1:], false)
if err != nil {
return 0, nil, fmt.Errorf("failed to parse array: %w", err)
}
res = append(res, smartcontract.Parameter{
Type: smartcontract.ArrayType,
Value: array,
})
k += 1 + numWordsRead // `1` for opening bracket
case ArrayEndSeparator:
if calledFromMain {
return 0, nil, errors.New("invalid array syntax: missing opening bracket")
}
return k + 1, res, nil // `1`to convert index to numWordsRead
default:
param, err := smartcontract.NewParameterFromString(s)
if err != nil {
return 0, nil, fmt.Errorf("failed to parse argument #%d: %w", k+1, err)
}
res = append(res, *param)
k++
}
}
if calledFromMain {
return len(args), res, nil
}
return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
}
// GetSignersAccounts returns the list of signers combined with the corresponding
// accounts from the provided wallet.
func GetSignersAccounts(wall *wallet.Wallet, signers []transaction.Signer) ([]client.SignerAccount, error) {
signersAccounts := make([]client.SignerAccount, len(signers))
for i := range signers {
signerAcc := wall.GetAccount(signers[i].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))
}
signersAccounts[i] = client.SignerAccount{
Signer: signers[i],
Account: signerAcc,
}
}
return signersAccounts, nil
}

362
cli/cmdargs/parser_test.go Normal file
View file

@ -0,0 +1,362 @@
package cmdargs
import (
"strings"
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require"
)
func TestParseCosigner(t *testing.T) {
acc := util.Uint160{1, 3, 5, 7}
testCases := map[string]transaction.Signer{
acc.StringLE(): {
Account: acc,
Scopes: transaction.CalledByEntry,
},
"0x" + acc.StringLE(): {
Account: acc,
Scopes: transaction.CalledByEntry,
},
acc.StringLE() + ":Global": {
Account: acc,
Scopes: transaction.Global,
},
acc.StringLE() + ":CalledByEntry": {
Account: acc,
Scopes: transaction.CalledByEntry,
},
acc.StringLE() + ":None": {
Account: acc,
Scopes: transaction.None,
},
acc.StringLE() + ":CalledByEntry,CustomContracts": {
Account: acc,
Scopes: transaction.CalledByEntry | transaction.CustomContracts,
},
}
for s, expected := range testCases {
actual, err := parseCosigner(s)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
errorCases := []string{
acc.StringLE() + "0",
acc.StringLE() + ":Unknown",
acc.StringLE() + ":Global,CustomContracts",
acc.StringLE() + ":Global,None",
}
for _, s := range errorCases {
_, err := parseCosigner(s)
require.Error(t, err)
}
}
func TestParseParams_CalledFromItself(t *testing.T) {
testCases := map[string]struct {
WordsRead int
Value []smartcontract.Parameter
}{
"]": {
WordsRead: 1,
Value: []smartcontract.Parameter{},
},
"[ [ ] ] ]": {
WordsRead: 5,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{},
},
},
},
},
},
"a b c ]": {
WordsRead: 4,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.StringType,
Value: "b",
},
{
Type: smartcontract.StringType,
Value: "c",
},
},
},
"a [ b [ [ c d ] e ] ] f ] extra items": {
WordsRead: 13, // the method should return right after the last bracket, as calledFromMain == false
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "b",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "c",
},
{
Type: smartcontract.StringType,
Value: "d",
},
},
},
{
Type: smartcontract.StringType,
Value: "e",
},
},
},
},
},
{
Type: smartcontract.StringType,
Value: "f",
},
},
},
}
for str, expected := range testCases {
input := strings.Split(str, " ")
offset, actual, err := ParseParams(input, false)
require.NoError(t, err)
require.Equal(t, expected.WordsRead, offset)
require.Equal(t, expected.Value, actual)
}
errorCases := []string{
"[ ]",
"[ a b [ c ] d ]",
"[ ] --",
"--",
"not-int:integer ]",
}
for _, str := range errorCases {
input := strings.Split(str, " ")
_, _, err := ParseParams(input, false)
require.Error(t, err)
}
}
func TestParseParams_CalledFromOutside(t *testing.T) {
testCases := map[string]struct {
WordsRead int
Parameters []smartcontract.Parameter
}{
"-- cosigner1": {
WordsRead: 1, // the `--` only
Parameters: []smartcontract.Parameter{},
},
"a b c": {
WordsRead: 3,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.StringType,
Value: "b",
},
{
Type: smartcontract.StringType,
Value: "c",
},
},
},
"a b c -- cosigner1": {
WordsRead: 4,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.StringType,
Value: "b",
},
{
Type: smartcontract.StringType,
Value: "c",
},
},
},
"a [ b [ [ c d ] e ] ] f": {
WordsRead: 12,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "b",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "c",
},
{
Type: smartcontract.StringType,
Value: "d",
},
},
},
{
Type: smartcontract.StringType,
Value: "e",
},
},
},
},
},
{
Type: smartcontract.StringType,
Value: "f",
},
},
},
"a [ b ] -- cosigner1 cosigner2": {
WordsRead: 5,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "b",
},
},
},
},
},
"a [ b ]": {
WordsRead: 4,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "b",
},
},
},
},
},
"a [ b ] [ [ c ] ] [ [ [ d ] ] ]": {
WordsRead: 16,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "b",
},
},
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "c",
},
},
},
},
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "d",
},
},
},
},
},
},
},
},
},
}
for str, expected := range testCases {
input := strings.Split(str, " ")
offset, arr, err := ParseParams(input, true)
require.NoError(t, err)
require.Equal(t, expected.WordsRead, offset)
require.Equal(t, expected.Parameters, arr)
}
errorCases := []string{
"[",
"]",
"[ [ ]",
"[ [ ] --",
"[ -- ]",
}
for _, str := range errorCases {
input := strings.Split(str, " ")
_, _, err := ParseParams(input, true)
require.Error(t, err)
}
}

View file

@ -226,6 +226,33 @@ func TestContractDeployWithData(t *testing.T) {
require.Equal(t, []byte("take_me_to_church"), res.Stack[0].Value()) require.Equal(t, []byte("take_me_to_church"), res.Stack[0].Value())
} }
func deployVerifyContract(t *testing.T, e *executor) util.Uint160 {
tmpDir := path.Join(os.TempDir(), "neogo.test.deployverifycontract")
require.NoError(t, os.Mkdir(tmpDir, os.ModePerm))
t.Cleanup(func() {
os.RemoveAll(tmpDir)
})
// deploy verification contract
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)
return hVerify
}
func TestComlileAndInvokeFunction(t *testing.T) { func TestComlileAndInvokeFunction(t *testing.T) {
e := newExecutor(t, true) e := newExecutor(t, true)
@ -315,23 +342,7 @@ func TestComlileAndInvokeFunction(t *testing.T) {
require.Equal(t, []byte("on create|sub create"), res.Stack[0].Value()) require.Equal(t, []byte("on create|sub create"), res.Stack[0].Value())
// deploy verification contract // deploy verification contract
nefName = path.Join(tmpDir, "verify.nef") hVerify := deployVerifyContract(t, e)
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)
t.Run("real invoke", func(t *testing.T) { t.Run("real invoke", func(t *testing.T) {
cmd := []string{"neo-go", "contract", "invokefunction", cmd := []string{"neo-go", "contract", "invokefunction",

View file

@ -132,6 +132,8 @@ func TestNEP17Transfer(t *testing.T) {
b, _ := e.Chain.GetGoverningTokenBalance(sh) b, _ := e.Chain.GetGoverningTokenBalance(sh)
require.Equal(t, big.NewInt(1), b) require.Equal(t, big.NewInt(1), b)
hVerify := deployVerifyContract(t, e)
t.Run("default address", func(t *testing.T) { t.Run("default address", func(t *testing.T) {
const validatorDefault = "NTh9TnZTstvAePEYWDGLLxidBikJE24uTo" const validatorDefault = "NTh9TnZTstvAePEYWDGLLxidBikJE24uTo"
e.In.WriteString("one\r") e.In.WriteString("one\r")
@ -159,21 +161,39 @@ func TestNEP17Transfer(t *testing.T) {
require.Equal(t, big.NewInt(41), b) require.Equal(t, big.NewInt(41), b)
}) })
validTil := e.Chain.BlockHeight() + 100
cmd := []string{
"neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://" + e.RPC.Addr,
"--wallet", validatorWallet,
"--to", address.Uint160ToString(e.Chain.GetNotaryContractScriptHash()),
"--token", "GAS",
"--amount", "1",
"--from", validatorAddr,
"[", validatorAddr, strconv.Itoa(int(validTil)), "]"}
t.Run("with data", func(t *testing.T) { t.Run("with data", func(t *testing.T) {
e.In.WriteString("one\r") e.In.WriteString("one\r")
validTil := e.Chain.BlockHeight() + 100 e.Run(t, cmd...)
e.Run(t, []string{
"neo-go", "wallet", "nep17", "transfer",
"--rpc-endpoint", "http://" + e.RPC.Addr,
"--wallet", validatorWallet,
"--to", address.Uint160ToString(e.Chain.GetNotaryContractScriptHash()),
"--token", "GAS",
"--amount", "1",
"--from", validatorAddr,
"[", validatorAddr, strconv.Itoa(int(validTil)), "]",
}...)
e.checkTxPersisted(t) e.checkTxPersisted(t)
}) })
t.Run("with data and signers", func(t *testing.T) {
t.Run("invalid sender's scope", func(t *testing.T) {
e.In.WriteString("one\r")
e.RunWithError(t, append(cmd, "--", validatorAddr+":None")...)
})
t.Run("good", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, append(cmd, "--", validatorAddr+":Global")...) // CalledByEntry is enough, but it's the default value, so check something else
e.checkTxPersisted(t)
})
t.Run("several signers", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, append(cmd, "--", validatorAddr, hVerify.StringLE())...)
e.checkTxPersisted(t)
})
})
} }
func TestNEP17MultiTransfer(t *testing.T) { func TestNEP17MultiTransfer(t *testing.T) {
@ -191,17 +211,38 @@ func TestNEP17MultiTransfer(t *testing.T) {
"GAS:" + privs[1].Address() + ":7", "GAS:" + privs[1].Address() + ":7",
neoContractHash.StringLE() + ":" + privs[2].Address() + ":13", neoContractHash.StringLE() + ":" + privs[2].Address() + ":13",
} }
hVerify := deployVerifyContract(t, e)
e.In.WriteString("one\r") t.Run("no cosigners", func(t *testing.T) {
e.Run(t, args...) e.In.WriteString("one\r")
e.checkTxPersisted(t) e.Run(t, args...)
e.checkTxPersisted(t)
b, _ := e.Chain.GetGoverningTokenBalance(privs[0].GetScriptHash()) b, _ := e.Chain.GetGoverningTokenBalance(privs[0].GetScriptHash())
require.Equal(t, big.NewInt(42), b) require.Equal(t, big.NewInt(42), b)
b = e.Chain.GetUtilityTokenBalance(privs[1].GetScriptHash()) b = e.Chain.GetUtilityTokenBalance(privs[1].GetScriptHash())
require.Equal(t, big.NewInt(int64(fixedn.Fixed8FromInt64(7))), b) require.Equal(t, big.NewInt(int64(fixedn.Fixed8FromInt64(7))), b)
b, _ = e.Chain.GetGoverningTokenBalance(privs[2].GetScriptHash()) b, _ = e.Chain.GetGoverningTokenBalance(privs[2].GetScriptHash())
require.Equal(t, big.NewInt(13), b) require.Equal(t, big.NewInt(13), b)
})
t.Run("invalid sender scope", func(t *testing.T) {
e.In.WriteString("one\r")
e.RunWithError(t, append(args,
"--", validatorAddr+":None")...) // invalid sender scope
})
t.Run("Global sender scope", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, append(args,
"--", validatorAddr+":Global")...)
e.checkTxPersisted(t)
})
t.Run("Several cosigners", func(t *testing.T) {
e.In.WriteString("one\r")
e.Run(t, append(args,
"--", validatorAddr, hVerify.StringLE())...)
e.checkTxPersisted(t)
})
} }
func TestNEP17ImportToken(t *testing.T) { func TestNEP17ImportToken(t *testing.T) {

View file

@ -11,6 +11,7 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"github.com/nspcc-dev/neo-go/cli/cmdargs"
"github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/input" "github.com/nspcc-dev/neo-go/cli/input"
"github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/options"
@ -86,11 +87,6 @@ func init() {
func RuntimeNotify(args []interface{}) { func RuntimeNotify(args []interface{}) {
runtime.Notify(notificationName, args) runtime.Notify(notificationName, args)
}` }`
// cosignersSeparator is a special value which is used to distinguish
// parameters and cosigners for invoke* commands
cosignersSeparator = "--"
arrayStartSeparator = "["
arrayEndSeparator = "]"
) )
// NewCommands returns 'contract' command. // NewCommands returns 'contract' command.
@ -513,6 +509,7 @@ func invokeFunction(ctx *cli.Context) error {
func invokeInternal(ctx *cli.Context, signAndPush bool) error { func invokeInternal(ctx *cli.Context, signAndPush bool) error {
var ( var (
err error err error
exitErr *cli.ExitError
gas fixedn.Fixed8 gas fixedn.Fixed8
operation string operation string
params = make([]smartcontract.Parameter, 0) params = make([]smartcontract.Parameter, 0)
@ -540,21 +537,16 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
paramsStart++ paramsStart++
if len(args) > paramsStart { if len(args) > paramsStart {
cosignersOffset, params, err = ParseParams(args[paramsStart:], true) cosignersOffset, params, err = cmdargs.ParseParams(args[paramsStart:], true)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
} }
cosignersStart := paramsStart + cosignersOffset cosignersStart := paramsStart + cosignersOffset
if len(args) > cosignersStart { cosigners, exitErr = cmdargs.GetSignersFromContext(ctx, cosignersStart)
for i, c := range args[cosignersStart:] { if exitErr != nil {
cosigner, err := parseCosigner(c) return exitErr
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to parse cosigner #%d: %w", i+1, err), 1)
}
cosigners = append(cosigners, cosigner)
}
} }
if signAndPush { if signAndPush {
@ -563,15 +555,9 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
if err != nil { if err != nil {
return err return err
} }
for i := range cosigners { cosignersAccounts, err = cmdargs.GetSignersAccounts(wall, cosigners)
cosignerAcc := wall.GetAccount(cosigners[i].Account) if err != nil {
if cosignerAcc == nil { return cli.NewExitError(fmt.Errorf("failed to calculate network fee: %w", err), 1)
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) gctx, cancel := options.GetTimeoutContext(ctx)
@ -625,52 +611,6 @@ func invokeInternal(ctx *cli.Context, signAndPush bool) error {
return nil return nil
} }
// ParseParams extracts array of smartcontract.Parameter from the given args and
// returns the number of handled words, the array itself and an error.
// `calledFromMain` denotes whether the method was called from the outside or
// recursively and used to check if cosignersSeparator and closing bracket are
// allowed to be in `args` sequence.
func ParseParams(args []string, calledFromMain bool) (int, []smartcontract.Parameter, error) {
res := []smartcontract.Parameter{}
for k := 0; k < len(args); {
s := args[k]
switch s {
case cosignersSeparator:
if calledFromMain {
return k + 1, res, nil // `1` to convert index to numWordsRead
}
return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
case arrayStartSeparator:
numWordsRead, array, err := ParseParams(args[k+1:], false)
if err != nil {
return 0, nil, fmt.Errorf("failed to parse array: %w", err)
}
res = append(res, smartcontract.Parameter{
Type: smartcontract.ArrayType,
Value: array,
})
k += 1 + numWordsRead // `1` for opening bracket
case arrayEndSeparator:
if calledFromMain {
return 0, nil, errors.New("invalid array syntax: missing opening bracket")
}
return k + 1, res, nil // `1`to convert index to numWordsRead
default:
param, err := smartcontract.NewParameterFromString(s)
if err != nil {
return 0, nil, fmt.Errorf("failed to parse argument #%d: %w", k+1, err)
}
res = append(res, *param)
k++
}
}
if calledFromMain {
return len(args), res, nil
}
return 0, []smartcontract.Parameter{}, errors.New("invalid array syntax: missing closing bracket")
}
func testInvokeScript(ctx *cli.Context) error { func testInvokeScript(ctx *cli.Context) error {
src := ctx.String("in") src := ctx.String("in")
if len(src) == 0 { if len(src) == 0 {
@ -686,16 +626,9 @@ func testInvokeScript(ctx *cli.Context) error {
return cli.NewExitError(fmt.Errorf("failed to restore .nef file: %w", err), 1) return cli.NewExitError(fmt.Errorf("failed to restore .nef file: %w", err), 1)
} }
args := ctx.Args() signers, exitErr := cmdargs.GetSignersFromContext(ctx, 0)
var signers []transaction.Signer if exitErr != nil {
if args.Present() { return exitErr
for i, c := range args[:] {
cosigner, err := parseCosigner(c)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to parse signer #%d: %w", i+1, err), 1)
}
signers = append(signers, cosigner)
}
} }
gctx, cancel := options.GetTimeoutContext(ctx) gctx, cancel := options.GetTimeoutContext(ctx)
@ -838,7 +771,7 @@ func contractDeploy(ctx *cli.Context) error {
return cli.NewExitError(fmt.Errorf("failed to restore manifest file: %w", err), 1) return cli.NewExitError(fmt.Errorf("failed to restore manifest file: %w", err), 1)
} }
data, extErr := GetDataFromContext(ctx) _, data, extErr := cmdargs.GetDataFromContext(ctx)
if extErr != nil { if extErr != nil {
return extErr return extErr
} }
@ -898,26 +831,6 @@ func contractDeploy(ctx *cli.Context) error {
return nil return nil
} }
// GetDataFromContext returns data parameter from context args.
func GetDataFromContext(ctx *cli.Context) (interface{}, *cli.ExitError) {
var data interface{}
args := ctx.Args()
if args.Present() {
_, params, err := ParseParams(args, true)
if err != nil {
return nil, cli.NewExitError(fmt.Errorf("unable to parse 'data' parameter: %w", err), 1)
}
if len(params) != 1 {
return nil, cli.NewExitError("'data' should be represented as a single parameter", 1)
}
data, err = smartcontract.ExpandParameterToEmitable(params[0])
if err != nil {
return nil, cli.NewExitError(fmt.Sprintf("failed to convert 'data' to emitable type: %s", err.Error()), 1)
}
}
return data, nil
}
// ParseContractConfig reads contract configuration file (.yaml) and returns unmarshalled ProjectConfig. // ParseContractConfig reads contract configuration file (.yaml) and returns unmarshalled ProjectConfig.
func ParseContractConfig(confFile string) (ProjectConfig, error) { func ParseContractConfig(confFile string) (ProjectConfig, error) {
conf := ProjectConfig{} conf := ProjectConfig{}
@ -932,25 +845,3 @@ func ParseContractConfig(confFile string) (ProjectConfig, error) {
} }
return conf, nil return conf, nil
} }
func parseCosigner(c string) (transaction.Signer, error) {
var (
err error
res = transaction.Signer{
Scopes: transaction.CalledByEntry,
}
)
data := strings.SplitN(c, ":", 2)
s := data[0]
res.Account, err = flags.ParseAddress(s)
if err != nil {
return res, err
}
if len(data) > 1 {
res.Scopes, err = transaction.ScopesFromString(data[1])
if err != nil {
return transaction.Signer{}, err
}
}
return res, nil
}

View file

@ -4,12 +4,8 @@ import (
"flag" "flag"
"io/ioutil" "io/ioutil"
"os" "os"
"strings"
"testing" "testing"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
@ -68,354 +64,3 @@ events:
type: Array type: Array
`, string(manifest)) `, string(manifest))
} }
func TestParseCosigner(t *testing.T) {
acc := util.Uint160{1, 3, 5, 7}
testCases := map[string]transaction.Signer{
acc.StringLE(): {
Account: acc,
Scopes: transaction.CalledByEntry,
},
"0x" + acc.StringLE(): {
Account: acc,
Scopes: transaction.CalledByEntry,
},
acc.StringLE() + ":Global": {
Account: acc,
Scopes: transaction.Global,
},
acc.StringLE() + ":CalledByEntry": {
Account: acc,
Scopes: transaction.CalledByEntry,
},
acc.StringLE() + ":None": {
Account: acc,
Scopes: transaction.None,
},
acc.StringLE() + ":CalledByEntry,CustomContracts": {
Account: acc,
Scopes: transaction.CalledByEntry | transaction.CustomContracts,
},
}
for s, expected := range testCases {
actual, err := parseCosigner(s)
require.NoError(t, err)
require.Equal(t, expected, actual)
}
errorCases := []string{
acc.StringLE() + "0",
acc.StringLE() + ":Unknown",
acc.StringLE() + ":Global,CustomContracts",
acc.StringLE() + ":Global,None",
}
for _, s := range errorCases {
_, err := parseCosigner(s)
require.Error(t, err)
}
}
func TestParseParams_CalledFromItself(t *testing.T) {
testCases := map[string]struct {
WordsRead int
Value []smartcontract.Parameter
}{
"]": {
WordsRead: 1,
Value: []smartcontract.Parameter{},
},
"[ [ ] ] ]": {
WordsRead: 5,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{},
},
},
},
},
},
"a b c ]": {
WordsRead: 4,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.StringType,
Value: "b",
},
{
Type: smartcontract.StringType,
Value: "c",
},
},
},
"a [ b [ [ c d ] e ] ] f ] extra items": {
WordsRead: 13, // the method should return right after the last bracket, as calledFromMain == false
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "b",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "c",
},
{
Type: smartcontract.StringType,
Value: "d",
},
},
},
{
Type: smartcontract.StringType,
Value: "e",
},
},
},
},
},
{
Type: smartcontract.StringType,
Value: "f",
},
},
},
}
for str, expected := range testCases {
input := strings.Split(str, " ")
offset, actual, err := ParseParams(input, false)
require.NoError(t, err)
require.Equal(t, expected.WordsRead, offset)
require.Equal(t, expected.Value, actual)
}
errorCases := []string{
"[ ]",
"[ a b [ c ] d ]",
"[ ] --",
"--",
"not-int:integer ]",
}
for _, str := range errorCases {
input := strings.Split(str, " ")
_, _, err := ParseParams(input, false)
require.Error(t, err)
}
}
func TestParseParams_CalledFromOutside(t *testing.T) {
testCases := map[string]struct {
WordsRead int
Parameters []smartcontract.Parameter
}{
"-- cosigner1": {
WordsRead: 1, // the `--` only
Parameters: []smartcontract.Parameter{},
},
"a b c": {
WordsRead: 3,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.StringType,
Value: "b",
},
{
Type: smartcontract.StringType,
Value: "c",
},
},
},
"a b c -- cosigner1": {
WordsRead: 4,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.StringType,
Value: "b",
},
{
Type: smartcontract.StringType,
Value: "c",
},
},
},
"a [ b [ [ c d ] e ] ] f": {
WordsRead: 12,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "b",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "c",
},
{
Type: smartcontract.StringType,
Value: "d",
},
},
},
{
Type: smartcontract.StringType,
Value: "e",
},
},
},
},
},
{
Type: smartcontract.StringType,
Value: "f",
},
},
},
"a [ b ] -- cosigner1 cosigner2": {
WordsRead: 5,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "b",
},
},
},
},
},
"a [ b ]": {
WordsRead: 4,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "b",
},
},
},
},
},
"a [ b ] [ [ c ] ] [ [ [ d ] ] ]": {
WordsRead: 16,
Parameters: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "a",
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "b",
},
},
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "c",
},
},
},
},
},
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.ArrayType,
Value: []smartcontract.Parameter{
{
Type: smartcontract.StringType,
Value: "d",
},
},
},
},
},
},
},
},
},
}
for str, expected := range testCases {
input := strings.Split(str, " ")
offset, arr, err := ParseParams(input, true)
require.NoError(t, err)
require.Equal(t, expected.WordsRead, offset)
require.Equal(t, expected.Parameters, arr)
}
errorCases := []string{
"[",
"]",
"[ [ ]",
"[ [ ] --",
"[ -- ]",
}
for _, str := range errorCases {
input := strings.Split(str, " ")
_, _, err := ParseParams(input, true)
require.Error(t, err)
}
}

View file

@ -6,10 +6,10 @@ import (
"math/big" "math/big"
"strings" "strings"
"github.com/nspcc-dev/neo-go/cli/cmdargs"
"github.com/nspcc-dev/neo-go/cli/flags" "github.com/nspcc-dev/neo-go/cli/flags"
"github.com/nspcc-dev/neo-go/cli/options" "github.com/nspcc-dev/neo-go/cli/options"
"github.com/nspcc-dev/neo-go/cli/paramcontext" "github.com/nspcc-dev/neo-go/cli/paramcontext"
smartcontractcli "github.com/nspcc-dev/neo-go/cli/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/encoding/address" "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/encoding/fixedn"
"github.com/nspcc-dev/neo-go/pkg/rpc/client" "github.com/nspcc-dev/neo-go/pkg/rpc/client"
@ -112,18 +112,21 @@ func newNEP17Commands() []cli.Command {
{ {
Name: "transfer", Name: "transfer",
Usage: "transfer NEP17 tokens", Usage: "transfer NEP17 tokens",
UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash> --amount string [data]", UsageText: "transfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr> --to <addr> --token <hash> --amount string [data] [-- <cosigner1:Scope> [<cosigner2> [...]]]",
Action: transferNEP17, Action: transferNEP17,
Flags: transferFlags, Flags: transferFlags,
Description: `Transfers specified NEP17 token amount with optional 'data' parameter attached to the transfer. Description: `Transfers specified NEP17 token amount with optional 'data' parameter and cosigners
See 'contract testinvokefunction' documentation for the details about 'data' list attached to the transfer. See 'contract testinvokefunction' documentation
parameter. If no 'data' is given then default nil value will be used`, for the details about 'data' parameter and cosigners syntax. If no 'data' is
given then default nil value will be used. If no cosigners are given then the
sender with CalledByEntry scope will be used as the only signer.
`,
}, },
{ {
Name: "multitransfer", Name: "multitransfer",
Usage: "transfer NEP17 tokens to multiple recipients", Usage: "transfer NEP17 tokens to multiple recipients",
UsageText: `multitransfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr>` + UsageText: `multitransfer --wallet <path> --rpc-endpoint <node> --timeout <time> --from <addr>` +
` <token1>:<addr1>:<amount1> [<token2>:<addr2>:<amount2> [...]]`, ` <token1>:<addr1>:<amount1> [<token2>:<addr2>:<amount2> [...]] [-- <cosigner1:Scope> [<cosigner2> [...]]]`,
Action: multiTransferNEP17, Action: multiTransferNEP17,
Flags: multiTransferFlags, Flags: multiTransferFlags,
}, },
@ -381,10 +384,17 @@ func multiTransferNEP17(ctx *cli.Context) error {
if ctx.NArg() == 0 { if ctx.NArg() == 0 {
return cli.NewExitError("empty recipients list", 1) return cli.NewExitError("empty recipients list", 1)
} }
var recipients []client.TransferTarget var (
recipients []client.TransferTarget
cosignersOffset = ctx.NArg()
)
cache := make(map[string]*wallet.Token) cache := make(map[string]*wallet.Token)
for i := 0; i < ctx.NArg(); i++ { for i := 0; i < ctx.NArg(); i++ {
arg := ctx.Args().Get(i) arg := ctx.Args().Get(i)
if arg == cmdargs.CosignersSeparator {
cosignersOffset = i + 1
break
}
ss := strings.SplitN(arg, ":", 3) ss := strings.SplitN(arg, ":", 3)
if len(ss) != 3 { if len(ss) != 3 {
return cli.NewExitError("send format must be '<token>:<addr>:<amount>", 1) return cli.NewExitError("send format must be '<token>:<addr>:<amount>", 1)
@ -417,7 +427,16 @@ func multiTransferNEP17(ctx *cli.Context) error {
}) })
} }
return signAndSendTransfer(ctx, c, acc, recipients) cosigners, extErr := cmdargs.GetSignersFromContext(ctx, cosignersOffset)
if extErr != nil {
return extErr
}
cosignersAccounts, err := cmdargs.GetSignersAccounts(wall, cosigners)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to create NEP17 multitransfer transaction: %w", err), 1)
}
return signAndSendTransfer(ctx, c, acc, recipients, cosignersAccounts)
} }
func transferNEP17(ctx *cli.Context) error { func transferNEP17(ctx *cli.Context) error {
@ -461,23 +480,32 @@ func transferNEP17(ctx *cli.Context) error {
return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1) return cli.NewExitError(fmt.Errorf("invalid amount: %w", err), 1)
} }
data, extErr := smartcontractcli.GetDataFromContext(ctx) cosignersOffset, data, extErr := cmdargs.GetDataFromContext(ctx)
if extErr != nil { if extErr != nil {
return extErr return extErr
} }
cosigners, extErr := cmdargs.GetSignersFromContext(ctx, cosignersOffset)
if extErr != nil {
return extErr
}
cosignersAccounts, err := cmdargs.GetSignersAccounts(wall, cosigners)
if err != nil {
return cli.NewExitError(fmt.Errorf("failed to create NEP17 transfer transaction: %w", err), 1)
}
return signAndSendTransfer(ctx, c, acc, []client.TransferTarget{{ return signAndSendTransfer(ctx, c, acc, []client.TransferTarget{{
Token: token.Hash, Token: token.Hash,
Address: to, Address: to,
Amount: amount.Int64(), Amount: amount.Int64(),
Data: data, Data: data,
}}) }}, cosignersAccounts)
} }
func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, recipients []client.TransferTarget) error { func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account, recipients []client.TransferTarget, cosigners []client.SignerAccount) error {
gas := flags.Fixed8FromContext(ctx, "gas") gas := flags.Fixed8FromContext(ctx, "gas")
tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients) tx, err := c.CreateNEP17MultiTransferTx(acc, int64(gas), recipients, cosigners)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
@ -487,13 +515,10 @@ func signAndSendTransfer(ctx *cli.Context, c *client.Client, acc *wallet.Account
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
} else { } else {
_ = acc.SignTx(c.GetNetwork(), tx) _, err := c.SignAndPushTx(tx, acc, cosigners)
res, err := c.SendRawTransaction(tx)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
fmt.Fprintln(ctx.App.Writer, res.StringLE())
return nil
} }
fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE()) fmt.Fprintln(ctx.App.Writer, tx.Hash().StringLE())

View file

@ -112,23 +112,15 @@ func handleCandidate(ctx *cli.Context, method string, sysGas int64) error {
w := io.NewBufBinWriter() w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, neoContractHash, method, callflag.States, acc.PrivateKey().PublicKey().Bytes()) emit.AppCall(w.BinWriter, neoContractHash, method, callflag.States, acc.PrivateKey().PublicKey().Bytes())
emit.Opcodes(w.BinWriter, opcode.ASSERT) emit.Opcodes(w.BinWriter, opcode.ASSERT)
tx, err := c.CreateTxFromScript(w.Bytes(), acc, sysGas, int64(gas), []client.SignerAccount{{ res, err := c.SignAndPushInvocationTx(w.Bytes(), acc, sysGas, gas, []client.SignerAccount{{
Signer: transaction.Signer{ Signer: transaction.Signer{
Account: acc.Contract.ScriptHash(), Account: acc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry, Scopes: transaction.CalledByEntry,
}, },
Account: acc, Account: acc,
}, }})
})
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(fmt.Errorf("failed to push transaction: %w", err), 1)
} else if err = acc.SignTx(c.GetNetwork(), tx); err != nil {
return cli.NewExitError(fmt.Errorf("can't sign tx: %v", err), 1)
}
res, err := c.SendRawTransaction(tx)
if err != nil {
return cli.NewExitError(err, 1)
} }
fmt.Fprintln(ctx.App.Writer, res.StringLE()) fmt.Fprintln(ctx.App.Writer, res.StringLE())
return nil return nil
@ -182,24 +174,14 @@ func handleVote(ctx *cli.Context) error {
emit.AppCall(w.BinWriter, neoContractHash, "vote", callflag.States, addr.BytesBE(), pubArg) emit.AppCall(w.BinWriter, neoContractHash, "vote", callflag.States, addr.BytesBE(), pubArg)
emit.Opcodes(w.BinWriter, opcode.ASSERT) emit.Opcodes(w.BinWriter, opcode.ASSERT)
tx, err := c.CreateTxFromScript(w.Bytes(), acc, -1, int64(gas), []client.SignerAccount{{ res, err := c.SignAndPushInvocationTx(w.Bytes(), acc, -1, gas, []client.SignerAccount{{
Signer: transaction.Signer{ Signer: transaction.Signer{
Account: acc.Contract.ScriptHash(), Account: acc.Contract.ScriptHash(),
Scopes: transaction.CalledByEntry, Scopes: transaction.CalledByEntry,
}, },
Account: acc, Account: acc}})
}})
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(fmt.Errorf("failed to push invocation transaction: %w", err), 1)
}
if err = acc.SignTx(c.GetNetwork(), tx); err != nil {
return cli.NewExitError(fmt.Errorf("can't sign tx: %v", err), 1)
}
res, err := c.SendRawTransaction(tx)
if err != nil {
return cli.NewExitError(err, 1)
} }
fmt.Fprintln(ctx.App.Writer, res.StringLE()) fmt.Fprintln(ctx.App.Writer, res.StringLE())
return nil return nil

View file

@ -272,7 +272,7 @@ func claimGas(ctx *cli.Context) error {
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
hash, err := c.TransferNEP17(acc, scriptHash, neoContractHash, 0, 0, nil) hash, err := c.TransferNEP17(acc, scriptHash, neoContractHash, 0, 0, nil, nil)
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }

View file

@ -39,20 +39,16 @@ func (c *Client) NEP11BalanceOf(tokenHash, owner util.Uint160) (int64, error) {
// on a given token to move the whole NEP11 token with the specified token ID to // on a given token to move the whole NEP11 token with the specified token ID to
// given account and sends it to the network returning just a hash of it. // given account and sends it to the network returning just a hash of it.
func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160, func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160,
tokenHash util.Uint160, tokenID string, gas int64) (util.Uint256, error) { tokenHash util.Uint160, tokenID string, gas int64, cosigners []SignerAccount) (util.Uint256, error) {
if !c.initDone { if !c.initDone {
return util.Uint256{}, errNetworkNotInitialized return util.Uint256{}, errNetworkNotInitialized
} }
tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, to, tokenID) tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, cosigners, to, tokenID)
if err != nil { if err != nil {
return util.Uint256{}, err return util.Uint256{}, err
} }
if err := acc.SignTx(c.GetNetwork(), tx); err != nil { return c.SignAndPushTx(tx, acc, cosigners)
return util.Uint256{}, fmt.Errorf("can't sign NEP11 transfer tx: %w", err)
}
return c.SendRawTransaction(tx)
} }
// createNEP11TransferTx is an internal helper for TransferNEP11 and // createNEP11TransferTx is an internal helper for TransferNEP11 and
@ -63,7 +59,7 @@ func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160,
// `args` for TransferNEP11: to util.Uint160, tokenID string; // `args` for TransferNEP11: to util.Uint160, tokenID string;
// `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string. // `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string.
func (c *Client) createNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160, func (c *Client) createNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160,
gas int64, args ...interface{}) (*transaction.Transaction, error) { gas int64, cosigners []SignerAccount, args ...interface{}) (*transaction.Transaction, error) {
w := io.NewBufBinWriter() w := io.NewBufBinWriter()
emit.AppCall(w.BinWriter, tokenHash, "transfer", callflag.All, args...) emit.AppCall(w.BinWriter, tokenHash, "transfer", callflag.All, args...)
emit.Opcodes(w.BinWriter, opcode.ASSERT) emit.Opcodes(w.BinWriter, opcode.ASSERT)
@ -74,13 +70,13 @@ func (c *Client) createNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint1
if err != nil { if err != nil {
return nil, fmt.Errorf("bad account address: %w", err) return nil, fmt.Errorf("bad account address: %w", err)
} }
return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, []SignerAccount{{ return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, append([]SignerAccount{{
Signer: transaction.Signer{ Signer: transaction.Signer{
Account: from, Account: from,
Scopes: transaction.CalledByEntry, Scopes: transaction.CalledByEntry,
}, },
Account: acc, Account: acc,
}}) }}, cosigners...))
} }
// Non-divisible NFT methods section start. // Non-divisible NFT methods section start.
@ -114,7 +110,7 @@ func (c *Client) NEP11NDOwnerOf(tokenHash util.Uint160, tokenID string) (util.Ui
// (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
// sends it to the network returning just a hash of it. // sends it to the network returning just a hash of it.
func (c *Client) TransferNEP11D(acc *wallet.Account, to util.Uint160, func (c *Client) TransferNEP11D(acc *wallet.Account, to util.Uint160,
tokenHash util.Uint160, amount int64, tokenID string, gas int64) (util.Uint256, error) { tokenHash util.Uint160, amount int64, tokenID string, gas int64, cosigners []SignerAccount) (util.Uint256, error) {
if !c.initDone { if !c.initDone {
return util.Uint256{}, errNetworkNotInitialized return util.Uint256{}, errNetworkNotInitialized
} }
@ -122,16 +118,12 @@ func (c *Client) TransferNEP11D(acc *wallet.Account, to util.Uint160,
if err != nil { if err != nil {
return util.Uint256{}, fmt.Errorf("bad account address: %w", err) return util.Uint256{}, fmt.Errorf("bad account address: %w", err)
} }
tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, acc.Address, from, to, amount, tokenID) tx, err := c.createNEP11TransferTx(acc, tokenHash, gas, cosigners, acc.Address, from, to, amount, tokenID)
if err != nil { if err != nil {
return util.Uint256{}, err return util.Uint256{}, err
} }
if err := acc.SignTx(c.GetNetwork(), tx); err != nil { return c.SignAndPushTx(tx, acc, cosigners)
return util.Uint256{}, fmt.Errorf("can't sign NEP11 divisible transfer tx: %w", err)
}
return c.SendRawTransaction(tx)
} }
// NEP11DBalanceOf invokes `balanceOf` divisible NEP11 method on a // NEP11DBalanceOf invokes `balanceOf` divisible NEP11 method on a

View file

@ -69,19 +69,23 @@ func (c *Client) NEP17TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) {
// method of a given contract (token) to move specified amount of NEP17 assets // method of a given contract (token) to move specified amount of NEP17 assets
// (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) CreateNEP17TransferTx(acc *wallet.Account, to util.Uint160, token util.Uint160, amount int64, gas int64, data interface{}) (*transaction.Transaction, error) { func (c *Client) CreateNEP17TransferTx(acc *wallet.Account, to util.Uint160,
token util.Uint160, amount int64, gas int64, data interface{}, cosigners []SignerAccount) (*transaction.Transaction, error) {
return c.CreateNEP17MultiTransferTx(acc, gas, []TransferTarget{ return c.CreateNEP17MultiTransferTx(acc, gas, []TransferTarget{
{Token: token, {Token: token,
Address: to, Address: to,
Amount: amount, Amount: amount,
Data: data, Data: data,
}, },
}) }, cosigners)
} }
// CreateNEP17MultiTransferTx creates an invocation transaction for performing NEP17 transfers // CreateNEP17MultiTransferTx creates an invocation transaction for performing
// from a single sender to multiple recipients with the given data. // NEP17 transfers from a single sender to multiple recipients with the given
func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, recipients []TransferTarget) (*transaction.Transaction, error) { // data and cosigners. Transaction's sender is included with the CalledByEntry
// scope by default.
func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64,
recipients []TransferTarget, cosigners []SignerAccount) (*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: %w", err) return nil, fmt.Errorf("bad account address: %w", err)
@ -95,13 +99,13 @@ func (c *Client) CreateNEP17MultiTransferTx(acc *wallet.Account, gas int64, reci
if w.Err != nil { if w.Err != nil {
return nil, fmt.Errorf("failed to create transfer script: %w", w.Err) return nil, fmt.Errorf("failed to create transfer script: %w", w.Err)
} }
return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, []SignerAccount{{ return c.CreateTxFromScript(w.Bytes(), acc, -1, gas, append([]SignerAccount{{
Signer: transaction.Signer{ Signer: transaction.Signer{
Account: from, Account: from,
Scopes: transaction.CalledByEntry, Scopes: transaction.CalledByEntry,
}, },
Account: acc, Account: acc,
}}) }}, cosigners...))
} }
// CreateTxFromScript creates transaction and properly sets cosigners and NetworkFee. // CreateTxFromScript creates transaction and properly sets cosigners and NetworkFee.
@ -143,38 +147,34 @@ func (c *Client) CreateTxFromScript(script []byte, acc *wallet.Account, sysFee,
// TransferNEP17 creates an invocation transaction that invokes 'transfer' method // TransferNEP17 creates an invocation transaction that invokes 'transfer' method
// on a given token to move specified amount of NEP17 assets (in FixedN format // on a given token to move specified amount of NEP17 assets (in FixedN format
// using contract's number of decimals) to given account with data specified and // using contract's number of decimals) to given account with data specified and
// sends it to the network returning just a hash of it. // sends it to the network returning just a hash of it. Cosigners argument
func (c *Client) TransferNEP17(acc *wallet.Account, to util.Uint160, token util.Uint160, amount int64, gas int64, data interface{}) (util.Uint256, error) { // specifies a set of the transaction cosigners (may be nil or may include sender)
// with proper scope and accounts to cosign the transaction. If cosigning is
// impossible (e.g. due to locked cosigner's account) an error is returned.
func (c *Client) TransferNEP17(acc *wallet.Account, to util.Uint160, token util.Uint160,
amount int64, gas int64, data interface{}, cosigners []SignerAccount) (util.Uint256, error) {
if !c.initDone { if !c.initDone {
return util.Uint256{}, errNetworkNotInitialized return util.Uint256{}, errNetworkNotInitialized
} }
tx, err := c.CreateNEP17TransferTx(acc, to, token, amount, gas, data) tx, err := c.CreateNEP17TransferTx(acc, to, token, amount, gas, data, cosigners)
if err != nil { if err != nil {
return util.Uint256{}, err return util.Uint256{}, err
} }
if err := acc.SignTx(c.GetNetwork(), tx); err != nil { return c.SignAndPushTx(tx, acc, cosigners)
return util.Uint256{}, fmt.Errorf("can't sign tx: %w", err)
}
return c.SendRawTransaction(tx)
} }
// MultiTransferNEP17 is similar to TransferNEP17, buf allows to have multiple recipients. // MultiTransferNEP17 is similar to TransferNEP17, buf allows to have multiple recipients.
func (c *Client) MultiTransferNEP17(acc *wallet.Account, gas int64, recipients []TransferTarget) (util.Uint256, error) { func (c *Client) MultiTransferNEP17(acc *wallet.Account, gas int64, recipients []TransferTarget, cosigners []SignerAccount) (util.Uint256, error) {
if !c.initDone { if !c.initDone {
return util.Uint256{}, errNetworkNotInitialized return util.Uint256{}, errNetworkNotInitialized
} }
tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients) tx, err := c.CreateNEP17MultiTransferTx(acc, gas, recipients, cosigners)
if err != nil { if err != nil {
return util.Uint256{}, err return util.Uint256{}, err
} }
if err := acc.SignTx(c.GetNetwork(), tx); err != nil { return c.SignAndPushTx(tx, acc, cosigners)
return util.Uint256{}, fmt.Errorf("can't sign tx: %w", err)
}
return c.SendRawTransaction(tx)
} }

View file

@ -535,13 +535,22 @@ func (c *Client) SubmitRawOracleResponse(ps request.RawParams) error {
// invocation transaction and an error. If one of the cosigners accounts is // invocation transaction and an error. If one of the cosigners accounts is
// neither contract-based nor unlocked an error is returned. // neither contract-based nor unlocked an error is returned.
func (c *Client) SignAndPushInvocationTx(script []byte, acc *wallet.Account, sysfee int64, netfee fixedn.Fixed8, cosigners []SignerAccount) (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
tx, err := c.CreateTxFromScript(script, acc, sysfee, int64(netfee), cosigners) tx, err := c.CreateTxFromScript(script, acc, sysfee, int64(netfee), cosigners)
if err != nil { if err != nil {
return txHash, fmt.Errorf("failed to create tx: %w", err) return util.Uint256{}, fmt.Errorf("failed to create tx: %w", err)
} }
return c.SignAndPushTx(tx, acc, cosigners)
}
// SignAndPushTx signs given transaction using given wif and cosigners and pushes
// it to the chain. It returns a hash of the transaction and an error. If one of
// the cosigners accounts is neither contract-based nor unlocked an error is
// returned.
func (c *Client) SignAndPushTx(tx *transaction.Transaction, acc *wallet.Account, cosigners []SignerAccount) (util.Uint256, error) {
var (
txHash util.Uint256
err error
)
if err = acc.SignTx(c.GetNetwork(), tx); err != nil { if err = acc.SignTx(c.GetNetwork(), tx); err != nil {
return txHash, fmt.Errorf("failed to sign tx: %w", err) return txHash, fmt.Errorf("failed to sign tx: %w", err)
} }

View file

@ -707,7 +707,7 @@ func TestCreateNEP17TransferTx(t *testing.T) {
gasContractHash, err := c.GetNativeContractHash(nativenames.Gas) gasContractHash, err := c.GetNativeContractHash(nativenames.Gas)
require.NoError(t, err) require.NoError(t, err)
tx, err := c.CreateNEP17TransferTx(acc, util.Uint160{}, gasContractHash, 1000, 0, nil) tx, err := c.CreateNEP17TransferTx(acc, util.Uint160{}, gasContractHash, 1000, 0, nil, nil)
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, acc.SignTx(testchain.Network(), tx)) require.NoError(t, acc.SignTx(testchain.Network(), tx))
require.NoError(t, chain.VerifyTx(tx)) require.NoError(t, chain.VerifyTx(tx))
@ -822,7 +822,7 @@ func TestClient_NEP11(t *testing.T) {
require.EqualValues(t, expected, p) require.EqualValues(t, expected, p)
}) })
t.Run("Transfer", func(t *testing.T) { t.Run("Transfer", func(t *testing.T) {
_, err := c.TransferNEP11(wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)), testchain.PrivateKeyByID(1).GetScriptHash(), h, "neo.com", 0) _, err := c.TransferNEP11(wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)), testchain.PrivateKeyByID(1).GetScriptHash(), h, "neo.com", 0, nil)
require.NoError(t, err) require.NoError(t, err)
}) })
} }