rpcclient: provide nep11 package for NEP-11 tokens

Unfortunately Go doesn't allow to easily reuse readers in full packages, still
we can have this wrapper with a little overhead (the alternative is to move
specific methods into types of their own, but I'm not sure how it's going to
be accepted user-side).
This commit is contained in:
Roman Khimov 2022-08-19 10:37:22 +03:00
parent d0702c2cf9
commit 194933a5cc
10 changed files with 1144 additions and 45 deletions

View file

@ -5,16 +5,20 @@ import (
"errors" "errors"
"fmt" "fmt"
"math/big" "math/big"
"strconv"
"github.com/nspcc-dev/neo-go/cli/cmdargs" "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"
"github.com/nspcc-dev/neo-go/cli/paramcontext" "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/core/transaction"
"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/rpcclient" "github.com/nspcc-dev/neo-go/pkg/rpcclient"
"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/smartcontract/manifest"
"github.com/nspcc-dev/neo-go/pkg/util" "github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem" "github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
@ -23,6 +27,7 @@ import (
) )
func newNEP11Commands() []cli.Command { func newNEP11Commands() []cli.Command {
maxIters := strconv.Itoa(config.DefaultMaxIteratorResultItems)
tokenAddressFlag := flags.AddressFlag{ tokenAddressFlag := flags.AddressFlag{
Name: "token", Name: "token",
Usage: "Token contract address or hash in LE", Usage: "Token contract address or hash in LE",
@ -119,7 +124,7 @@ func newNEP11Commands() []cli.Command {
}, },
{ {
Name: "ownerOfD", Name: "ownerOfD",
Usage: "print set of owners of divisible NEP-11 token with the specified ID (the default MaxIteratorResultItems will be printed at max)", Usage: "print set of owners of divisible NEP-11 token with the specified ID (" + maxIters + " will be printed at max)",
UsageText: "ownerOfD --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>", UsageText: "ownerOfD --rpc-endpoint <node> --timeout <time> --token <hash> --id <token-id>",
Action: printNEP11DOwner, Action: printNEP11DOwner,
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
@ -129,7 +134,7 @@ func newNEP11Commands() []cli.Command {
}, },
{ {
Name: "tokensOf", Name: "tokensOf",
Usage: "print list of tokens IDs for the specified NFT owner (the default MaxIteratorResultItems will be printed at max)", Usage: "print list of tokens IDs for the specified NFT owner (" + maxIters + " will be printed at max)",
UsageText: "tokensOf --rpc-endpoint <node> --timeout <time> --token <hash> --address <addr>", UsageText: "tokensOf --rpc-endpoint <node> --timeout <time> --token <hash> --address <addr>",
Action: printNEP11TokensOf, Action: printNEP11TokensOf,
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
@ -139,7 +144,7 @@ func newNEP11Commands() []cli.Command {
}, },
{ {
Name: "tokens", Name: "tokens",
Usage: "print list of tokens IDs minted by the specified NFT (optional method; the default MaxIteratorResultItems will be printed at max)", Usage: "print list of tokens IDs minted by the specified NFT (optional method; " + maxIters + " will be printed at max)",
UsageText: "tokens --rpc-endpoint <node> --timeout <time> --token <hash>", UsageText: "tokens --rpc-endpoint <node> --timeout <time> --token <hash>",
Action: printNEP11Tokens, Action: printNEP11Tokens,
Flags: append([]cli.Flag{ Flags: append([]cli.Flag{
@ -211,6 +216,8 @@ func getNEP11Balance(ctx *cli.Context) error {
return cli.NewExitError(err.Error(), 1) return cli.NewExitError(err.Error(), 1)
} }
} }
// Always initialize divisible token to be able to use both balanceOf methods.
n11 := nep11.NewDivisibleReader(invoker.New(c, nil), token.Hash)
tokenID := ctx.String("id") tokenID := ctx.String("id")
tokenIDBytes, err := hex.DecodeString(tokenID) tokenIDBytes, err := hex.DecodeString(tokenID)
@ -228,16 +235,16 @@ func getNEP11Balance(ctx *cli.Context) error {
} }
fmt.Fprintf(ctx.App.Writer, "Account %s\n", acc.Address) fmt.Fprintf(ctx.App.Writer, "Account %s\n", acc.Address)
var amount int64 var amount *big.Int
if len(tokenIDBytes) == 0 { if len(tokenIDBytes) == 0 {
amount, err = c.NEP11BalanceOf(token.Hash, addrHash) amount, err = n11.BalanceOf(addrHash)
} else { } else {
amount, err = c.NEP11DBalanceOf(token.Hash, addrHash, tokenIDBytes) amount, err = n11.BalanceOfD(addrHash, tokenIDBytes)
} }
if err != nil { if err != nil {
continue continue
} }
amountStr := fixedn.ToString(big.NewInt(amount), int(token.Decimals)) amountStr := fixedn.ToString(amount, int(token.Decimals))
format := "%s: %s (%s)\n" format := "%s: %s (%s)\n"
formatArgs := []interface{}{token.Symbol, token.Name, token.Hash.StringLE()} formatArgs := []interface{}{token.Symbol, token.Name, token.Hash.StringLE()}
@ -270,9 +277,9 @@ func signAndSendNEP11Transfer(ctx *cli.Context, c *rpcclient.Client, acc *wallet
if err != nil { if err != nil {
return cli.NewExitError(fmt.Errorf("bad account address: %w", err), 1) 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) tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, from, to, amount, tokenID, data) //nolint:staticcheck // SA1019: c.CreateNEP11TransferTx is deprecated
} else { } else {
tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, to, tokenID, data) tx, err = c.CreateNEP11TransferTx(acc, token, int64(gas), cosigners, to, tokenID, data) //nolint:staticcheck // SA1019: c.CreateNEP11TransferTx is deprecated
} }
if err != nil { if err != nil {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
@ -346,7 +353,8 @@ func printNEP11Owner(ctx *cli.Context, divisible bool) error {
} }
if divisible { if divisible {
result, err := c.NEP11DUnpackedOwnerOf(tokenHash.Uint160(), tokenIDBytes) n11 := nep11.NewDivisibleReader(invoker.New(c, nil), tokenHash.Uint160())
result, err := n11.OwnerOfExpanded(tokenIDBytes, config.DefaultMaxIteratorResultItems)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 divisible `ownerOf` method: %s", err.Error()), 1) return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 divisible `ownerOf` method: %s", err.Error()), 1)
} }
@ -354,7 +362,8 @@ func printNEP11Owner(ctx *cli.Context, divisible bool) error {
fmt.Fprintln(ctx.App.Writer, address.Uint160ToString(h)) fmt.Fprintln(ctx.App.Writer, address.Uint160ToString(h))
} }
} else { } else {
result, err := c.NEP11NDOwnerOf(tokenHash.Uint160(), tokenIDBytes) n11 := nep11.NewNonDivisibleReader(invoker.New(c, nil), tokenHash.Uint160())
result, err := n11.OwnerOf(tokenIDBytes)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 non-divisible `ownerOf` method: %s", err.Error()), 1) return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 non-divisible `ownerOf` method: %s", err.Error()), 1)
} }
@ -384,7 +393,8 @@ func printNEP11TokensOf(ctx *cli.Context) error {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
result, err := c.NEP11UnpackedTokensOf(tokenHash.Uint160(), acc.Uint160()) n11 := nep11.NewBaseReader(invoker.New(c, nil), tokenHash.Uint160())
result, err := n11.TokensOfExpanded(acc.Uint160(), config.DefaultMaxIteratorResultItems)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `tokensOf` method: %s", err.Error()), 1) return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `tokensOf` method: %s", err.Error()), 1)
} }
@ -413,7 +423,8 @@ func printNEP11Tokens(ctx *cli.Context) error {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
result, err := c.NEP11UnpackedTokens(tokenHash.Uint160()) n11 := nep11.NewBaseReader(invoker.New(c, nil), tokenHash.Uint160())
result, err := n11.TokensExpanded(config.DefaultMaxIteratorResultItems)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call optional NEP-11 `tokens` method: %s", err.Error()), 1) return cli.NewExitError(fmt.Sprintf("failed to call optional NEP-11 `tokens` method: %s", err.Error()), 1)
} }
@ -451,7 +462,8 @@ func printNEP11Properties(ctx *cli.Context) error {
return cli.NewExitError(err, 1) return cli.NewExitError(err, 1)
} }
result, err := c.NEP11Properties(tokenHash.Uint160(), tokenIDBytes) n11 := nep11.NewBaseReader(invoker.New(c, nil), tokenHash.Uint160())
result, err := n11.Properties(tokenIDBytes)
if err != nil { if err != nil {
return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `properties` method: %s", err.Error()), 1) return cli.NewExitError(fmt.Sprintf("failed to call NEP-11 `properties` method: %s", err.Error()), 1)
} }

