@btc-vision/btc-runtime 1.9.0 → 1.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/package.json +13 -4
  2. package/runtime/constants/Exports.ts +86 -0
  3. package/runtime/contracts/OP1155.ts +1042 -0
  4. package/runtime/contracts/OP20.ts +64 -38
  5. package/runtime/contracts/OP721.ts +882 -0
  6. package/runtime/contracts/OP_NET.ts +5 -0
  7. package/runtime/contracts/ReentrancyGuard.ts +136 -0
  8. package/runtime/contracts/interfaces/IOP1155.ts +33 -0
  9. package/runtime/contracts/interfaces/IOP721.ts +29 -0
  10. package/runtime/contracts/interfaces/OP1155InitParameters.ts +11 -0
  11. package/runtime/contracts/interfaces/OP721InitParameters.ts +15 -0
  12. package/runtime/env/BlockchainEnvironment.ts +32 -3
  13. package/runtime/events/predefined/ApprovedForAll.ts +16 -0
  14. package/runtime/events/predefined/TransferredBatchEvent.ts +35 -0
  15. package/runtime/events/predefined/TransferredSingleEvent.ts +18 -0
  16. package/runtime/events/predefined/URIEvent.ts +23 -0
  17. package/runtime/events/predefined/index.ts +4 -0
  18. package/runtime/index.ts +9 -0
  19. package/runtime/math/abi.ts +23 -0
  20. package/runtime/math/bytes.ts +4 -0
  21. package/runtime/memory/AddressMemoryMap.ts +20 -0
  22. package/runtime/nested/storage/StorageMap.ts +3 -23
  23. package/runtime/nested/storage/StorageSet.ts +6 -3
  24. package/runtime/script/Script.ts +1 -1
  25. package/runtime/secp256k1/ECPoint.ts +3 -3
  26. package/runtime/shared-libraries/OP20Utils.ts +1 -2
  27. package/runtime/storage/AdvancedStoredString.ts +8 -189
  28. package/runtime/storage/BaseStoredString.ts +206 -0
  29. package/runtime/storage/StoredString.ts +15 -194
  30. package/runtime/storage/arrays/StoredPackedArray.ts +19 -5
  31. package/runtime/types/SafeMath.ts +125 -94
