github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/pkg/rpcclient/nep11/base.go (about)

     1  /*
     2  Package nep11 contains RPC wrappers for NEP-11 contracts.
     3  
     4  The set of types provided is split between common NEP-11 methods (BaseReader and
     5  Base types) and divisible (DivisibleReader and Divisible) and non-divisible
     6  (NonDivisibleReader and NonDivisible). If you don't know the type of NEP-11
     7  contract you're going to use you can use Base and BaseReader types for many
     8  purposes, otherwise more specific types are recommended.
     9  */
    10  package nep11
    11  
    12  import (
    13  	"errors"
    14  	"fmt"
    15  	"math/big"
    16  	"unicode/utf8"
    17  
    18  	"github.com/google/uuid"
    19  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    20  	"github.com/nspcc-dev/neo-go/pkg/neorpc/result"
    21  	"github.com/nspcc-dev/neo-go/pkg/rpcclient/neptoken"
    22  	"github.com/nspcc-dev/neo-go/pkg/rpcclient/unwrap"
    23  	"github.com/nspcc-dev/neo-go/pkg/smartcontract"
    24  	"github.com/nspcc-dev/neo-go/pkg/util"
    25  	"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
    26  )
    27  
    28  // Invoker is used by reader types to call various methods.
    29  type Invoker interface {
    30  	neptoken.Invoker
    31  
    32  	CallAndExpandIterator(contract util.Uint160, method string, maxItems int, params ...any) (*result.Invoke, error)
    33  	TerminateSession(sessionID uuid.UUID) error
    34  	TraverseIterator(sessionID uuid.UUID, iterator *result.Iterator, num int) ([]stackitem.Item, error)
    35  }
    36  
    37  // Actor is used by complete NEP-11 types to create and send transactions.
    38  type Actor interface {
    39  	Invoker
    40  
    41  	MakeRun(script []byte) (*transaction.Transaction, error)
    42  	MakeUnsignedRun(script []byte, attrs []transaction.Attribute) (*transaction.Transaction, error)
    43  	SendRun(script []byte) (util.Uint256, uint32, error)
    44  }
    45  
    46  // BaseReader is a reader interface for common divisible and non-divisible NEP-11
    47  // methods. It allows to invoke safe methods.
    48  type BaseReader struct {
    49  	neptoken.Base
    50  
    51  	invoker Invoker
    52  	hash    util.Uint160
    53  }
    54  
    55  // BaseWriter is a transaction-creating interface for common divisible and
    56  // non-divisible NEP-11 methods. It simplifies reusing this set of methods,
    57  // but a complete Base is expected to be used in other packages.
    58  type BaseWriter struct {
    59  	hash  util.Uint160
    60  	actor Actor
    61  }
    62  
    63  // Base is a state-changing interface for common divisible and non-divisible NEP-11
    64  // methods.
    65  type Base struct {
    66  	BaseReader
    67  	BaseWriter
    68  }
    69  
    70  // TransferEvent represents a Transfer event as defined in the NEP-11 standard.
    71  type TransferEvent struct {
    72  	From   util.Uint160
    73  	To     util.Uint160
    74  	Amount *big.Int
    75  	ID     []byte
    76  }
    77  
    78  // TokenIterator is used for iterating over TokensOf results.
    79  type TokenIterator struct {
    80  	client   Invoker
    81  	session  uuid.UUID
    82  	iterator result.Iterator
    83  }
    84  
    85  // NewBaseReader creates an instance of BaseReader for a contract with the given
    86  // hash using the given invoker.
    87  func NewBaseReader(invoker Invoker, hash util.Uint160) *BaseReader {
    88  	return &BaseReader{*neptoken.New(invoker, hash), invoker, hash}
    89  }
    90  
    91  // NewBase creates an instance of Base for contract with the given
    92  // hash using the given actor.
    93  func NewBase(actor Actor, hash util.Uint160) *Base {
    94  	return &Base{*NewBaseReader(actor, hash), BaseWriter{hash, actor}}
    95  }
    96  
    97  // Properties returns a set of token's properties such as name or URL. The map
    98  // is returned as is from this method (stack item) for maximum flexibility,
    99  // contracts can return a lot of specific data there. Most of the time though
   100  // they return well-defined properties outlined in NEP-11 and
   101  // UnwrapKnownProperties can be used to get them in more convenient way. It's an
   102  // optional method per NEP-11 specification, so it can fail.
   103  func (t *BaseReader) Properties(token []byte) (*stackitem.Map, error) {
   104  	return unwrap.Map(t.invoker.Call(t.hash, "properties", token))
   105  }
   106  
   107  // Tokens returns an iterator that allows to retrieve all tokens minted by the
   108  // contract. It depends on the server to provide proper session-based
   109  // iterator, but can also work with expanded one. The method itself is optional
   110  // per NEP-11 specification, so it can fail.
   111  func (t *BaseReader) Tokens() (*TokenIterator, error) {
   112  	sess, iter, err := unwrap.SessionIterator(t.invoker.Call(t.hash, "tokens"))
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  	return &TokenIterator{t.invoker, sess, iter}, nil
   117  }
   118  
   119  // TokensExpanded uses the same NEP-11 method as Tokens, but can be useful if
   120  // the server used doesn't support sessions and doesn't expand iterators. It
   121  // creates a script that will get num of result items from the iterator right in
   122  // the VM and return them to you. It's only limited by VM stack and GAS available
   123  // for RPC invocations.
   124  func (t *BaseReader) TokensExpanded(num int) ([][]byte, error) {
   125  	return unwrap.ArrayOfBytes(t.invoker.CallAndExpandIterator(t.hash, "tokens", num))
   126  }
   127  
   128  // TokensOf returns an iterator that allows to walk through all tokens owned by
   129  // the given account. It depends on the server to provide proper session-based
   130  // iterator, but can also work with expanded one.
   131  func (t *BaseReader) TokensOf(account util.Uint160) (*TokenIterator, error) {
   132  	sess, iter, err := unwrap.SessionIterator(t.invoker.Call(t.hash, "tokensOf", account))
   133  	if err != nil {
   134  		return nil, err
   135  	}
   136  	return &TokenIterator{t.invoker, sess, iter}, nil
   137  }
   138  
   139  // TokensOfExpanded uses the same NEP-11 method as TokensOf, but can be useful if
   140  // the server used doesn't support sessions and doesn't expand iterators. It
   141  // creates a script that will get num of result items from the iterator right in
   142  // the VM and return them to you. It's only limited by VM stack and GAS available
   143  // for RPC invocations.
   144  func (t *BaseReader) TokensOfExpanded(account util.Uint160, num int) ([][]byte, error) {
   145  	return unwrap.ArrayOfBytes(t.invoker.CallAndExpandIterator(t.hash, "tokensOf", num, account))
   146  }
   147  
   148  // Transfer creates and sends a transaction that performs a `transfer` method
   149  // call using the given parameters and checks for this call result, failing the
   150  // transaction if it's not true. It works for divisible NFTs only when there is
   151  // one owner for the particular token. The returned values are transaction hash,
   152  // its ValidUntilBlock value and an error if any.
   153  func (t *BaseWriter) Transfer(to util.Uint160, id []byte, data any) (util.Uint256, uint32, error) {
   154  	script, err := t.transferScript(to, id, data)
   155  	if err != nil {
   156  		return util.Uint256{}, 0, err
   157  	}
   158  	return t.actor.SendRun(script)
   159  }
   160  
   161  // TransferTransaction creates a transaction that performs a `transfer` method
   162  // call using the given parameters and checks for this call result, failing the
   163  // transaction if it's not true. It works for divisible NFTs only when there is
   164  // one owner for the particular token. This transaction is signed, but not sent
   165  // to the network, instead it's returned to the caller.
   166  func (t *BaseWriter) TransferTransaction(to util.Uint160, id []byte, data any) (*transaction.Transaction, error) {
   167  	script, err := t.transferScript(to, id, data)
   168  	if err != nil {
   169  		return nil, err
   170  	}
   171  	return t.actor.MakeRun(script)
   172  }
   173  
   174  // TransferUnsigned creates a transaction that performs a `transfer` method
   175  // call using the given parameters and checks for this call result, failing the
   176  // transaction if it's not true. It works for divisible NFTs only when there is
   177  // one owner for the particular token. This transaction is not signed and just
   178  // returned to the caller.
   179  func (t *BaseWriter) TransferUnsigned(to util.Uint160, id []byte, data any) (*transaction.Transaction, error) {
   180  	script, err := t.transferScript(to, id, data)
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  	return t.actor.MakeUnsignedRun(script, nil)
   185  }
   186  
   187  func (t *BaseWriter) transferScript(params ...any) ([]byte, error) {
   188  	return smartcontract.CreateCallWithAssertScript(t.hash, "transfer", params...)
   189  }
   190  
   191  // Next returns the next set of elements from the iterator (up to num of them).
   192  // It can return less than num elements in case iterator doesn't have that many
   193  // or zero elements if the iterator has no more elements or the session is
   194  // expired.
   195  func (v *TokenIterator) Next(num int) ([][]byte, error) {
   196  	items, err := v.client.TraverseIterator(v.session, &v.iterator, num)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	res := make([][]byte, len(items))
   201  	for i := range items {
   202  		b, err := items[i].TryBytes()
   203  		if err != nil {
   204  			return nil, fmt.Errorf("element %d is not a byte string: %w", i, err)
   205  		}
   206  		res[i] = b
   207  	}
   208  	return res, nil
   209  }
   210  
   211  // Terminate closes the iterator session used by TokenIterator (if it's
   212  // session-based).
   213  func (v *TokenIterator) Terminate() error {
   214  	if v.iterator.ID == nil {
   215  		return nil
   216  	}
   217  	return v.client.TerminateSession(v.session)
   218  }
   219  
   220  // UnwrapKnownProperties can be used as a proxy function to extract well-known
   221  // NEP-11 properties (name/description/image/tokenURI) defined in the standard.
   222  // These properties are checked to be valid UTF-8 strings, but can contain
   223  // control codes or special characters.
   224  func UnwrapKnownProperties(m *stackitem.Map, err error) (map[string]string, error) {
   225  	if err != nil {
   226  		return nil, err
   227  	}
   228  	elems := m.Value().([]stackitem.MapElement)
   229  	res := make(map[string]string)
   230  	for _, e := range elems {
   231  		k, err := e.Key.TryBytes()
   232  		if err != nil { // Shouldn't ever happen in the valid Map, but.
   233  			continue
   234  		}
   235  		ks := string(k)
   236  		if !result.KnownNEP11Properties[ks] { // Some additional elements are OK.
   237  			continue
   238  		}
   239  		v, err := e.Value.TryBytes()
   240  		if err != nil { // But known ones MUST be proper strings.
   241  			return nil, fmt.Errorf("invalid %s property: %w", ks, err)
   242  		}
   243  		if !utf8.Valid(v) {
   244  			return nil, fmt.Errorf("invalid %s property: not a UTF-8 string", ks)
   245  		}
   246  		res[ks] = string(v)
   247  	}
   248  	return res, nil
   249  }
   250  
   251  // TransferEventsFromApplicationLog retrieves all emitted TransferEvents from the
   252  // provided [result.ApplicationLog].
   253  func TransferEventsFromApplicationLog(log *result.ApplicationLog) ([]*TransferEvent, error) {
   254  	if log == nil {
   255  		return nil, errors.New("nil application log")
   256  	}
   257  	var res []*TransferEvent
   258  	for i, ex := range log.Executions {
   259  		for j, e := range ex.Events {
   260  			if e.Name != "Transfer" {
   261  				continue
   262  			}
   263  			event := new(TransferEvent)
   264  			err := event.FromStackItem(e.Item)
   265  			if err != nil {
   266  				return nil, fmt.Errorf("failed to decode event from stackitem (event #%d, execution #%d): %w", j, i, err)
   267  			}
   268  			res = append(res, event)
   269  		}
   270  	}
   271  	return res, nil
   272  }
   273  
   274  // FromStackItem converts provided [stackitem.Array] to TransferEvent or returns an
   275  // error if it's not possible to do to so.
   276  func (e *TransferEvent) FromStackItem(item *stackitem.Array) error {
   277  	if item == nil {
   278  		return errors.New("nil item")
   279  	}
   280  	arr, ok := item.Value().([]stackitem.Item)
   281  	if !ok {
   282  		return errors.New("not an array")
   283  	}
   284  	if len(arr) != 4 {
   285  		return errors.New("wrong number of event parameters")
   286  	}
   287  
   288  	b, err := arr[0].TryBytes()
   289  	if err != nil {
   290  		return fmt.Errorf("invalid From: %w", err)
   291  	}
   292  	e.From, err = util.Uint160DecodeBytesBE(b)
   293  	if err != nil {
   294  		return fmt.Errorf("failed to decode From: %w", err)
   295  	}
   296  
   297  	b, err = arr[1].TryBytes()
   298  	if err != nil {
   299  		return fmt.Errorf("invalid To: %w", err)
   300  	}
   301  	e.To, err = util.Uint160DecodeBytesBE(b)
   302  	if err != nil {
   303  		return fmt.Errorf("failed to decode To: %w", err)
   304  	}
   305  
   306  	e.Amount, err = arr[2].TryInteger()
   307  	if err != nil {
   308  		return fmt.Errorf("field to decode Avount: %w", err)
   309  	}
   310  
   311  	e.ID, err = arr[3].TryBytes()
   312  	if err != nil {
   313  		return fmt.Errorf("failed to decode ID: %w", err)
   314  	}
   315  
   316  	return nil
   317  }