View file

@ -17,21 +17,33 @@ import (
) )
// NEP11Decimals invokes `decimals` NEP-11 method on the specified contract. // NEP11Decimals invokes `decimals` NEP-11 method on the specified contract.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11Decimals(tokenHash util.Uint160) (int64, error) { func (c *Client) NEP11Decimals(tokenHash util.Uint160) (int64, error) {
return c.nepDecimals(tokenHash) return c.nepDecimals(tokenHash)
} }
// NEP11Symbol invokes `symbol` NEP-11 method on the specified contract. // NEP11Symbol invokes `symbol` NEP-11 method on the specified contract.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11Symbol(tokenHash util.Uint160) (string, error) { func (c *Client) NEP11Symbol(tokenHash util.Uint160) (string, error) {
return c.nepSymbol(tokenHash) return c.nepSymbol(tokenHash)
} }
// NEP11TotalSupply invokes `totalSupply` NEP-11 method on the specified contract. // NEP11TotalSupply invokes `totalSupply` NEP-11 method on the specified contract.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11TotalSupply(tokenHash util.Uint160) (int64, error) { func (c *Client) NEP11TotalSupply(tokenHash util.Uint160) (int64, error) {
return c.nepTotalSupply(tokenHash) return c.nepTotalSupply(tokenHash)
} }
// NEP11BalanceOf invokes `balanceOf` NEP-11 method on the specified contract. // NEP11BalanceOf invokes `balanceOf` NEP-11 method on the specified contract.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11BalanceOf(tokenHash, owner util.Uint160) (int64, error) { func (c *Client) NEP11BalanceOf(tokenHash, owner util.Uint160) (int64, error) {
return c.nepBalanceOf(tokenHash, owner, nil) return c.nepBalanceOf(tokenHash, owner, nil)
} }
@ -44,6 +56,9 @@ func (c *Client) NEP11TokenInfo(tokenHash util.Uint160) (*wallet.Token, error) {
// TransferNEP11 creates an invocation transaction that invokes 'transfer' method // TransferNEP11 creates an invocation transaction that invokes 'transfer' method
// on the given token to move the whole NEP-11 token with the specified token ID to // on the given token to move the whole NEP-11 token with the specified token ID to
// the given account and sends it to the network returning just a hash of it. // the given account and sends it to the network returning just a hash of it.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
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, data interface{}, gas int64, cosigners []SignerAccount) (util.Uint256, error) { tokenHash util.Uint160, tokenID string, data interface{}, gas int64, cosigners []SignerAccount) (util.Uint256, error) {
tx, err := c.CreateNEP11TransferTx(acc, tokenHash, gas, cosigners, to, tokenID, data) tx, err := c.CreateNEP11TransferTx(acc, tokenHash, gas, cosigners, to, tokenID, data)
@ -61,6 +76,9 @@ func (c *Client) TransferNEP11(acc *wallet.Account, to util.Uint160,
// helper for TransferNEP11 and TransferNEP11D. // helper for TransferNEP11 and TransferNEP11D.
// `args` for TransferNEP11: to util.Uint160, tokenID string, data interface{}; // `args` for TransferNEP11: to util.Uint160, tokenID string, data interface{};
// `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string, data interface{}. // `args` for TransferNEP11D: from, to util.Uint160, amount int64, tokenID string, data interface{}.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160, func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint160,
gas int64, cosigners []SignerAccount, args ...interface{}) (*transaction.Transaction, error) { gas int64, cosigners []SignerAccount, args ...interface{}) (*transaction.Transaction, error) {
script, err := smartcontract.CreateCallWithAssertScript(tokenHash, "transfer", args...) script, err := smartcontract.CreateCallWithAssertScript(tokenHash, "transfer", args...)
@ -85,6 +103,9 @@ func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint1
// Iterator itself, the third one is an error. Use TraverseIterator method to // Iterator itself, the third one is an error. Use TraverseIterator method to
// traverse iterator values or TerminateSession to terminate opened iterator // traverse iterator values or TerminateSession to terminate opened iterator
// session. See TraverseIterator and TerminateSession documentation for more details. // session. See TraverseIterator and TerminateSession documentation for more details.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) (uuid.UUID, result.Iterator, error) { func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) (uuid.UUID, result.Iterator, error) {
return unwrap.SessionIterator(c.reader.Call(tokenHash, "tokensOf", owner)) return unwrap.SessionIterator(c.reader.Call(tokenHash, "tokensOf", owner))
} }
@ -93,6 +114,9 @@ func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) (uuid
// (config.DefaultMaxIteratorResultItems at max). It differs from NEP11TokensOf in that no iterator session // (config.DefaultMaxIteratorResultItems at max). It differs from NEP11TokensOf in that no iterator session
// is used to retrieve values from iterator. Instead, unpacking VM script is created and invoked via // is used to retrieve values from iterator. Instead, unpacking VM script is created and invoked via
// `invokescript` JSON-RPC call. // `invokescript` JSON-RPC call.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11UnpackedTokensOf(tokenHash util.Uint160, owner util.Uint160) ([][]byte, error) { func (c *Client) NEP11UnpackedTokensOf(tokenHash util.Uint160, owner util.Uint160) ([][]byte, error) {
return unwrap.ArrayOfBytes(c.reader.CallAndExpandIterator(tokenHash, "tokensOf", config.DefaultMaxIteratorResultItems, owner)) return unwrap.ArrayOfBytes(c.reader.CallAndExpandIterator(tokenHash, "tokensOf", config.DefaultMaxIteratorResultItems, owner))
} }
@ -101,6 +125,9 @@ func (c *Client) NEP11UnpackedTokensOf(tokenHash util.Uint160, owner util.Uint16
// NEP11NDOwnerOf invokes `ownerOf` non-divisible NEP-11 method with the // NEP11NDOwnerOf invokes `ownerOf` non-divisible NEP-11 method with the
// specified token ID on the specified contract. // specified token ID on the specified contract.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11NDOwnerOf(tokenHash util.Uint160, tokenID []byte) (util.Uint160, error) { func (c *Client) NEP11NDOwnerOf(tokenHash util.Uint160, tokenID []byte) (util.Uint160, error) {
return unwrap.Uint160(c.reader.Call(tokenHash, "ownerOf", tokenID)) return unwrap.Uint160(c.reader.Call(tokenHash, "ownerOf", tokenID))
} }
@ -113,6 +140,9 @@ func (c *Client) NEP11NDOwnerOf(tokenHash util.Uint160, tokenID []byte) (util.Ui
// method on the given token to move the specified amount of divisible NEP-11 assets // method on the given token to move the specified amount of divisible NEP-11 assets
// (in FixedN format using contract's number of decimals) to the given account and // (in FixedN format using contract's number of decimals) to the 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.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
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 []byte, data interface{}, gas int64, cosigners []SignerAccount) (util.Uint256, error) { tokenHash util.Uint160, amount int64, tokenID []byte, data interface{}, gas int64, cosigners []SignerAccount) (util.Uint256, error) {
from, err := address.StringToUint160(acc.Address) from, err := address.StringToUint160(acc.Address)
@ -129,6 +159,9 @@ func (c *Client) TransferNEP11D(acc *wallet.Account, to util.Uint160,
// NEP11DBalanceOf invokes `balanceOf` divisible NEP-11 method on a // NEP11DBalanceOf invokes `balanceOf` divisible NEP-11 method on a
// specified contract. // specified contract.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11DBalanceOf(tokenHash, owner util.Uint160, tokenID []byte) (int64, error) { func (c *Client) NEP11DBalanceOf(tokenHash, owner util.Uint160, tokenID []byte) (int64, error) {
return c.nepBalanceOf(tokenHash, owner, tokenID) return c.nepBalanceOf(tokenHash, owner, tokenID)
} }
@ -137,6 +170,9 @@ func (c *Client) NEP11DBalanceOf(tokenHash, owner util.Uint160, tokenID []byte)
// is the session ID, the second one is Iterator itself, the third one is an error. Use TraverseIterator // is the session ID, the second one is Iterator itself, the third one is an error. Use TraverseIterator
// method to traverse iterator values or TerminateSession to terminate opened iterator session. See // method to traverse iterator values or TerminateSession to terminate opened iterator session. See
// TraverseIterator and TerminateSession documentation for more details. // TraverseIterator and TerminateSession documentation for more details.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID []byte) (uuid.UUID, result.Iterator, error) { func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID []byte) (uuid.UUID, result.Iterator, error) {
return unwrap.SessionIterator(c.reader.Call(tokenHash, "ownerOf", tokenID)) return unwrap.SessionIterator(c.reader.Call(tokenHash, "ownerOf", tokenID))
} }
@ -145,6 +181,9 @@ func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID []byte) (uuid.UUI
// (config.DefaultMaxIteratorResultItems at max). It differs from NEP11DOwnerOf in that no // (config.DefaultMaxIteratorResultItems at max). It differs from NEP11DOwnerOf in that no
// iterator session is used to retrieve values from iterator. Instead, unpacking VM // iterator session is used to retrieve values from iterator. Instead, unpacking VM
// script is created and invoked via `invokescript` JSON-RPC call. // script is created and invoked via `invokescript` JSON-RPC call.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11DUnpackedOwnerOf(tokenHash util.Uint160, tokenID []byte) ([]util.Uint160, error) { func (c *Client) NEP11DUnpackedOwnerOf(tokenHash util.Uint160, tokenID []byte) ([]util.Uint160, error) {
arr, err := unwrap.ArrayOfBytes(c.reader.CallAndExpandIterator(tokenHash, "ownerOf", config.DefaultMaxIteratorResultItems, tokenID)) arr, err := unwrap.ArrayOfBytes(c.reader.CallAndExpandIterator(tokenHash, "ownerOf", config.DefaultMaxIteratorResultItems, tokenID))
if err != nil { if err != nil {
@ -166,6 +205,9 @@ func (c *Client) NEP11DUnpackedOwnerOf(tokenHash util.Uint160, tokenID []byte) (
// NEP11Properties invokes `properties` optional NEP-11 method on the // NEP11Properties invokes `properties` optional NEP-11 method on the
// specified contract. // specified contract.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID []byte) (*stackitem.Map, error) { func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID []byte) (*stackitem.Map, error) {
return unwrap.Map(c.reader.Call(tokenHash, "properties", tokenID)) return unwrap.Map(c.reader.Call(tokenHash, "properties", tokenID))
} }
@ -175,6 +217,9 @@ func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID []byte) (*stack
// error. Use TraverseIterator method to traverse iterator values or // error. Use TraverseIterator method to traverse iterator values or
// TerminateSession to terminate opened iterator session. See TraverseIterator and // TerminateSession to terminate opened iterator session. See TraverseIterator and
// TerminateSession documentation for more details. // TerminateSession documentation for more details.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11Tokens(tokenHash util.Uint160) (uuid.UUID, result.Iterator, error) { func (c *Client) NEP11Tokens(tokenHash util.Uint160) (uuid.UUID, result.Iterator, error) {
return unwrap.SessionIterator(c.reader.Call(tokenHash, "tokens")) return unwrap.SessionIterator(c.reader.Call(tokenHash, "tokens"))
} }
@ -183,6 +228,9 @@ func (c *Client) NEP11Tokens(tokenHash util.Uint160) (uuid.UUID, result.Iterator
// (config.DefaultMaxIteratorResultItems at max). It differs from NEP11Tokens in that no // (config.DefaultMaxIteratorResultItems at max). It differs from NEP11Tokens in that no
// iterator session is used to retrieve values from iterator. Instead, unpacking // iterator session is used to retrieve values from iterator. Instead, unpacking
// VM script is created and invoked via `invokescript` JSON-RPC call. // VM script is created and invoked via `invokescript` JSON-RPC call.
//
// Deprecated: please use nep11 package, this method will be removed in future
// versions.
func (c *Client) NEP11UnpackedTokens(tokenHash util.Uint160) ([][]byte, error) { func (c *Client) NEP11UnpackedTokens(tokenHash util.Uint160) ([][]byte, error) {
return unwrap.ArrayOfBytes(c.reader.CallAndExpandIterator(tokenHash, "tokens", config.DefaultMaxIteratorResultItems)) return unwrap.ArrayOfBytes(c.reader.CallAndExpandIterator(tokenHash, "tokens", config.DefaultMaxIteratorResultItems))
} }

247
pkg/rpcclient/nep11/base.go Normal file
View file

@ -0,0 +1,247 @@
/*
Package nep11 contains RPC wrappers for NEP-11 contracts.
The set of types provided is split between common NEP-11 methods (BaseReader and
Base types) and divisible (DivisibleReader and Divisible) and non-divisible
(NonDivisibleReader and NonDivisible). If you don't know the type of NEP-11
contract you're going to use you can use Base and BaseReader types for many
purposes, otherwise more specific types are recommended.
*/
package nep11
import (
"fmt"
"math/big"
"unicode/utf8"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neptoken"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
)
// Invoker is used by reader types to call various methods.
type Invoker interface {
neptoken.Invoker
CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error)
TerminateSession(sessionID uuid.UUID) error
TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error)
}
// Actor is used by complete NEP-11 types to create and send transactions.
type Actor interface {
Invoker
MakeRun(script []byte) (*transaction.Transaction, error)
MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error)
SendRun(script []byte) (util.Uint256, uint32, error)
}
// BaseReader is a reader interface for common divisible and non-divisible NEP-11
// methods. It allows to invoke safe methods.
type BaseReader struct {
neptoken.Base
invoker Invoker
hash util.Uint160
}
// Base is a state-changing interface for common divisible and non-divisible NEP-11
// methods.
type Base struct {
BaseReader
actor Actor
}
// TransferEvent represents a Transfer event as defined in the NEP-11 standard.
type TransferEvent struct {
From util.Uint160
To util.Uint160
Amount *big.Int
ID []byte
}
// TokenIterator is used for iterating over TokensOf results.
type TokenIterator struct {
client Invoker
session uuid.UUID
iterator result.Iterator
}
// NewBaseReader creates an instance of BaseReader for a contract with the given
// hash using the given invoker.
func NewBaseReader(invoker Invoker, hash util.Uint160) *BaseReader {
return &BaseReader{*neptoken.New(invoker, hash), invoker, hash}
}
// NewBase creates an instance of Base for contract with the given
// hash using the given actor.
func NewBase(actor Actor, hash util.Uint160) *Base {
return &Base{*NewBaseReader(actor, hash), actor}
}
// BalanceOf returns the number of NFTs owned by the given account. For divisible
// NFTs that's the sum of all parts of tokens.
func (t *BaseReader) BalanceOf(account util.Uint160) (*big.Int, error) {
return unwrap.BigInt(t.invoker.Call(t.hash, "balanceOf", account))
}
// Properties returns a set of token's properties such as name or URL. The map
// is returned as is from this method (stack item) for maximum flexibility,
// contracts can return a lot of specific data there. Most of the time though
// they return well-defined properties outlined in NEP-11 and
// UnwrapKnownProperties can be used to get them in more convenient way. It's an
// optional method per NEP-11 specification, so it can fail.
func (t *BaseReader) Properties(token []byte) (*stackitem.Map, error) {
return unwrap.Map(t.invoker.Call(t.hash, "properties", token))
}
// Tokens returns an iterator that allows to retrieve all tokens minted by the
// contract. It depends on the server to provide proper session-based
// iterator, but can also work with expanded one. The method itself is optional
// per NEP-11 specification, so it can fail.
func (t *BaseReader) Tokens() (*TokenIterator, error) {
sess, iter, err := unwrap.SessionIterator(t.invoker.Call(t.hash, "tokens"))
if err != nil {
return nil, err
}
return &TokenIterator{t.invoker, sess, iter}, nil
}
// TokensExpanded uses the same NEP-11 method as Tokens, but can be useful if
// the server used doesn't support sessions and doesn't expand iterators. It
// creates a script that will get num of result items from the iterator right in
// the VM and return them to you. It's only limited by VM stack and GAS available
// for RPC invocations.
func (t *BaseReader) TokensExpanded(num int) ([][]byte, error) {
return unwrap.ArrayOfBytes(t.invoker.CallAndExpandIterator(t.hash, "tokens", num))
}
// TokensOf returns an iterator that allows to walk through all tokens owned by
// the given account. It depends on the server to provide proper session-based
// iterator, but can also work with expanded one.
func (t *BaseReader) TokensOf(account util.Uint160) (*TokenIterator, error) {
sess, iter, err := unwrap.SessionIterator(t.invoker.Call(t.hash, "tokensOf", account))
if err != nil {
return nil, err
}
return &TokenIterator{t.invoker, sess, iter}, nil
}
// TokensOfExpanded uses the same NEP-11 method as TokensOf, but can be useful if
// the server used doesn't support sessions and doesn't expand iterators. It
// creates a script that will get num of result items from the iterator right in
// the VM and return them to you. It's only limited by VM stack and GAS available
// for RPC invocations.
func (t *BaseReader) TokensOfExpanded(account util.Uint160, num int) ([][]byte, error) {
return unwrap.ArrayOfBytes(t.invoker.CallAndExpandIterator(t.hash, "tokensOf", num, account))
}
// Transfer creates and sends a transaction that performs a `transfer` method
// call using the given parameters and checks for this call result, failing the
// transaction if it's not true. It works for divisible NFTs only when there is
// one owner for the particular token. The returned values are transaction hash,
// its ValidUntilBlock value and an error if any.
func (t *Base) Transfer(to util.Uint160, id []byte, data interface{}) (util.Uint256, uint32, error) {
script, err := t.transferScript(to, id, data)
if err != nil {
return util.Uint256{}, 0, err
}
return t.actor.SendRun(script)
}
// TransferTransaction creates a transaction that performs a `transfer` method
// call using the given parameters and checks for this call result, failing the
// transaction if it's not true. It works for divisible NFTs only when there is
// one owner for the particular token. This transaction is signed, but not sent
// to the network, instead it's returned to the caller.
func (t *Base) TransferTransaction(to util.Uint160, id []byte, data interface{}) (*transaction.Transaction, error) {
script, err := t.transferScript(to, id, data)
if err != nil {
return nil, err
}
return t.actor.MakeRun(script)
}
// TransferUnsigned creates a transaction that performs a `transfer` method
// call using the given parameters and checks for this call result, failing the
// transaction if it's not true. It works for divisible NFTs only when there is
// one owner for the particular token. This transaction is not signed and just
// returned to the caller.
func (t *Base) TransferUnsigned(to util.Uint160, id []byte, data interface{}) (*transaction.Transaction, error) {
script, err := t.transferScript(to, id, data)
if err != nil {
return nil, err
}
return t.actor.MakeUnsignedRun(script, nil)
}
func (t *Base) transferScript(params ...interface{}) ([]byte, error) {
return smartcontract.CreateCallWithAssertScript(t.hash, "transfer", params...)
}
// Next returns the next set of elements from the iterator (up to num of them).
// It can return less than num elements in case iterator doesn't have that many
// or zero elements if the iterator has no more elements or the session is
// expired.
func (v *TokenIterator) Next(num int) ([][]byte, error) {
items, err := v.client.TraverseIterator(v.session, &v.iterator, num)
if err != nil {
return nil, err
}
res := make([][]byte, len(items))
for i := range items {
b, err := items[i].TryBytes()
if err != nil {
return nil, fmt.Errorf("element %d is not a byte string: %w", i, err)
}
res[i] = b
}
return res, nil
}
// Terminate closes the iterator session used by TokenIterator (if it's
// session-based).
func (v *TokenIterator) Terminate() error {
if v.iterator.ID == nil {
return nil
}
return v.client.TerminateSession(v.session)
}
// UnwrapKnownProperties can be used as a proxy function to extract well-known
// NEP-11 properties (name/description/image/tokenURI) defined in the standard.
// These properties are checked to be valid UTF-8 strings, but can contain
// control codes or special characters.
func UnwrapKnownProperties(m *stackitem.Map, err error) (map[string]string, error) {
if err != nil {
return nil, err
}
elems := m.Value().([]stackitem.MapElement)
res := make(map[string]string)
for _, e := range elems {
k, err := e.Key.TryBytes()
if err != nil { // Shouldn't ever happen in the valid Map, but.
continue
}
ks := string(k)
if !result.KnownNEP11Properties[ks] { // Some additional elements are OK.
continue
}
v, err := e.Value.TryBytes()
if err != nil { // But known ones MUST be proper strings.
return nil, fmt.Errorf("invalid %s property: %w", ks, err)
}
if !utf8.Valid(v) {
return nil, fmt.Errorf("invalid %s property: not a UTF-8 string", ks)
}
res[ks] = string(v)
}
return res, nil
}

