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 }