-
Notifications
You must be signed in to change notification settings - Fork 77
/
base.go
317 lines (283 loc) · 10.9 KB
/
base.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
/*
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 (
"errors"
"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 ...any) (*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
}
// BaseWriter is a transaction-creating interface for common divisible and
// non-divisible NEP-11 methods. It simplifies reusing this set of methods,
// but a complete Base is expected to be used in other packages.
type BaseWriter struct {
hash util.Uint160
actor Actor
}
// Base is a state-changing interface for common divisible and non-divisible NEP-11
// methods.
type Base struct {
BaseReader
BaseWriter
}
// 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), BaseWriter{hash, actor}}
}
// 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 *BaseWriter) Transfer(to util.Uint160, id []byte, data any) (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 *BaseWriter) TransferTransaction(to util.Uint160, id []byte, data any) (*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 *BaseWriter) TransferUnsigned(to util.Uint160, id []byte, data any) (*transaction.Transaction, error) {
script, err := t.transferScript(to, id, data)
if err != nil {
return nil, err
}
return t.actor.MakeUnsignedRun(script, nil)
}
func (t *BaseWriter) transferScript(params ...any) ([]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
}
// TransferEventsFromApplicationLog retrieves all emitted TransferEvents from the
// provided [result.ApplicationLog].
func TransferEventsFromApplicationLog(log *result.ApplicationLog) ([]*TransferEvent, error) {
if log == nil {
return nil, errors.New("nil application log")
}
var res []*TransferEvent
for i, ex := range log.Executions {
for j, e := range ex.Events {
if e.Name != "Transfer" {
continue
}
event := new(TransferEvent)
err := event.FromStackItem(e.Item)
if err != nil {
return nil, fmt.Errorf("failed to decode event from stackitem (event #%d, execution #%d): %w", j, i, err)
}
res = append(res, event)
}
}
return res, nil
}
// FromStackItem converts provided [stackitem.Array] to TransferEvent or returns an
// error if it's not possible to do to so.
func (e *TransferEvent) FromStackItem(item *stackitem.Array) error {
if item == nil {
return errors.New("nil item")
}
arr, ok := item.Value().([]stackitem.Item)
if !ok {
return errors.New("not an array")
}
if len(arr) != 4 {
return errors.New("wrong number of event parameters")
}
b, err := arr[0].TryBytes()
if err != nil {
return fmt.Errorf("invalid From: %w", err)
}
e.From, err = util.Uint160DecodeBytesBE(b)
if err != nil {
return fmt.Errorf("failed to decode From: %w", err)
}
b, err = arr[1].TryBytes()
if err != nil {
return fmt.Errorf("invalid To: %w", err)
}
e.To, err = util.Uint160DecodeBytesBE(b)
if err != nil {
return fmt.Errorf("failed to decode To: %w", err)
}
e.Amount, err = arr[2].TryInteger()
if err != nil {
return fmt.Errorf("field to decode Avount: %w", err)
}
e.ID, err = arr[3].TryBytes()
if err != nil {
return fmt.Errorf("failed to decode ID: %w", err)
}
return nil
}