@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.
- package/package.json +13 -4
- package/runtime/constants/Exports.ts +86 -0
- package/runtime/contracts/OP1155.ts +1042 -0
- package/runtime/contracts/OP20.ts +64 -38
- package/runtime/contracts/OP721.ts +882 -0
- package/runtime/contracts/OP_NET.ts +5 -0
- package/runtime/contracts/ReentrancyGuard.ts +136 -0
- package/runtime/contracts/interfaces/IOP1155.ts +33 -0
- package/runtime/contracts/interfaces/IOP721.ts +29 -0
- package/runtime/contracts/interfaces/OP1155InitParameters.ts +11 -0
- package/runtime/contracts/interfaces/OP721InitParameters.ts +15 -0
- package/runtime/env/BlockchainEnvironment.ts +32 -3
- package/runtime/events/predefined/ApprovedForAll.ts +16 -0
- package/runtime/events/predefined/TransferredBatchEvent.ts +35 -0
- package/runtime/events/predefined/TransferredSingleEvent.ts +18 -0
- package/runtime/events/predefined/URIEvent.ts +23 -0
- package/runtime/events/predefined/index.ts +4 -0
- package/runtime/index.ts +9 -0
- package/runtime/math/abi.ts +23 -0
- package/runtime/math/bytes.ts +4 -0
- package/runtime/memory/AddressMemoryMap.ts +20 -0
- package/runtime/nested/storage/StorageMap.ts +3 -23
- package/runtime/nested/storage/StorageSet.ts +6 -3
- package/runtime/script/Script.ts +1 -1
- package/runtime/secp256k1/ECPoint.ts +3 -3
- package/runtime/shared-libraries/OP20Utils.ts +1 -2
- package/runtime/storage/AdvancedStoredString.ts +8 -189
- package/runtime/storage/BaseStoredString.ts +206 -0
- package/runtime/storage/StoredString.ts +15 -194
- package/runtime/storage/arrays/StoredPackedArray.ts +19 -5
- 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
|
+
}
|