View file

@ -0,0 +1,305 @@
package nep11
import (
"errors"
"math/big"
"testing"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
type testAct struct {
err error
res *result.Invoke
tx *transaction.Transaction
txh util.Uint256
vub uint32
}
func (t *testAct) Call(contract util.Uint160, operation string, params ...interface{}) (*result.Invoke, error) {
return t.res, t.err
}
func (t *testAct) MakeRun(script []byte) (*transaction.Transaction, error) {
return t.tx, t.err
}
func (t *testAct) MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error) {
return t.tx, t.err
}
func (t *testAct) SendRun(script []byte) (util.Uint256, uint32, error) {
return t.txh, t.vub, t.err
}
func (t *testAct) CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...interface{}) (*result.Invoke, error) {
return t.res, t.err
}
func (t *testAct) TerminateSession(sessionID uuid.UUID) error {
return t.err
}
func (t *testAct) TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error) {
return t.res.Stack, t.err
}
func TestReaderBalanceOf(t *testing.T) {
ta := new(testAct)
tr := NewBaseReader(ta, util.Uint160{1, 2, 3})
ta.err = errors.New("")
_, err := tr.BalanceOf(util.Uint160{3, 2, 1})
require.Error(t, err)
ta.err = nil
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(100500),
},
}
bal, err := tr.BalanceOf(util.Uint160{3, 2, 1})
require.NoError(t, err)
require.Equal(t, big.NewInt(100500), bal)
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{}),
},
}
_, err = tr.BalanceOf(util.Uint160{3, 2, 1})
require.Error(t, err)
}
func TestReaderProperties(t *testing.T) {
ta := new(testAct)
tr := NewBaseReader(ta, util.Uint160{1, 2, 3})
ta.err = errors.New("")
_, err := tr.Properties([]byte{3, 2, 1})
require.Error(t, err)
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{}),
},
}
_, err = tr.Properties([]byte{3, 2, 1})
require.Error(t, err)
ta.err = nil
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.NewMap(),
},
}
m, err := tr.Properties([]byte{3, 2, 1})
require.NoError(t, err)
require.Equal(t, 0, m.Len())
}
func TestReaderTokensOfExpanded(t *testing.T) {
ta := new(testAct)
tr := NewBaseReader(ta, util.Uint160{1, 2, 3})
for name, fun := range map[string]func(int) ([][]byte, error){
"Tokens": tr.TokensExpanded,
"TokensOf": func(n int) ([][]byte, error) {
return tr.TokensOfExpanded(util.Uint160{1, 2, 3}, n)
},
} {
t.Run(name, func(t *testing.T) {
ta.err = errors.New("")
_, err := fun(1)
require.Error(t, err)
ta.err = nil
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(100500),
},
}
_, err = fun(1)
require.Error(t, err)
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{stackitem.Make("one")}),
},
}
toks, err := fun(1)
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("one")}, toks)
})
}
}
func TestReaderTokensOf(t *testing.T) {
ta := new(testAct)
tr := NewBaseReader(ta, util.Uint160{1, 2, 3})
for name, fun := range map[string]func() (*TokenIterator, error){
"Tokens": tr.Tokens,
"TokensOf": func() (*TokenIterator, error) {
return tr.TokensOf(util.Uint160{1, 2, 3})
},
} {
t.Run(name, func(t *testing.T) {
ta.err = errors.New("")
_, err := fun()
require.Error(t, err)
iid := uuid.New()
ta.err = nil
ta.res = &result.Invoke{
Session: uuid.New(),
State: "HALT",
Stack: []stackitem.Item{
stackitem.NewInterop(result.Iterator{
ID: &iid,
}),
},
}
iter, err := fun()
require.NoError(t, err)
ta.res = &result.Invoke{
Stack: []stackitem.Item{
stackitem.Make("one"),
stackitem.Make([]stackitem.Item{}),
},
}
_, err = iter.Next(10)
require.Error(t, err)
ta.res = &result.Invoke{
Stack: []stackitem.Item{
stackitem.Make("one"),
stackitem.Make("two"),
},
}
vals, err := iter.Next(10)
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("one"), []byte("two")}, vals)
ta.err = errors.New("")
_, err = iter.Next(1)
require.Error(t, err)
err = iter.Terminate()
require.Error(t, err)
// Value-based iterator.
ta.err = nil
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.NewInterop(result.Iterator{
Values: []stackitem.Item{
stackitem.Make("one"),
stackitem.Make("two"),
},
}),
},
}
iter, err = fun()
require.NoError(t, err)
ta.err = errors.New("")
err = iter.Terminate()
require.NoError(t, err)
})
}
}
func TestTokenTransfer(t *testing.T) {
ta := new(testAct)
tok := NewBase(ta, util.Uint160{1, 2, 3})
ta.err = errors.New("")
_, _, err := tok.Transfer(util.Uint160{3, 2, 1}, []byte{3, 2, 1}, 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}, []byte{3, 2, 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}, []byte{3, 2, 1}, stackitem.NewMap())
require.Error(t, err)
}
func TestTokenTransferTransaction(t *testing.T) {
ta := new(testAct)
tok := NewBase(ta, util.Uint160{1, 2, 3})
for _, fun := range []func(to util.Uint160, token []byte, data interface{}) (*transaction.Transaction, error){
tok.TransferTransaction,
tok.TransferUnsigned,
} {
ta.err = errors.New("")
_, err := fun(util.Uint160{3, 2, 1}, []byte{3, 2, 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}, []byte{3, 2, 1}, nil)
require.NoError(t, err)
require.Equal(t, ta.tx, tx)
_, err = fun(util.Uint160{3, 2, 1}, []byte{3, 2, 1}, stackitem.NewMap())
require.Error(t, err)
}
}
func TestUnwrapKnownProperties(t *testing.T) {
_, err := UnwrapKnownProperties(stackitem.NewMap(), errors.New(""))
require.Error(t, err)
m, err := UnwrapKnownProperties(stackitem.NewMap(), nil)
require.NoError(t, err)
require.NotNil(t, m)
require.Equal(t, 0, len(m))
m, err = UnwrapKnownProperties(stackitem.NewMapWithValue([]stackitem.MapElement{
{Key: stackitem.Make("some"), Value: stackitem.Make("thing")},
}), nil)
require.NoError(t, err)
require.NotNil(t, m)
require.Equal(t, 0, len(m))
m, err = UnwrapKnownProperties(stackitem.NewMapWithValue([]stackitem.MapElement{
{Key: stackitem.Make([]stackitem.Item{}), Value: stackitem.Make("thing")},
}), nil)
require.NoError(t, err)
require.NotNil(t, m)
require.Equal(t, 0, len(m))
_, err = UnwrapKnownProperties(stackitem.NewMapWithValue([]stackitem.MapElement{
{Key: stackitem.Make("name"), Value: stackitem.Make([]stackitem.Item{})},
}), nil)
require.Error(t, err)
_, err = UnwrapKnownProperties(stackitem.NewMapWithValue([]stackitem.MapElement{
{Key: stackitem.Make("name"), Value: stackitem.Make([]byte{0xff})},
}), nil)
require.Error(t, err)
m, err = UnwrapKnownProperties(stackitem.NewMapWithValue([]stackitem.MapElement{
{Key: stackitem.Make("name"), Value: stackitem.Make("thing")},
{Key: stackitem.Make("description"), Value: stackitem.Make("good NFT")},
}), nil)
require.NoError(t, err)
require.NotNil(t, m)
require.Equal(t, 2, len(m))
require.Equal(t, "thing", m["name"])
require.Equal(t, "good NFT", m["description"])
}

