diff --git a/examples/nft-nd/nft.go b/examples/nft-nd/nft.go index 5319ff03e..5df36422e 100644 --- a/examples/nft-nd/nft.go +++ b/examples/nft-nd/nft.go @@ -25,6 +25,7 @@ const ( totalSupplyPrefix = "s" accountPrefix = "a" tokenPrefix = "t" + tokensPrefix = "ts" ) var ( @@ -76,6 +77,10 @@ func mkTokenKey(token []byte) []byte { return append(res, token...) } +func mkTokensKey() []byte { + return []byte(tokensPrefix) +} + // BalanceOf returns the number of tokens owned by specified address. func BalanceOf(holder interop.Hash160) int { if len(holder) != 20 { @@ -112,6 +117,36 @@ func setTokensOf(ctx storage.Context, holder interop.Hash160, tokens []string) { } } +// setTokens saves minted token if it is not saved yet. +func setTokens(ctx storage.Context, newToken string) { + key := mkTokensKey() + var tokens = []string{} + val := storage.Get(ctx, key) + if val != nil { + tokens = std.Deserialize(val.([]byte)).([]string) + } + for i := 0; i < len(tokens); i++ { + if util.Equals(tokens[i], newToken) { + return + } + } + tokens = append(tokens, newToken) + val = std.Serialize(tokens) + storage.Put(ctx, key, val) +} + +// Tokens returns an iterator that contains all of the tokens minted by the contract. +func Tokens() iterator.Iterator { + ctx := storage.GetReadOnlyContext() + var arr = []string{} + key := mkTokensKey() + val := storage.Get(ctx, key) + if val != nil { + arr = std.Deserialize(val.([]byte)).([]string) + } + return iterator.Create(arr) +} + // TokensOf returns an iterator with all tokens held by specified address. func TokensOf(holder interop.Hash160) iterator.Iterator { if len(holder) != 20 { @@ -219,6 +254,7 @@ func OnNEP17Payment(from interop.Hash160, amount int, data interface{}) { toksOf = append(toksOf, token) setTokensOf(ctx, from, toksOf) setOwnerOf(ctx, []byte(token), from) + setTokens(ctx, token) total++ storage.Put(ctx, []byte(totalSupplyPrefix), total) diff --git a/examples/nft-nd/nft.yml b/examples/nft-nd/nft.yml index 33f8627fa..3441ee8e9 100644 --- a/examples/nft-nd/nft.yml +++ b/examples/nft-nd/nft.yml @@ -1,6 +1,6 @@ name: "HASHY NFT" supportedstandards: ["NEP-11"] -safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf"] +safemethods: ["balanceOf", "decimals", "symbol", "totalSupply", "tokensOf", "ownerOf", "tokens"] events: - name: Transfer parameters: diff --git a/pkg/rpc/client/helper.go b/pkg/rpc/client/helper.go index 74f02d8cb..2744deb16 100644 --- a/pkg/rpc/client/helper.go +++ b/pkg/rpc/client/helper.go @@ -95,3 +95,38 @@ func topMapFromStack(st []stackitem.Item) (*stackitem.Map, error) { } return st[index].(*stackitem.Map), nil } + +// topIterableFromStack returns top list of elements of `resultItemType` type from stack. +func topIterableFromStack(st []stackitem.Item, resultItemType interface{}) ([]interface{}, error) { + index := len(st) - 1 // top stack element is last in the array + if t := st[index].Type(); t != stackitem.InteropT { + return nil, fmt.Errorf("invalid return stackitem type: %s (InteropInterface expected)", t.String()) + } + iter, ok := st[index].Value().(result.Iterator) + if !ok { + return nil, fmt.Errorf("failed to deserialize iterable from interop stackitem: invalid value type (Array expected)") + } + result := make([]interface{}, len(iter.Values)) + for i := range iter.Values { + switch resultItemType.(type) { + case string: + bytes, err := iter.Values[i].TryBytes() + if err != nil { + return nil, fmt.Errorf("failed to deserialize string from stackitem #%d: %w", i, err) + } + result[i] = string(bytes) + case util.Uint160: + bytes, err := iter.Values[i].TryBytes() + if err != nil { + return nil, fmt.Errorf("failed to deserialize uint160 from stackitem #%d: %w", i, err) + } + result[i], err = util.Uint160DecodeBytesBE(bytes) + if err != nil { + return nil, fmt.Errorf("failed to decode uint160 from stackitem #%d: %w", i, err) + } + default: + return nil, errors.New("unsupported iterable type") + } + } + return result, nil +} diff --git a/pkg/rpc/client/nep11.go b/pkg/rpc/client/nep11.go index 62629a977..5fc10c83a 100644 --- a/pkg/rpc/client/nep11.go +++ b/pkg/rpc/client/nep11.go @@ -85,6 +85,33 @@ func (c *Client) CreateNEP11TransferTx(acc *wallet.Account, tokenHash util.Uint1 }}, cosigners...)) } +// NEP11TokensOf returns an array of token IDs for the specified owner of the specified NFT token. +func (c *Client) NEP11TokensOf(tokenHash util.Uint160, owner util.Uint160) ([]string, error) { + result, err := c.InvokeFunction(tokenHash, "tokensOf", []smartcontract.Parameter{ + { + Type: smartcontract.Hash160Type, + Value: owner, + }, + }, nil) + if err != nil { + return nil, err + } + err = getInvocationError(result) + if err != nil { + return nil, err + } + + arr, err := topIterableFromStack(result.Stack, string("")) + if err != nil { + return nil, fmt.Errorf("failed to get token IDs from stack: %w", err) + } + ids := make([]string, len(arr)) + for i := range ids { + ids[i] = arr[i].(string) + } + return ids, nil +} + // Non-divisible NFT methods section start. // NEP11NDOwnerOf invokes `ownerOf` non-devisible NEP11 method with the @@ -138,6 +165,33 @@ func (c *Client) NEP11DBalanceOf(tokenHash, owner util.Uint160, tokenID string) return c.nepBalanceOf(tokenHash, owner, &tokenID) } +// NEP11DOwnerOf returns list of the specified NEP11 divisible token owners. +func (c *Client) NEP11DOwnerOf(tokenHash util.Uint160, tokenID string) ([]util.Uint160, error) { + result, err := c.InvokeFunction(tokenHash, "ownerOf", []smartcontract.Parameter{ + { + Type: smartcontract.StringType, + Value: tokenID, + }, + }, nil) + if err != nil { + return nil, err + } + err = getInvocationError(result) + if err != nil { + return nil, err + } + + arr, err := topIterableFromStack(result.Stack, util.Uint160{}) + if err != nil { + return nil, fmt.Errorf("failed to get token IDs from stack: %w", err) + } + owners := make([]util.Uint160, len(arr)) + for i := range owners { + owners[i] = arr[i].(util.Uint160) + } + return owners, nil +} + // Divisible NFT methods section end. // Optional NFT methods section start. @@ -160,4 +214,26 @@ func (c *Client) NEP11Properties(tokenHash util.Uint160, tokenID string) (*stack return topMapFromStack(result.Stack) } +// NEP11Tokens returns list of the tokens minted by the contract. +func (c *Client) NEP11Tokens(tokenHash util.Uint160) ([]string, error) { + result, err := c.InvokeFunction(tokenHash, "tokens", []smartcontract.Parameter{}, nil) + if err != nil { + return nil, err + } + err = getInvocationError(result) + if err != nil { + return nil, err + } + + arr, err := topIterableFromStack(result.Stack, string("")) + if err != nil { + return nil, fmt.Errorf("failed to get token IDs from stack: %w", err) + } + tokens := make([]string, len(arr)) + for i := range tokens { + tokens[i] = arr[i].(string) + } + return tokens, nil +} + // Optional NFT methods section end.