@@ -0,0 +1,882 @@
1
+ import { u256 } from '@btc-vision/as-bignum/assembly';
2
+ import { BytesWriter } from '../buffer/BytesWriter';
3
+ import { Blockchain } from '../env';
4
+ import { sha256 } from '../env/global';
5
+ import { EMPTY_POINTER } from '../math/bytes';
6
+ import { AddressMemoryMap } from '../memory/AddressMemoryMap';
7
+ import { MapOfMap } from '../memory/MapOfMap';
8
+ import { StoredString } from '../storage/StoredString';
9
+ import { StoredU256 } from '../storage/StoredU256';
10
+ import { StoredU256Array } from '../storage/arrays/StoredU256Array';
11
+ import { Calldata } from '../types';
12
+ import { Address } from '../types/Address';
13
+ import { Revert } from '../types/Revert';
14
+ import { SafeMath } from '../types/SafeMath';
15
+ import {
16
+ ADDRESS_BYTE_LENGTH,
17
+ SELECTOR_BYTE_LENGTH,
18
+ U256_BYTE_LENGTH,
19
+ U32_BYTE_LENGTH,
20
+ U64_BYTE_LENGTH,
21
+ U8_BYTE_LENGTH,
22
+ } from '../utils';
23
+ import { IOP721 } from './interfaces/IOP721';
24
+ import { OP721InitParameters } from './interfaces/OP721InitParameters';
25
+ import { ReentrancyGuard } from './ReentrancyGuard';
26
+ import { StoredMapU256 } from '../storage/maps/StoredMapU256';
27
+ import { ApprovedEvent, ApprovedForAllEvent, MAX_URI_LENGTH, TransferredEvent, URIEvent } from '../events/predefined';
28
+ import {
29
+ ON_OP721_RECEIVED_SELECTOR,
30
+ OP712_DOMAIN_TYPE_HASH,
31
+ OP712_VERSION_HASH,
32
+ OP721_APPROVE_TYPE_HASH,
33
+ OP721_TRANSFER_TYPE_HASH,
34
+ } from '../constants/Exports';
35
+
36
+ // Storage pointers
37
+ const namePointer: u16 = Blockchain.nextPointer;
38
+ const symbolPointer: u16 = Blockchain.nextPointer;
39
+ const baseURIPointer: u16 = Blockchain.nextPointer;
40
+ const totalSupplyPointer: u16 = Blockchain.nextPointer;
41
+ const maxSupplyPointer: u16 = Blockchain.nextPointer;
42
+ const ownerOfMapPointer: u16 = Blockchain.nextPointer;
43
+ const tokenApprovalMapPointer: u16 = Blockchain.nextPointer;
44
+ const operatorApprovalMapPointer: u16 = Blockchain.nextPointer;
45
+ const balanceOfMapPointer: u16 = Blockchain.nextPointer;
46
+ const tokenURIMapPointer: u16 = Blockchain.nextPointer;
47
+ const nextTokenIdPointer: u16 = Blockchain.nextPointer;
48
+ const ownerTokensMapPointer: u16 = Blockchain.nextPointer;
49
+ const tokenIndexMapPointer: u16 = Blockchain.nextPointer;
50
+ const initializedPointer: u16 = Blockchain.nextPointer;
51
+ const tokenURICounterPointer: u16 = Blockchain.nextPointer;
52
+ const transferNonceMapPointer: u16 = Blockchain.nextPointer;
53
+ const approveNonceMapPointer: u16 = Blockchain.nextPointer;
54
+
55
+ export abstract class OP721 extends ReentrancyGuard implements IOP721 {
56
+ protected readonly _name: StoredString;
57
+ protected readonly _symbol: StoredString;
58
+ protected readonly _baseURI: StoredString;
59
+ protected readonly _totalSupply: StoredU256;
60
+ protected readonly _maxSupply: StoredU256;
61
+ protected readonly _nextTokenId: StoredU256;
62
+ protected readonly _initialized: StoredU256;
63
+ protected readonly _tokenURICounter: StoredU256;
64
+
65
+ protected readonly ownerOfMap: StoredMapU256;
66
+ protected readonly tokenApprovalMap: StoredMapU256;
67
+ protected readonly balanceOfMap: AddressMemoryMap;
68
+ protected readonly operatorApprovalMap: MapOfMap<u256>;
69
+
70
+ // Separate nonces for different operations
71
+ protected readonly _transferNonceMap: AddressMemoryMap;
72
+ protected readonly _approveNonceMap: AddressMemoryMap;
73
+
74
+ // Token URI storage - stores index to StoredString array
75
+ protected readonly tokenURIIndices: StoredMapU256;
76
+ protected readonly tokenURIStorage: Map<u32, StoredString> = new Map();
77
+
78
+ // Enumerable extension - owner -> array of token IDs
79
+ protected readonly ownerTokensMap: Map<Address, StoredU256Array> = new Map();
80
+
81
+ // Token ID -> index in owner's array
82
+ protected readonly tokenIndexMap: StoredMapU256;
83
+
84
+ public constructor() {
85
+ super();
86
+
87
+ this._name = new StoredString(namePointer, 0);
88
+ this._symbol = new StoredString(symbolPointer, 0);
89
+ this._baseURI = new StoredString(baseURIPointer, 0);
90
+ this._totalSupply = new StoredU256(totalSupplyPointer, EMPTY_POINTER);
91
+ this._maxSupply = new StoredU256(maxSupplyPointer, EMPTY_POINTER);
92
+ this._nextTokenId = new StoredU256(nextTokenIdPointer, EMPTY_POINTER);
93
+ this._initialized = new StoredU256(initializedPointer, EMPTY_POINTER);
94
+ this._tokenURICounter = new StoredU256(tokenURICounterPointer, EMPTY_POINTER);
95
+
96
+ this.ownerOfMap = new StoredMapU256(ownerOfMapPointer);
97
+ this.tokenApprovalMap = new StoredMapU256(tokenApprovalMapPointer);
98
+ this.balanceOfMap = new AddressMemoryMap(balanceOfMapPointer);
99
+ this.operatorApprovalMap = new MapOfMap<u256>(operatorApprovalMapPointer);
100
+
101
+ // Initialize separate nonce maps
102
+ this._transferNonceMap = new AddressMemoryMap(transferNonceMapPointer);
103
+ this._approveNonceMap = new AddressMemoryMap(approveNonceMapPointer);
104
+
105
+ this.tokenURIIndices = new StoredMapU256(tokenURIMapPointer);
106
+ this.tokenIndexMap = new StoredMapU256(tokenIndexMapPointer);
107
+ }
108
+
109
+ public get name(): string {
110
+ return this._name.value;
111
+ }
112
+
113
+ public get symbol(): string {
114
+ return this._symbol.value;
115
+ }
116
+
117
+ public get baseURI(): string {
118
+ return this._baseURI.value;
119
+ }
120
+
121
+ public get totalSupply(): u256 {
122
+ return this._totalSupply.value;
123
+ }
124
+
125
+ public get maxSupply(): u256 {
126
+ return this._maxSupply.value;
127
+ }
128
+
129
+ public instantiate(
130
+ params: OP721InitParameters,
131
+ skipDeployerVerification: boolean = false,
132
+ ): void {
133
+ if (!this._initialized.value.isZero()) throw new Revert('Already initialized');
134
+ if (!skipDeployerVerification) this.onlyDeployer(Blockchain.tx.sender);
135
+
136
+ if (params.name.length == 0) throw new Revert('Name cannot be empty');
137
+ if (params.symbol.length == 0) throw new Revert('Symbol cannot be empty');
138
+ if (params.maxSupply.isZero()) throw new Revert('Max supply cannot be zero');
139
+
140
+ this._name.value = params.name;
141
+ this._symbol.value = params.symbol;
142
+ this._baseURI.value = params.baseURI;
143
+ this._maxSupply.value = params.maxSupply;
144
+ this._nextTokenId.value = u256.One;
145
+ this._initialized.value = u256.One;
146
+ this._tokenURICounter.value = u256.Zero;
147
+ }
148
+
149
+ @method('name')
150
+ @returns({ name: 'name', type: ABIDataTypes.STRING })
151
+ public fn_name(_: Calldata): BytesWriter {
152
+ const name = this.name;
153
+ const w = new BytesWriter(String.UTF8.byteLength(name) + 4);
154
+ w.writeStringWithLength(name);
155
+ return w;
156
+ }
157
+
158
+ @method('symbol')
159
+ @returns({ name: 'symbol', type: ABIDataTypes.STRING })
160
+ public fn_symbol(_: Calldata): BytesWriter {
161
+ const symbol = this.symbol;
162
+ const w = new BytesWriter(String.UTF8.byteLength(symbol) + 4);
163
+ w.writeStringWithLength(symbol);
164
+ return w;
165
+ }
166
+
167
+ @method()
168
+ @returns({ name: 'maxSupply', type: ABIDataTypes.UINT256 })
169
+ public fn_maxSupply(_: Calldata): BytesWriter {
170
+ const w = new BytesWriter(U256_BYTE_LENGTH);
171
+ w.writeU256(this.maxSupply);
172
+ return w;
173
+ }
174
+
175
+ @method({ name: 'tokenId', type: ABIDataTypes.UINT256 })
176
+ @returns({ name: 'uri', type: ABIDataTypes.STRING })
177
+ public tokenURI(calldata: Calldata): BytesWriter {
178
+ const tokenId = calldata.readU256();
179
+ if (!this._exists(tokenId)) throw new Revert('Token does not exist');
180
+
181
+ // Check if custom URI exists
182
+ const uriIndex = this.tokenURIIndices.get(tokenId);
183
+ let uri: string;
184
+
185
+ if (!uriIndex.isZero()) {
186
+ // Get custom URI from storage
187
+ const index = uriIndex.toU32();
188
+ if (!this.tokenURIStorage.has(index)) {
189
+ // Lazy load from storage
190
+ const storedURI = new StoredString(tokenURIMapPointer, index);
191
+ this.tokenURIStorage.set(index, storedURI);
192
+ }
193
+ uri = this.tokenURIStorage.get(index).value;
194
+ } else {
195
+ // Return baseURI + tokenId
196
+ uri = this.baseURI + tokenId.toString();
197
+ }
198
+
199
+ const w = new BytesWriter(String.UTF8.byteLength(uri) + 4);
200
+ w.writeStringWithLength(uri);
201
+ return w;
202
+ }
203
+
204
+ @method()
205
+ @returns({ name: 'totalSupply', type: ABIDataTypes.UINT256 })
206
+ public fn_totalSupply(_: Calldata): BytesWriter {
207
+ const w = new BytesWriter(U256_BYTE_LENGTH);
208
+ w.writeU256(this.totalSupply);
209
+ return w;
210
+ }
211
+
212
+ @method({ name: 'owner', type: ABIDataTypes.ADDRESS })
213
+ @returns({ name: 'balance', type: ABIDataTypes.UINT256 })
214
+ public balanceOf(calldata: Calldata): BytesWriter {
215
+ const owner = calldata.readAddress();
216
+ const balance = this._balanceOf(owner);
217
+ const w = new BytesWriter(U256_BYTE_LENGTH);
218
+ w.writeU256(balance);
219
+ return w;
220
+ }
221
+
222
+ @method({ name: 'tokenId', type: ABIDataTypes.UINT256 })
223
+ @returns({ name: 'owner', type: ABIDataTypes.ADDRESS })
224
+ public ownerOf(calldata: Calldata): BytesWriter {
225
+ const tokenId = calldata.readU256();
226
+ const owner = this._ownerOf(tokenId);
227
+ const w = new BytesWriter(ADDRESS_BYTE_LENGTH);
228
+ w.writeAddress(owner);
229
+ return w;
230
+ }
231
+
232
+ @method(
233
+ { name: 'from', type: ABIDataTypes.ADDRESS },
234
+ { name: 'to', type: ABIDataTypes.ADDRESS },
235
+ { name: 'tokenId', type: ABIDataTypes.UINT256 },
236
+ { name: 'data', type: ABIDataTypes.BYTES },
237
+ )
238
+ @emit('Transferred')
239
+ public safeTransferFrom(calldata: Calldata): BytesWriter {
240
+ const from = calldata.readAddress();
241
+ const to = calldata.readAddress();
242
+ const tokenId = calldata.readU256();
243
+ const data = calldata.readBytesWithLength();
244
+
245
+ // All state changes happen before external call
246
+ this._transfer(from, to, tokenId);
247
+
248
+ // External call happens after all state changes
249
+ if (Blockchain.isContract(to)) {
250
+ this._checkOnOP721Received(from, to, tokenId, data);
251
+ }
252
+
253
+ return new BytesWriter(0);
254
+ }
255
+
256
+ @method(
257
+ { name: 'from', type: ABIDataTypes.ADDRESS },
258
+ { name: 'to', type: ABIDataTypes.ADDRESS },
259
+ { name: 'tokenId', type: ABIDataTypes.UINT256 },
260
+ )
261
+ @emit('Transferred')
262
+ public transferFrom(calldata: Calldata): BytesWriter {
263
+ const from = calldata.readAddress();
264
+ const to = calldata.readAddress();
265
+ const tokenId = calldata.readU256();
266
+
267
+ this._transfer(from, to, tokenId);
268
+
269
+ return new BytesWriter(0);
270
+ }
271
+
272
+ @method(
273
+ { name: 'to', type: ABIDataTypes.ADDRESS },
274
+ { name: 'tokenId', type: ABIDataTypes.UINT256 },
275
+ )
276
+ @emit('Approved')
277
+ public approve(calldata: Calldata): BytesWriter {
278
+ const to = calldata.readAddress();
279
+ const tokenId = calldata.readU256();
280
+
281
+ // Validate to address
282
+ if (to === Address.zero()) throw new Revert('Cannot approve to zero address');
283
+
284
+ const owner = this._ownerOf(tokenId);
285
+ if (to === owner) throw new Revert('Approval to current owner');
286
+
287
+ if (
288
+ owner !== Blockchain.tx.sender &&
289
+ !this._isApprovedForAll(owner, Blockchain.tx.sender)
290
+ ) {
291
+ throw new Revert('Not authorized to approve');
292
+ }
293
+
294
+ this._approve(to, tokenId);
295
+
296
+ return new BytesWriter(0);
297
+ }
298
+
299
+ @method({ name: 'tokenId', type: ABIDataTypes.UINT256 })
300
+ @returns({ name: 'approved', type: ABIDataTypes.ADDRESS })
301
+ public getApproved(calldata: Calldata): BytesWriter {
302
+ const tokenId = calldata.readU256();
303
+ if (!this._exists(tokenId)) throw new Revert('Token does not exist');
304
+
305
+ const approved = this._addressFromU256(this.tokenApprovalMap.get(tokenId));
306
+ const w = new BytesWriter(ADDRESS_BYTE_LENGTH);
307
+ w.writeAddress(approved);
308
+ return w;
309
+ }
310
+
311
+ @method(
312
+ { name: 'operator', type: ABIDataTypes.ADDRESS },
313
+ { name: 'approved', type: ABIDataTypes.BOOL },
314
+ )
315
+ @emit('ApprovedForAll')
316
+ public setApprovalForAll(calldata: Calldata): BytesWriter {
317
+ const operator = calldata.readAddress();
318
+ const approved = calldata.readBoolean();
319
+
320
+ if (operator === Blockchain.tx.sender) throw new Revert('Cannot approve self');
321
+
322
+ this._setApprovalForAll(Blockchain.tx.sender, operator, approved);
323
+
324
+ return new BytesWriter(0);
325
+ }
326
+
327
+ @method(
328
+ { name: 'owner', type: ABIDataTypes.ADDRESS },
329
+ { name: 'operator', type: ABIDataTypes.ADDRESS },
330
+ )
331
+ @returns({ name: 'approved', type: ABIDataTypes.BOOL })
332
+ public isApprovedForAll(calldata: Calldata): BytesWriter {
333
+ const owner = calldata.readAddress();
334
+ const operator = calldata.readAddress();
335
+
336
+ const approved: boolean = this._isApprovedForAll(owner, operator);
337
+ const w = new BytesWriter(U8_BYTE_LENGTH);
338
+ w.writeBoolean(approved);
339
+ return w;
340
+ }
341
+
342
+ @method(
343
+ { name: 'owner', type: ABIDataTypes.ADDRESS },
344
+ { name: 'to', type: ABIDataTypes.ADDRESS },
345
+ { name: 'tokenId', type: ABIDataTypes.UINT256 },
346
+ { name: 'deadline', type: ABIDataTypes.UINT64 },
347
+ { name: 'signature', type: ABIDataTypes.BYTES },
348
+ )
349
+ @emit('Transferred')
350
+ public transferBySignature(calldata: Calldata): BytesWriter {
351
+ const owner = calldata.readAddress();
352
+ const to = calldata.readAddress();
353
+ const tokenId = calldata.readU256();
354
+ const deadline = calldata.readU64();
355
+ const signature = calldata.readBytesWithLength();
356
+
357
+ this._verifyTransferSignature(owner, to, tokenId, deadline, signature);
358
+ this._transfer(owner, to, tokenId);
359
+
360
+ return new BytesWriter(0);
361
+ }
362
+
363
+ @method(
364
+ { name: 'owner', type: ABIDataTypes.ADDRESS },
365
+ { name: 'spender', type: ABIDataTypes.ADDRESS },
366
+ { name: 'tokenId', type: ABIDataTypes.UINT256 },
367
+ { name: 'deadline', type: ABIDataTypes.UINT64 },
368
+ { name: 'signature', type: ABIDataTypes.BYTES },
369
+ )
370
+ @emit('Approved')
371
+ public approveBySignature(calldata: Calldata): BytesWriter {
372
+ const owner = calldata.readAddress();
373
+ const spender = calldata.readAddress();
374
+ const tokenId = calldata.readU256();
375
+ const deadline = calldata.readU64();
376
+ const signature = calldata.readBytesWithLength();
377
+
378
+ // Verify ownership
379
+ const tokenOwner = this._ownerOf(tokenId);
380
+ if (tokenOwner !== owner) throw new Revert('Not token owner');
381
+
382
+ this._verifyApproveSignature(owner, spender, tokenId, deadline, signature);
383
+
384
+ this._approve(spender, tokenId);
385
+
386
+ return new BytesWriter(0);
387
+ }
388
+
389
+ @method({ name: 'tokenId', type: ABIDataTypes.UINT256 })
390
+ @emit('Transferred')
391
+ public burn(calldata: Calldata): BytesWriter {
392
+ const tokenId = calldata.readU256();
393
+ this._burn(tokenId);
394
+ return new BytesWriter(0);
395
+ }
396
+
397
+ @method()
398
+ @returns({ name: 'domainSeparator', type: ABIDataTypes.BYTES32 })
399
+ public domainSeparator(_: Calldata): BytesWriter {
400
+ const w = new BytesWriter(32);
401
+ w.writeBytes(this._buildDomainSeparator());
402
+ return w;
403
+ }
404
+
405
+ @method(
406
+ { name: 'owner', type: ABIDataTypes.ADDRESS },
407
+ { name: 'index', type: ABIDataTypes.UINT256 },
408
+ )
409
+ @returns({ name: 'tokenId', type: ABIDataTypes.UINT256 })
410
+ public tokenOfOwnerByIndex(calldata: Calldata): BytesWriter {
411
+ const owner = calldata.readAddress();
412
+ const index = calldata.readU256();
413
+
414
+ const balance = this._balanceOf(owner);
415
+ if (index >= balance) throw new Revert('Index out of bounds');
416
+
417
+ const tokenArray = this._getOwnerTokenArray(owner);
418
+ const tokenId = tokenArray.get(index.toU32());
419
+
420
+ const w = new BytesWriter(U256_BYTE_LENGTH);
421
+ w.writeU256(tokenId);
422
+ return w;
423
+ }
424
+
425
+ @method({ name: 'owner', type: ABIDataTypes.ADDRESS })
426
+ @returns({ name: 'nonce', type: ABIDataTypes.UINT256 })
427
+ public getTransferNonce(calldata: Calldata): BytesWriter {
428
+ const owner = calldata.readAddress();
429
+ const nonce = this._transferNonceMap.get(owner);
430
+ const w = new BytesWriter(U256_BYTE_LENGTH);
431
+ w.writeU256(nonce);
432
+ return w;
433
+ }
434
+
435
+ @method({ name: 'owner', type: ABIDataTypes.ADDRESS })
436
+ @returns({ name: 'nonce', type: ABIDataTypes.UINT256 })
437
+ public getApproveNonce(calldata: Calldata): BytesWriter {
438
+ const owner = calldata.readAddress();
439
+ const nonce = this._approveNonceMap.get(owner);
440
+ const w = new BytesWriter(U256_BYTE_LENGTH);
441
+ w.writeU256(nonce);
442
+ return w;
443
+ }
444
+
445
+ @method({ name: 'baseURI', type: ABIDataTypes.STRING })
446
+ @emit('URI')
447
+ public setBaseURI(calldata: Calldata): BytesWriter {
448
+ this.onlyDeployer(Blockchain.tx.sender);
449
+
450
+ const baseURI: string = calldata.readStringWithLength();
451
+
452
+ if (baseURI.length == 0) throw new Revert('Base URI cannot be empty');
453
+ if (<u32>baseURI.length > MAX_URI_LENGTH) {
454
+ throw new Revert('Base URI exceeds maximum length');
455
+ }
456
+
457
+ this._setBaseURI(baseURI);
458
+
459
+ return new BytesWriter(0);
460
+ }
461
+
462
+ protected _mint(to: Address, tokenId: u256): void {
463
+ if (to === Address.zero() || to === Address.dead()) {
464
+ throw new Revert('Cannot mint to zero address');
465
+ }
466
+ if (this._exists(tokenId)) {
467
+ throw new Revert('Token already exists');
468
+ }
469
+ if (!this._maxSupply.value.isZero() && this._totalSupply.value >= this._maxSupply.value) {
470
+ throw new Revert('Max supply reached');
471
+ }
472
+
473
+ // Set owner
474
+ this.ownerOfMap.set(tokenId, this._u256FromAddress(to));
475
+
476
+ // Add to enumeration
477
+ this._addTokenToOwnerEnumeration(to, tokenId);
478
+
479
+ // Update balance
480
+ const currentBalance = this.balanceOfMap.get(to);
481
+ this.balanceOfMap.set(to, SafeMath.add(currentBalance, u256.One));
482
+
483
+ // Update total supply
484
+ this._totalSupply.value = SafeMath.add(this._totalSupply.value, u256.One);
485
+
486
+ this.createTransferEvent(Address.zero(), to, tokenId);
487
+ }
488
+
489
+ protected _burn(tokenId: u256): void {
490
+ const owner = this._ownerOf(tokenId);
491
+
492
+ // Check authorization
493
+ if (
494
+ owner !== Blockchain.tx.sender &&
495
+ !this._isApprovedForAll(owner, Blockchain.tx.sender)
496
+ ) {
497
+ const approved = this._addressFromU256(this.tokenApprovalMap.get(tokenId));
498
+ if (approved !== Blockchain.tx.sender) {
499
+ throw new Revert('Not authorized to burn');
500
+ }
501
+ }
502
+
503
+ // Clear approvals
504
+ this.tokenApprovalMap.delete(tokenId);
505
+
506
+ // Remove from enumeration
507
+ this._removeTokenFromOwnerEnumeration(owner, tokenId);
508
+
509
+ // Update balance
510
+ const currentBalance = this.balanceOfMap.get(owner);
511
+ this.balanceOfMap.set(owner, SafeMath.sub(currentBalance, u256.One));
512
+
513
+ // Remove owner
514
+ this.ownerOfMap.delete(tokenId);
515
+
516
+ // Clear custom URI if exists
517
+ const uriIndex = this.tokenURIIndices.get(tokenId);
518
+ if (!uriIndex.isZero()) {
519
+ this.tokenURIIndices.delete(tokenId);
520
+ }
521
+
522
+ // Update total supply
523
+ this._totalSupply.value = SafeMath.sub(this._totalSupply.value, u256.One);
524
+
525
+ this.createTransferEvent(owner, Address.zero(), tokenId);
526
+ }
527
+
528
+ protected _transfer(from: Address, to: Address, tokenId: u256): void {
529
+ // Skip self-transfers
530
+ if (from === to) return;
531
+
532
+ const owner = this._ownerOf(tokenId);
533
+
534
+ if (owner !== from) {
535
+ throw new Revert('Transfer from incorrect owner');
536
+ }
537
+
538
+ if (to === Address.zero() || to === Address.dead()) {
539
+ throw new Revert('Transfer to zero address');
540
+ }
541
+
542
+ // Check authorization
543
+ const sender = Blockchain.tx.sender;
544
+ if (sender !== from && !this._isApprovedForAll(from, sender)) {
545
+ const approved = this._addressFromU256(this.tokenApprovalMap.get(tokenId));
546
+ if (approved !== sender) {
547
+ throw new Revert('Not authorized to transfer');
548
+ }
549
+ }
550
+
551
+ // Clear approval
552
+ this.tokenApprovalMap.delete(tokenId);
553
+
554
+ // Remove from old owner enumeration
555
+ this._removeTokenFromOwnerEnumeration(from, tokenId);
556
+
557
+ // Add to new owner enumeration
558
+ this._addTokenToOwnerEnumeration(to, tokenId);
559
+
560
+ // Update balances
561
+ const fromBalance = this.balanceOfMap.get(from);
562
+ this.balanceOfMap.set(from, SafeMath.sub(fromBalance, u256.One));
563
+
564
+ const toBalance = this.balanceOfMap.get(to);
565
+ this.balanceOfMap.set(to, SafeMath.add(toBalance, u256.One));
566
+
567
+ // Transfer ownership
568
+ this.ownerOfMap.set(tokenId, this._u256FromAddress(to));
569
+
570
+ this.createTransferEvent(from, to, tokenId);
571
+ }
572
+
573
+ protected _approve(to: Address, tokenId: u256): void {
574
+ this.tokenApprovalMap.set(tokenId, this._u256FromAddress(to));
575
+ const owner = this._ownerOf(tokenId);
576
+ this.createApprovedEvent(owner, to, tokenId);
577
+ }
578
+
579
+ protected _setApprovalForAll(owner: Address, operator: Address, approved: boolean): void {
580
+ const operatorMap = this.operatorApprovalMap.get(owner);
581
+ operatorMap.set(operator, approved ? u256.One : u256.Zero);
582
+ this.operatorApprovalMap.set(owner, operatorMap);
583
+
584
+ this.createApprovedForAllEvent(owner, operator, approved);
585
+ }
586
+
587
+ protected _isApprovedForAll(owner: Address, operator: Address): boolean {
588
+ const operatorMap = this.operatorApprovalMap.get(owner);
589
+ const approval = operatorMap.get(operator);
590
+ return !approval.isZero();
591
+ }
592
+
593
+ protected _exists(tokenId: u256): bool {
594
+ const owner = this.ownerOfMap.get(tokenId);
595
+ return !owner.isZero();
596
+ }
597
+
598
+ protected _ownerOf(tokenId: u256): Address {
599
+ const ownerU256 = this.ownerOfMap.get(tokenId);
600
+ if (ownerU256.isZero()) {
601
+ throw new Revert('Token does not exist');
602
+ }
603
+ return this._addressFromU256(ownerU256);
604
+ }
605
+
606
+ protected _balanceOf(owner: Address): u256 {
607
+ if (owner === Address.zero() || owner === Address.dead()) {
608
+ throw new Revert('Invalid address');
609
+ }
610
+ return this.balanceOfMap.get(owner);
611
+ }
612
+
613
+ protected _setTokenURI(tokenId: u256, uri: string): void {
614
+ if (!this._exists(tokenId)) throw new Revert('Token does not exist');
615
+
616
+ if (uri.length > MAX_URI_LENGTH) {
617
+ throw new Revert('URI exceeds maximum length');
618
+ }
619
+
620
+ // Use incremental counter for URI storage
621
+ const currentIndex = this._tokenURICounter.value.toU32();
622
+ const uriStorage = new StoredString(tokenURIMapPointer, currentIndex);
623
+ uriStorage.value = uri;
624
+
625
+ // Store index reference
626
+ this.tokenURIIndices.set(tokenId, u256.fromU32(currentIndex));
627
+
628
+ // Increment counter for next URI
629
+ this._tokenURICounter.value = SafeMath.add(this._tokenURICounter.value, u256.One);
630
+
631
+ // Cache in memory
632
+ this.tokenURIStorage.set(currentIndex, uriStorage);
633
+
634
+ this.emitEvent(new URIEvent(uri, tokenId));
635
+ }
636
+
637
+ protected _checkOnOP721Received(
638
+ from: Address,
639
+ to: Address,
640
+ tokenId: u256,
641
+ data: Uint8Array,
642
+ ): void {
643
+ const calldata = new BytesWriter(
644
+ SELECTOR_BYTE_LENGTH +
645
+ ADDRESS_BYTE_LENGTH * 2 +
646
+ U256_BYTE_LENGTH +
647
+ U32_BYTE_LENGTH +
648
+ data.length,
649
+ );
650
+ calldata.writeSelector(ON_OP721_RECEIVED_SELECTOR);
651
+ calldata.writeAddress(Blockchain.tx.sender);
652
+ calldata.writeAddress(from);
653
+ calldata.writeU256(tokenId);
654
+ calldata.writeBytesWithLength(data);
655
+
656
+ const response = Blockchain.call(to, calldata);
657
+ if (response.data.byteLength < SELECTOR_BYTE_LENGTH) {
658
+ throw new Revert('Transfer rejected by recipient');
659
+ }
660
+
661
+ const retVal = response.data.readSelector();
662
+ if (retVal !== ON_OP721_RECEIVED_SELECTOR) {
663
+ throw new Revert('Transfer rejected by recipient');
664
+ }
665
+ }
666
+
667
+ protected _verifyTransferSignature(
668
+ owner: Address,
669
+ to: Address,
670
+ tokenId: u256,
671
+ deadline: u64,
672
+ signature: Uint8Array,
673
+ ): void {
674
+ if (signature.length !== 64) {
675
+ throw new Revert('Invalid signature length');
676
+ }
677
+ if (Blockchain.block.number > deadline) {
678
+ throw new Revert('Signature expired');
679
+ }
680
+
681
+ const nonce = this._transferNonceMap.get(owner);
682
+
683
+ const structWriter = new BytesWriter(
684
+ 32 + ADDRESS_BYTE_LENGTH * 2 + U256_BYTE_LENGTH * 2 + U64_BYTE_LENGTH,
685
+ );
686
+ structWriter.writeBytesU8Array(OP721_TRANSFER_TYPE_HASH);
687
+ structWriter.writeAddress(owner);
688
+ structWriter.writeAddress(to);
689
+ structWriter.writeU256(tokenId);
690
+ structWriter.writeU256(nonce);
691
+ structWriter.writeU64(deadline);
692
+
693
+ const structHash = sha256(structWriter.getBuffer());
694
+
695
+ const messageWriter = new BytesWriter(2 + 32 + 32);
696
+ messageWriter.writeU16(0x1901);
697
+ messageWriter.writeBytes(this._buildDomainSeparator());
698
+ messageWriter.writeBytes(structHash);
699
+
700
+ const hash = sha256(messageWriter.getBuffer());
701
+
702
+ if (!Blockchain.verifySchnorrSignature(owner, signature, hash)) {
703
+ throw new Revert('Invalid signature');
704
+ }
705
+
706
+ this._transferNonceMap.set(owner, SafeMath.add(nonce, u256.One));
707
+ }
708
+
709
+ protected _verifyApproveSignature(
710
+ owner: Address,
711
+ spender: Address,
712
+ tokenId: u256,
713
+ deadline: u64,
714
+ signature: Uint8Array,
715
+ ): void {
716
+ if (signature.length !== 64) {
717
+ throw new Revert('Invalid signature length');
718
+ }
719
+ if (Blockchain.block.number > deadline) {
720
+ throw new Revert('Signature expired');
721
+ }
722
+
723
+ const nonce = this._approveNonceMap.get(owner);
724
+
725
+ const structWriter = new BytesWriter(
726
+ 32 + ADDRESS_BYTE_LENGTH * 2 + U256_BYTE_LENGTH * 2 + U64_BYTE_LENGTH,
727
+ );
728
+ structWriter.writeBytesU8Array(OP721_APPROVE_TYPE_HASH);
729
+ structWriter.writeAddress(owner);
730
+ structWriter.writeAddress(spender);
731
+ structWriter.writeU256(tokenId);
732
+ structWriter.writeU256(nonce);
733
+ structWriter.writeU64(deadline);
734
+
735
+ const structHash = sha256(structWriter.getBuffer());
736
+
737
+ const messageWriter = new BytesWriter(2 + 32 + 32);
738
+ messageWriter.writeU16(0x1901);
739
+ messageWriter.writeBytes(this._buildDomainSeparator());
740
+ messageWriter.writeBytes(structHash);
741
+
742
+ const hash = sha256(messageWriter.getBuffer());
743
+
744
+ if (!Blockchain.verifySchnorrSignature(owner, signature, hash)) {
745
+ throw new Revert('Invalid signature');
746
+ }
747
+
748
+ this._approveNonceMap.set(owner, SafeMath.add(nonce, u256.One));
749
+ }
750
+
751
+ protected _setBaseURI(baseURI: string): void {
752
+ this._baseURI.value = baseURI;
753
+ }
754
+
755
+ protected _buildDomainSeparator(): Uint8Array {
756
+ const writer = new BytesWriter(32 * 5 + ADDRESS_BYTE_LENGTH);
757
+ writer.writeBytesU8Array(OP712_DOMAIN_TYPE_HASH);
758
+
759
+ // Hash the name string for domain separator
760
+ const nameBytes = Uint8Array.wrap(String.UTF8.encode(this.name));
761
+ writer.writeBytes(sha256(nameBytes));
762
+
763
+ writer.writeBytesU8Array(OP712_VERSION_HASH);
764
+ writer.writeBytes(Blockchain.chainId);
765
+ writer.writeBytes(Blockchain.protocolId);
766
+ writer.writeAddress(this.address);
767
+
768
+ return sha256(writer.getBuffer());
769
+ }
770
+
771
+ // Enumeration helpers
772
+ protected _addTokenToOwnerEnumeration(to: Address, tokenId: u256): void {
773
+ const tokenArray = this._getOwnerTokenArray(to);
774
+ const newIndex = tokenArray.getLength();
775
+ tokenArray.push(tokenId);
776
+ this.tokenIndexMap.set(tokenId, u256.fromU32(newIndex));
777
+ }
778
+
779
+ protected _removeTokenFromOwnerEnumeration(from: Address, tokenId: u256): void {
780
+ const tokenArray = this._getOwnerTokenArray(from);
781
+ const arrayLength = tokenArray.getLength();
782
+
783
+ // Check for empty array
784
+ if (arrayLength == 0) {
785
+ throw new Revert('Token array is empty');
786
+ }
787
+
788
+ const lastIndex = arrayLength - 1;
789
+ const tokenIndex = this.tokenIndexMap.get(tokenId).toU32();
790
+
791
+ if (tokenIndex != lastIndex) {
792
+ // Move last token to removed token's position
793
+ const lastTokenId = tokenArray.get(lastIndex);
794
+ tokenArray.set(tokenIndex, lastTokenId);
795
+ this.tokenIndexMap.set(lastTokenId, u256.fromU32(tokenIndex));
796
+ }
797
+
798
+ // Remove last element
799
+ tokenArray.deleteLast();
800
+ this.tokenIndexMap.delete(tokenId);
801
+ }
802
+
803
+ /**
804
+ * SECURITY NOTICE:
805
+ *
806
+ * This function uses a 30-byte truncation of addresses for storage pointer generation.
807
+ * While this may appear to introduce collision risks, it is secure within the OP_NET
808
+ * protocol context because:
809
+ *
810
+ * 1. All addresses in OP_NET are tweaked public keys (32-byte elliptic curve points)
811
+ * 2. Tweaked public keys are uniformly distributed across the secp256k1 curve space
812
+ * 3. Finding two public keys with identical 30-byte prefixes (240 bits) requires
813
+ * approximately 2^120 operations due to the birthday paradox
814
+ * 4. The probability of accidentally generating colliding addresses through normal
815
+ * key generation is cryptographically negligible
816
+ *
817
+ * The truncation from 32 to 30 bytes is a space optimization that does not
818
+ * meaningfully impact security given the uniform distribution of elliptic curve points.
819
+ */
820
+ protected _getOwnerTokenArray(owner: Address): StoredU256Array {
821
+ // Truncate the 32-byte address to 30 bytes for the storage pointer
822
+ // This is safe due to the uniform distribution of tweaked public keys
823
+ const truncatedAddress = new Uint8Array(30);
824
+ for (let i: i32 = 0; i < 30; i++) {
825
+ truncatedAddress[i] = owner[i];
826
+ }
827
+
828
+ if (!this.ownerTokensMap.has(owner)) {
829
+ const array = new StoredU256Array(ownerTokensMapPointer, truncatedAddress);
830
+ this.ownerTokensMap.set(owner, array);
831
+ }
832
+
833
+ return this.ownerTokensMap.get(owner);
834
+ }
835
+
836
+ // Helper functions for 32-byte address conversions
837
+ protected _u256FromAddress(addr: Address): u256 {
838
+ // OP_NET addresses are already 32 bytes (tweaked public keys)
839
+ // Direct conversion from 32-byte address to u256
840
+ return u256.fromUint8ArrayBE(addr);
841
+ }
842
+
843
+ protected _addressFromU256(value: u256): Address {
844
+ // Convert u256 back to 32-byte address
845
+ const bytes = value.toUint8Array(true); // Returns 32 bytes in BE
846
+ const addr = new Address();
847
+
848
+ // Direct copy since both are 32 bytes
849
+ for (let i: i32 = 0; i < 32; i++) {
850
+ addr[i] = bytes[i];
851
+ }
852
+ return addr;
853
+ }
854
+
855
+ protected _addressToString(addr: Address): string {
856
+ let result = '0x';
857
+ // Convert all 32 bytes to hex string
858
+ for (let i: i32 = 0; i < 32; i++) {
859
+ const byte = addr[i];
860
+ const hex = byte.toString(16);
861
+ result += hex.length == 1 ? '0' + hex : hex;
862
+ }
863
+ return result;
864
+ }
865
+
866
+ // Event creation helpers
867
+ protected createTransferEvent(from: Address, to: Address, tokenId: u256): void {
868
+ this.emitEvent(new TransferredEvent(Blockchain.tx.sender, from, to, tokenId));
869
+ }
870
+
871
+ protected createApprovedEvent(owner: Address, approved: Address, tokenId: u256): void {
872
+ this.emitEvent(new ApprovedEvent(owner, approved, tokenId));
873
+ }
874
+
875
+ protected createApprovedForAllEvent(
876
+ owner: Address,
877
+ operator: Address,
878
+ approved: boolean,
879
+ ): void {
880
+ this.emitEvent(new ApprovedForAllEvent(owner, operator, approved));
881
+ }
882
+ }