View file

@ -0,0 +1,157 @@
package nep11
import (
"fmt"
"math/big"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/util"
)
// DivisibleReader is a reader interface for divisible NEP-11 contract.
type DivisibleReader struct {
BaseReader
}
// Divisible is a state-changing interface for divisible NEP-11 contract.
type Divisible struct {
Base
}
// OwnerIterator is used for iterating over OwnerOf (for divisible NFTs) results.
type OwnerIterator struct {
client Invoker
session uuid.UUID
iterator result.Iterator
}
// NewDivisibleReader creates an instance of DivisibleReader for a contract
// with the given hash using the given invoker.
func NewDivisibleReader(invoker Invoker, hash util.Uint160) *DivisibleReader {
return &DivisibleReader{*NewBaseReader(invoker, hash)}
}
// NewDivisible creates an instance of Divisible for a contract
// with the given hash using the given actor.
func NewDivisible(actor Actor, hash util.Uint160) *Divisible {
return &Divisible{*NewBase(actor, hash)}
}
// OwnerOf returns returns an iterator that allows to walk through all owners of
// the given token. It depends on the server to provide proper session-based
// iterator, but can also work with expanded one.
func (t *DivisibleReader) OwnerOf(token []byte) (*OwnerIterator, error) {
sess, iter, err := unwrap.SessionIterator(t.invoker.Call(t.hash, "ownerOf", token))
if err != nil {
return nil, err
}
return &OwnerIterator{t.invoker, sess, iter}, nil
}
// OwnerOfExpanded uses the same NEP-11 method as OwnerOf, but can be useful if
// the server used doesn't support sessions and doesn't expand iterators. It
// creates a script that will get num of result items from the iterator right in
// the VM and return them to you. It's only limited by VM stack and GAS available
// for RPC invocations.
func (t *DivisibleReader) OwnerOfExpanded(token []byte, num int) ([]util.Uint160, error) {
return unwrap.ArrayOfUint160(t.invoker.CallAndExpandIterator(t.hash, "ownerOf", num, token))
}
// BalanceOfD is a BalanceOf for divisible NFTs, it returns the amount of token
// owned by a particular account.
func (t *DivisibleReader) BalanceOfD(owner util.Uint160, token []byte) (*big.Int, error) {
return unwrap.BigInt(t.invoker.Call(t.hash, "balanceOf", owner, token))
}
// OwnerOf is the same as (*DivisibleReader).OwnerOf.
func (t *Divisible) OwnerOf(token []byte) (*OwnerIterator, error) {
r := DivisibleReader{t.BaseReader}
return r.OwnerOf(token)
}
// OwnerOfExpanded is the same as (*DivisibleReader).OwnerOfExpanded.
func (t *Divisible) OwnerOfExpanded(token []byte, num int) ([]util.Uint160, error) {
r := DivisibleReader{t.BaseReader}
return r.OwnerOfExpanded(token, num)
}
// BalanceOfD is the same as (*DivisibleReader).BalanceOfD.
func (t *Divisible) BalanceOfD(owner util.Uint160, token []byte) (*big.Int, error) {
r := DivisibleReader{t.BaseReader}
return r.BalanceOfD(owner, token)
}
// TransferD is a divisible version of (*Base).Transfer, allowing to transfer a
// part of NFT. It creates and sends a transaction that performs a `transfer`
// method call using the given parameters and checks for this call result,
// failing the transaction if it's not true. The returned values are transaction
// hash, its ValidUntilBlock value and an error if any.
func (t *Divisible) TransferD(from util.Uint160, to util.Uint160, amount *big.Int, id []byte, data interface{}) (util.Uint256, uint32, error) {
script, err := t.transferScript(from, to, amount, id, data)
if err != nil {
return util.Uint256{}, 0, err
}
return t.actor.SendRun(script)
}
// TransferDTransaction is a divisible version of (*Base).TransferTransaction,
// allowing to transfer a part of NFT. It creates a transaction that performs a
// `transfer` method call using the given parameters and checks for this call
// result, failing the 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 *Divisible) TransferDTransaction(from util.Uint160, to util.Uint160, amount *big.Int, id []byte, data interface{}) (*transaction.Transaction, error) {
script, err := t.transferScript(from, to, amount, id, data)
if err != nil {
return nil, err
}
return t.actor.MakeRun(script)
}
// TransferDUnsigned is a divisible version of (*Base).TransferUnsigned,
// allowing to transfer a part of NFT. It creates a transaction that performs a
// `transfer` method call using the given parameters and checks for this call
// result, failing the transaction if it's not true. This transaction is not
// signed and just returned to the caller.
func (t *Divisible) TransferDUnsigned(from util.Uint160, to util.Uint160, amount *big.Int, id []byte, data interface{}) (*transaction.Transaction, error) {
script, err := t.transferScript(from, to, amount, id, data)
if err != nil {
return nil, err
}
return t.actor.MakeUnsignedRun(script, nil)
}
// Next returns the next set of elements from the iterator (up to num of them).
// It can return less than num elements in case iterator doesn't have that many
// or zero elements if the iterator has no more elements or the session is
// expired.
func (v *OwnerIterator) Next(num int) ([]util.Uint160, error) {
items, err := v.client.TraverseIterator(v.session, &v.iterator, num)
if err != nil {
return nil, err
}
res := make([]util.Uint160, len(items))
for i := range items {
b, err := items[i].TryBytes()
if err != nil {
return nil, fmt.Errorf("element %d is not a byte string: %w", i, err)
}
u, err := util.Uint160DecodeBytesBE(b)
if err != nil {
return nil, fmt.Errorf("element %d is not a uint160: %w", i, err)
}
res[i] = u
}
return res, nil
}
// Terminate closes the iterator session used by OwnerIterator (if it's
// session-based).
func (v *OwnerIterator) Terminate() error {
if v.iterator.ID == nil {
return nil
}
return v.client.TerminateSession(v.session)
}

View file

@ -0,0 +1,218 @@
package nep11
import (
"errors"
"math/big"
"testing"
"github.com/google/uuid"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
func TestDivisibleBalanceOf(t *testing.T) {
ta := new(testAct)
tr := NewDivisibleReader(ta, util.Uint160{1, 2, 3})
tt := NewDivisible(ta, util.Uint160{1, 2, 3})
for name, fun := range map[string]func(util.Uint160, []byte) (*big.Int, error){
"Reader": tr.BalanceOfD,
"Full": tt.BalanceOfD,
} {
t.Run(name, func(t *testing.T) {
ta.err = errors.New("")
_, err := fun(util.Uint160{3, 2, 1}, []byte{1, 2, 3})
require.Error(t, err)
ta.err = nil
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(100500),
},
}
bal, err := fun(util.Uint160{3, 2, 1}, []byte{1, 2, 3})
require.NoError(t, err)
require.Equal(t, big.NewInt(100500), bal)
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{}),
},
}
_, err = fun(util.Uint160{3, 2, 1}, []byte{1, 2, 3})
require.Error(t, err)
})
}
}
func TestDivisibleOwnerOfExpanded(t *testing.T) {
ta := new(testAct)
tr := NewDivisibleReader(ta, util.Uint160{1, 2, 3})
tt := NewDivisible(ta, util.Uint160{1, 2, 3})
for name, fun := range map[string]func([]byte, int) ([]util.Uint160, error){
"Reader": tr.OwnerOfExpanded,
"Full": tt.OwnerOfExpanded,
} {
t.Run(name, func(t *testing.T) {
ta.err = errors.New("")
_, err := fun([]byte{1, 2, 3}, 1)
require.Error(t, err)
ta.err = nil
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(100500),
},
}
_, err = fun([]byte{1, 2, 3}, 1)
require.Error(t, err)
h := util.Uint160{3, 2, 1}
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{stackitem.Make(h.BytesBE())}),
},
}
owls, err := fun([]byte{1, 2, 3}, 1)
require.NoError(t, err)
require.Equal(t, []util.Uint160{h}, owls)
})
}
}
func TestDivisibleOwnerOf(t *testing.T) {
ta := new(testAct)
tr := NewDivisibleReader(ta, util.Uint160{1, 2, 3})
tt := NewDivisible(ta, util.Uint160{1, 2, 3})
for name, fun := range map[string]func([]byte) (*OwnerIterator, error){
"Reader": tr.OwnerOf,
"Full": tt.OwnerOf,
} {
t.Run(name, func(t *testing.T) {
ta.err = errors.New("")
_, err := fun([]byte{1})
require.Error(t, err)
iid := uuid.New()
ta.err = nil
ta.res = &result.Invoke{
Session: uuid.New(),
State: "HALT",
Stack: []stackitem.Item{
stackitem.NewInterop(result.Iterator{
ID: &iid,
}),
},
}
iter, err := fun([]byte{1})
require.NoError(t, err)
ta.res = &result.Invoke{
Stack: []stackitem.Item{
stackitem.Make([]stackitem.Item{}),
},
}
_, err = iter.Next(10)
require.Error(t, err)
ta.res = &result.Invoke{
Stack: []stackitem.Item{
stackitem.Make("not uint160"),
},
}
_, err = iter.Next(10)
require.Error(t, err)
h1 := util.Uint160{1, 2, 3}
h2 := util.Uint160{3, 2, 1}
ta.res = &result.Invoke{
Stack: []stackitem.Item{
stackitem.Make(h1.BytesBE()),
stackitem.Make(h2.BytesBE()),
},
}
vals, err := iter.Next(10)
require.NoError(t, err)
require.Equal(t, []util.Uint160{h1, h2}, vals)
ta.err = errors.New("")
_, err = iter.Next(1)
require.Error(t, err)
err = iter.Terminate()
require.Error(t, err)
// Value-based iterator.
ta.err = nil
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.NewInterop(result.Iterator{
Values: []stackitem.Item{
stackitem.Make(h1.BytesBE()),
stackitem.Make(h2.BytesBE()),
},
}),
},
}
iter, err = fun([]byte{1})
require.NoError(t, err)
ta.err = errors.New("")
err = iter.Terminate()
require.NoError(t, err)
})
}
}
func TestDivisibleTransfer(t *testing.T) {
ta := new(testAct)
tok := NewDivisible(ta, util.Uint160{1, 2, 3})
ta.err = errors.New("")
_, _, err := tok.TransferD(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, nil)
require.Error(t, err)
ta.err = nil
ta.txh = util.Uint256{1, 2, 3}
ta.vub = 42
h, vub, err := tok.TransferD(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, nil)
require.NoError(t, err)
require.Equal(t, ta.txh, h)
require.Equal(t, ta.vub, vub)
_, _, err = tok.TransferD(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, stackitem.NewMap())
require.Error(t, err)
}
func TestDivisibleTransferTransaction(t *testing.T) {
ta := new(testAct)
tok := NewDivisible(ta, util.Uint160{1, 2, 3})
for _, fun := range []func(from util.Uint160, to util.Uint160, amount *big.Int, id []byte, data interface{}) (*transaction.Transaction, error){
tok.TransferDTransaction,
tok.TransferDUnsigned,
} {
ta.err = errors.New("")
_, err := fun(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, nil)
require.Error(t, err)
ta.err = nil
ta.tx = &transaction.Transaction{Nonce: 100500, ValidUntilBlock: 42}
tx, err := fun(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, nil)
require.NoError(t, err)
require.Equal(t, ta.tx, tx)
_, err = fun(util.Uint160{1, 2, 3}, util.Uint160{3, 2, 1}, big.NewInt(10), []byte{3, 2, 1}, stackitem.NewMap())
require.Error(t, err)
}
}

View file

@ -0,0 +1,39 @@
package nep11
import (
"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
"github.com/nspcc-dev/neo-go/pkg/util"
)
// NonDivisibleReader is a reader interface for non-divisble NEP-11 contract.
type NonDivisibleReader struct {
BaseReader
}
// NonDivisible is a state-changing interface for non-divisble NEP-11 contract.
type NonDivisible struct {
Base
}
// NewNonDivisibleReader creates an instance of NonDivisibleReader for a contract
// with the given hash using the given invoker.
func NewNonDivisibleReader(invoker Invoker, hash util.Uint160) *NonDivisibleReader {
return &NonDivisibleReader{*NewBaseReader(invoker, hash)}
}
// NewNonDivisible creates an instance of NonDivisible for a contract
// with the given hash using the given actor.
func NewNonDivisible(actor Actor, hash util.Uint160) *NonDivisible {
return &NonDivisible{*NewBase(actor, hash)}
}
// OwnerOf returns the owner of the given NFT.
func (t *NonDivisibleReader) OwnerOf(token []byte) (util.Uint160, error) {
return unwrap.Uint160(t.invoker.Call(t.hash, "ownerOf", token))
}
// OwnerOf is the same as (*NonDivisibleReader).OwnerOf.
func (t *NonDivisible) OwnerOf(token []byte) (util.Uint160, error) {
r := NonDivisibleReader{t.BaseReader}
return r.OwnerOf(token)
}

View file

@ -0,0 +1,49 @@
package nep11
import (
"errors"
"testing"
"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
"github.com/nspcc-dev/neo-go/pkg/util"
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
"github.com/stretchr/testify/require"
)
func TestNDOwnerOf(t *testing.T) {
ta := new(testAct)
tr := NewNonDivisibleReader(ta, util.Uint160{1, 2, 3})
tt := NewNonDivisible(ta, util.Uint160{1, 2, 3})
for name, fun := range map[string]func([]byte) (util.Uint160, error){
"Reader": tr.OwnerOf,
"Full": tt.OwnerOf,
} {
t.Run(name, func(t *testing.T) {
ta.err = errors.New("")
_, err := fun([]byte{3, 2, 1})
require.Error(t, err)
ta.err = nil
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(100500),
},
}
_, err = fun([]byte{3, 2, 1})
require.Error(t, err)
own := util.Uint160{1, 2, 3}
ta.res = &result.Invoke{
State: "HALT",
Stack: []stackitem.Item{
stackitem.Make(own.BytesBE()),
},
}
owl, err := fun([]byte{3, 2, 1})
require.NoError(t, err)
require.Equal(t, own, owl)
})
}
}

View file

@ -302,7 +302,7 @@ func (c *Client) GetNEP17Balances(address util.Uint160) (*result.NEP17Balances,
} }
// GetNEP11Properties is a wrapper for getnep11properties RPC. We recommend using // GetNEP11Properties is a wrapper for getnep11properties RPC. We recommend using
// NEP11Properties method instead of this to receive proper VM types and work with them. // nep11 package and Properties method there to receive proper VM types and work with them.
// This method is provided mostly for the sake of completeness. For well-known // This method is provided mostly for the sake of completeness. For well-known
// attributes like "description", "image", "name" and "tokenURI" it returns strings, // attributes like "description", "image", "name" and "tokenURI" it returns strings,
// while for all others []byte (which can be nil). // while for all others []byte (which can be nil).

View file

@ -34,6 +34,7 @@ import (
"github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker" "github.com/nspcc-dev/neo-go/pkg/rpcclient/invoker"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/management" "github.com/nspcc-dev/neo-go/pkg/rpcclient/management"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/neo" "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/rpcclient/nep17"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/nns" "github.com/nspcc-dev/neo-go/pkg/rpcclient/nns"
"github.com/nspcc-dev/neo-go/pkg/rpcclient/oracle" "github.com/nspcc-dev/neo-go/pkg/rpcclient/oracle"
@ -1221,20 +1222,24 @@ func TestClient_NEP11_ND(t *testing.T) {
h, err := util.Uint160DecodeStringLE(nnsContractHash) h, err := util.Uint160DecodeStringLE(nnsContractHash)
require.NoError(t, err) require.NoError(t, err)
acc := testchain.PrivateKeyByID(0).GetScriptHash() priv0 := testchain.PrivateKeyByID(0)
act, err := actor.NewSimple(c, wallet.NewAccountFromPrivateKey(priv0))
require.NoError(t, err)
n11 := nep11.NewNonDivisible(act, h)
acc := priv0.GetScriptHash()
t.Run("Decimals", func(t *testing.T) { t.Run("Decimals", func(t *testing.T) {
d, err := c.NEP11Decimals(h) d, err := n11.Decimals()
require.NoError(t, err) require.NoError(t, err)
require.EqualValues(t, 0, d) // non-divisible require.EqualValues(t, 0, d) // non-divisible
}) })
t.Run("TotalSupply", func(t *testing.T) { t.Run("TotalSupply", func(t *testing.T) {
s, err := c.NEP11TotalSupply(h) s, err := n11.TotalSupply()
require.NoError(t, err) require.NoError(t, err)
require.EqualValues(t, 1, s) // the only `neo.com` of acc0 require.EqualValues(t, big.NewInt(1), s) // the only `neo.com` of acc0
}) })
t.Run("Symbol", func(t *testing.T) { t.Run("Symbol", func(t *testing.T) {
sym, err := c.NEP11Symbol(h) sym, err := n11.Symbol()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "NNS", sym) require.Equal(t, "NNS", sym)
}) })
@ -1250,17 +1255,31 @@ func TestClient_NEP11_ND(t *testing.T) {
}, tok) }, tok)
}) })
t.Run("BalanceOf", func(t *testing.T) { t.Run("BalanceOf", func(t *testing.T) {
b, err := c.NEP11BalanceOf(h, acc) b, err := n11.BalanceOf(acc)
require.NoError(t, err) require.NoError(t, err)
require.EqualValues(t, 1, b) require.EqualValues(t, big.NewInt(1), b)
}) })
t.Run("OwnerOf", func(t *testing.T) { t.Run("OwnerOf", func(t *testing.T) {
b, err := c.NEP11NDOwnerOf(h, []byte("neo.com")) b, err := n11.OwnerOf([]byte("neo.com"))
require.NoError(t, err) require.NoError(t, err)
require.EqualValues(t, acc, b) require.EqualValues(t, acc, b)
}) })
t.Run("Tokens", func(t *testing.T) {
iter, err := n11.Tokens()
require.NoError(t, err)
items, err := iter.Next(config.DefaultMaxIteratorResultItems)
require.NoError(t, err)
require.Equal(t, 1, len(items))
require.Equal(t, [][]byte{[]byte("neo.com")}, items)
require.NoError(t, iter.Terminate())
})
t.Run("TokensExpanded", func(t *testing.T) {
items, err := n11.TokensExpanded(config.DefaultMaxIteratorResultItems)
require.NoError(t, err)
require.Equal(t, [][]byte{[]byte("neo.com")}, items)
})
t.Run("Properties", func(t *testing.T) { t.Run("Properties", func(t *testing.T) {
p, err := c.NEP11Properties(h, []byte("neo.com")) p, err := n11.Properties([]byte("neo.com"))
require.NoError(t, err) require.NoError(t, err)
blockRegisterDomain, err := chain.GetBlock(chain.GetHeaderHash(14)) // `neo.com` domain was registered in 14th block blockRegisterDomain, err := chain.GetBlock(chain.GetHeaderHash(14)) // `neo.com` domain was registered in 14th block
require.NoError(t, err) require.NoError(t, err)
@ -1271,7 +1290,7 @@ func TestClient_NEP11_ND(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", nil, 0, nil) _, _, err := n11.Transfer(testchain.PrivateKeyByID(1).GetScriptHash(), []byte("neo.com"), nil)
require.NoError(t, err) require.NoError(t, err)
}) })
} }
@ -1285,23 +1304,28 @@ func TestClient_NEP11_D(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
require.NoError(t, c.Init()) require.NoError(t, c.Init())
priv0 := testchain.PrivateKeyByID(0).GetScriptHash() pkey0 := testchain.PrivateKeyByID(0)
priv0 := pkey0.GetScriptHash()
priv1 := testchain.PrivateKeyByID(1).GetScriptHash() priv1 := testchain.PrivateKeyByID(1).GetScriptHash()
token1ID, err := hex.DecodeString(nfsoToken1ID) token1ID, err := hex.DecodeString(nfsoToken1ID)
require.NoError(t, err) require.NoError(t, err)
act, err := actor.NewSimple(c, wallet.NewAccountFromPrivateKey(pkey0))
require.NoError(t, err)
n11 := nep11.NewDivisible(act, nfsoHash)
t.Run("Decimals", func(t *testing.T) { t.Run("Decimals", func(t *testing.T) {
d, err := c.NEP11Decimals(nfsoHash) d, err := n11.Decimals()
require.NoError(t, err) require.NoError(t, err)
require.EqualValues(t, 2, d) // Divisible. require.EqualValues(t, 2, d) // Divisible.
}) })
t.Run("TotalSupply", func(t *testing.T) { t.Run("TotalSupply", func(t *testing.T) {
s, err := c.NEP11TotalSupply(nfsoHash) s, err := n11.TotalSupply()
require.NoError(t, err) require.NoError(t, err)
require.EqualValues(t, 1, s) // the only NFSO of acc0 require.EqualValues(t, big.NewInt(1), s) // the only NFSO of acc0
}) })
t.Run("Symbol", func(t *testing.T) { t.Run("Symbol", func(t *testing.T) {
sym, err := c.NEP11Symbol(nfsoHash) sym, err := n11.Symbol()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, "NFSO", sym) require.Equal(t, "NFSO", sym)
}) })
@ -1317,29 +1341,31 @@ func TestClient_NEP11_D(t *testing.T) {
}, tok) }, tok)
}) })
t.Run("BalanceOf", func(t *testing.T) { t.Run("BalanceOf", func(t *testing.T) {
b, err := c.NEP11BalanceOf(nfsoHash, priv0) b, err := n11.BalanceOf(priv0)
require.NoError(t, err) require.NoError(t, err)
require.EqualValues(t, 80, b) require.EqualValues(t, big.NewInt(80), b)
})
t.Run("BalanceOfD", func(t *testing.T) {
b, err := n11.BalanceOfD(priv0, token1ID)
require.NoError(t, err)
require.EqualValues(t, big.NewInt(80), b)
}) })
t.Run("OwnerOf", func(t *testing.T) { t.Run("OwnerOf", func(t *testing.T) {
sessID, iter, err := c.NEP11DOwnerOf(nfsoHash, token1ID) iter, err := n11.OwnerOf(token1ID)
require.NoError(t, err) require.NoError(t, err)
items, err := c.TraverseIterator(sessID, *iter.ID, config.DefaultMaxIteratorResultItems) items, err := iter.Next(config.DefaultMaxIteratorResultItems)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, 2, len(items)) require.Equal(t, 2, len(items))
actual1, err := util.Uint160DecodeBytesBE(items[0].Value().([]byte)) require.Equal(t, []util.Uint160{priv1, priv0}, items)
require.NoError(t, err) require.NoError(t, iter.Terminate())
actual0, err := util.Uint160DecodeBytesBE(items[1].Value().([]byte))
require.NoError(t, err)
require.Equal(t, []util.Uint160{priv1, priv0}, []util.Uint160{actual1, actual0})
}) })
t.Run("UnpackedOwnerOf", func(t *testing.T) { t.Run("OwnerOfExpanded", func(t *testing.T) {
b, err := c.NEP11DUnpackedOwnerOf(nfsoHash, token1ID) b, err := n11.OwnerOfExpanded(token1ID, config.DefaultMaxIteratorResultItems)
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, []util.Uint160{priv1, priv0}, b) require.Equal(t, []util.Uint160{priv1, priv0}, b)
}) })
t.Run("Properties", func(t *testing.T) { t.Run("Properties", func(t *testing.T) {
p, err := c.NEP11Properties(nfsoHash, token1ID) p, err := n11.Properties(token1ID)
require.NoError(t, err) require.NoError(t, err)
expected := stackitem.NewMap() expected := stackitem.NewMap()
expected.Add(stackitem.Make([]byte("name")), stackitem.NewBuffer([]byte("NeoFS Object "+base64.StdEncoding.EncodeToString(token1ID)))) expected.Add(stackitem.Make([]byte("name")), stackitem.NewBuffer([]byte("NeoFS Object "+base64.StdEncoding.EncodeToString(token1ID))))
@ -1348,9 +1374,7 @@ func TestClient_NEP11_D(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.TransferNEP11D(wallet.NewAccountFromPrivateKey(testchain.PrivateKeyByID(0)), _, _, err := n11.TransferD(priv0, priv1, big.NewInt(20), token1ID, nil)
testchain.PrivateKeyByID(1).GetScriptHash(),
nfsoHash, 20, token1ID, nil, 0, nil)
require.NoError(t, err) require.NoError(t, err)
}) })
} }