@btc-vision/btc-runtime 1.9.2 → 1.9.4
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 +1 -1
- package/runtime/contracts/OP721.ts +11 -16
- package/runtime/env/BlockchainEnvironment.ts +491 -43
- package/runtime/math/abi.ts +1 -3
- package/runtime/types/SafeMath.ts +1047 -334
|
@@ -35,6 +35,10 @@ import { Network, Networks } from '../script/Networks';
|
|
|
35
35
|
|
|
36
36
|
export * from '../env/global';
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* CallResult encapsulates the outcome of a cross-contract call.
|
|
40
|
+
* Contains both a success flag and the response data, enabling try-catch patterns.
|
|
41
|
+
*/
|
|
38
42
|
@final
|
|
39
43
|
export class CallResult {
|
|
40
44
|
constructor(
|
|
@@ -43,146 +47,287 @@ export class CallResult {
|
|
|
43
47
|
) {}
|
|
44
48
|
}
|
|
45
49
|
|
|
50
|
+
/**
|
|
51
|
+
* BlockchainEnvironment - Core Runtime Environment for OP_NET Smart Contracts
|
|
52
|
+
*
|
|
53
|
+
* Provides the interface between smart contracts and the blockchain runtime,
|
|
54
|
+
* managing storage, cross-contract calls, cryptographic operations, and execution context.
|
|
55
|
+
*
|
|
56
|
+
* @module BlockchainEnvironment
|
|
57
|
+
*/
|
|
46
58
|
@final
|
|
47
59
|
export class BlockchainEnvironment {
|
|
60
|
+
/**
|
|
61
|
+
* Standard dead address for burn operations.
|
|
62
|
+
* Assets sent here are permanently unrecoverable.
|
|
63
|
+
*/
|
|
48
64
|
public readonly DEAD_ADDRESS: Address = Address.dead();
|
|
49
65
|
|
|
50
66
|
private storage: MapUint8Array = new MapUint8Array();
|
|
51
67
|
private transientStorage: MapUint8Array = new MapUint8Array();
|
|
52
68
|
private _selfContract: Potential<OP_NET> = null;
|
|
53
69
|
private _plugins: Plugin[] = [];
|
|
54
|
-
|
|
55
70
|
private _network: Networks = Networks.Unknown;
|
|
56
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Returns the current blockchain network identifier.
|
|
74
|
+
*
|
|
75
|
+
* @returns The network enum value (Mainnet, Testnet, etc.)
|
|
76
|
+
* @throws {Revert} When network is not initialized
|
|
77
|
+
*
|
|
78
|
+
* @remarks
|
|
79
|
+
* Determines address validation rules and network-specific constants.
|
|
80
|
+
*/
|
|
57
81
|
@inline
|
|
58
82
|
public get network(): Networks {
|
|
59
83
|
if (this._network === Networks.Unknown) {
|
|
60
84
|
throw new Revert('Network is required');
|
|
61
85
|
}
|
|
62
|
-
|
|
63
86
|
return this._network as Networks;
|
|
64
87
|
}
|
|
65
88
|
|
|
66
89
|
private _block: Potential<Block> = null;
|
|
67
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Provides access to current block information.
|
|
93
|
+
*
|
|
94
|
+
* @returns Block object containing hash, number, and median time
|
|
95
|
+
* @throws {Revert} When block context is not initialized
|
|
96
|
+
*
|
|
97
|
+
* @warning Block timestamps can vary ±900 seconds. Use median time for reliability.
|
|
98
|
+
* Never use block properties for randomness generation.
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* ```typescript
|
|
102
|
+
* const currentBlock = Blockchain.block;
|
|
103
|
+
* const blockNumber = currentBlock.number;
|
|
104
|
+
* const blockTime = currentBlock.medianTime;
|
|
105
|
+
* ```
|
|
106
|
+
*/
|
|
68
107
|
@inline
|
|
69
108
|
public get block(): Block {
|
|
70
109
|
if (!this._block) {
|
|
71
110
|
throw new Revert('Block is required');
|
|
72
111
|
}
|
|
73
|
-
|
|
74
112
|
return this._block as Block;
|
|
75
113
|
}
|
|
76
114
|
|
|
77
115
|
private _tx: Potential<Transaction> = null;
|
|
78
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Provides access to current transaction information.
|
|
119
|
+
*
|
|
120
|
+
* @returns Transaction object with caller, origin, and tx identifiers
|
|
121
|
+
* @throws {Revert} When transaction context is not initialized
|
|
122
|
+
*
|
|
123
|
+
* @warning tx.caller = immediate calling contract (changes in call chain)
|
|
124
|
+
* tx.origin = original transaction initiator (stays constant)
|
|
125
|
+
* Using tx.origin for authentication is usually wrong.
|
|
126
|
+
*
|
|
127
|
+
* @example
|
|
128
|
+
* ```typescript
|
|
129
|
+
* const tx = Blockchain.tx;
|
|
130
|
+
* const directCaller = tx.caller; // Who called this function
|
|
131
|
+
* const txInitiator = tx.origin; // Who started the transaction
|
|
132
|
+
* ```
|
|
133
|
+
*/
|
|
79
134
|
@inline
|
|
80
135
|
public get tx(): Transaction {
|
|
81
136
|
if (!this._tx) {
|
|
82
137
|
throw new Revert('Transaction is required');
|
|
83
138
|
}
|
|
84
|
-
|
|
85
139
|
return this._tx as Transaction;
|
|
86
140
|
}
|
|
87
141
|
|
|
88
142
|
private _contract: Potential<() => OP_NET> = null;
|
|
89
143
|
|
|
144
|
+
/**
|
|
145
|
+
* Returns the current contract instance.
|
|
146
|
+
*
|
|
147
|
+
* @returns The initialized OP_NET contract
|
|
148
|
+
*/
|
|
90
149
|
public get contract(): OP_NET {
|
|
91
150
|
return this._selfContract as OP_NET;
|
|
92
151
|
}
|
|
93
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Sets the contract factory function for lazy initialization.
|
|
155
|
+
*
|
|
156
|
+
* @param contract - Factory function that creates the contract instance
|
|
157
|
+
*/
|
|
94
158
|
public set contract(contract: () => OP_NET) {
|
|
95
159
|
this._contract = contract;
|
|
96
|
-
|
|
97
160
|
this.createContractIfNotExists();
|
|
98
161
|
}
|
|
99
162
|
|
|
100
163
|
private _nextPointer: u16 = 0;
|
|
101
164
|
|
|
165
|
+
/**
|
|
166
|
+
* Generates the next available storage pointer.
|
|
167
|
+
*
|
|
168
|
+
* @returns Unique pointer value for storage allocation
|
|
169
|
+
* @throws {Revert} When pointer space is exhausted (after 65,535 allocations)
|
|
170
|
+
*
|
|
171
|
+
* @warning Limited to 65,535 storage slots per contract.
|
|
172
|
+
* Use mappings for dynamic data to avoid exhaustion.
|
|
173
|
+
*
|
|
174
|
+
* @example
|
|
175
|
+
* ```typescript
|
|
176
|
+
* const ptr = Blockchain.nextPointer; // Gets next unique pointer
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
102
179
|
public get nextPointer(): u16 {
|
|
103
180
|
if (this._nextPointer === u16.MAX_VALUE) {
|
|
104
181
|
throw new Revert(`Out of storage pointer.`);
|
|
105
182
|
}
|
|
106
|
-
|
|
107
183
|
this._nextPointer += 1;
|
|
108
|
-
|
|
109
184
|
return this._nextPointer;
|
|
110
185
|
}
|
|
111
186
|
|
|
112
187
|
public _contractDeployer: Potential<Address> = null;
|
|
113
188
|
|
|
189
|
+
/**
|
|
190
|
+
* Returns the address that deployed this contract.
|
|
191
|
+
*
|
|
192
|
+
* @returns Deployer's address
|
|
193
|
+
* @throws {Revert} When deployer is not set
|
|
194
|
+
*
|
|
195
|
+
* @remarks
|
|
196
|
+
* Immutable after deployment. Often used for admin privileges.
|
|
197
|
+
*/
|
|
114
198
|
public get contractDeployer(): Address {
|
|
115
199
|
if (!this._contractDeployer) {
|
|
116
200
|
throw new Revert('Deployer is required');
|
|
117
201
|
}
|
|
118
|
-
|
|
119
202
|
return this._contractDeployer as Address;
|
|
120
203
|
}
|
|
121
204
|
|
|
122
205
|
public _contractAddress: Potential<Address> = null;
|
|
123
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Returns this contract's own address.
|
|
209
|
+
*
|
|
210
|
+
* @returns Current contract address
|
|
211
|
+
* @throws {Revert} When address is not initialized
|
|
212
|
+
*
|
|
213
|
+
* @example
|
|
214
|
+
* ```typescript
|
|
215
|
+
* const selfAddress = Blockchain.contractAddress;
|
|
216
|
+
* if (caller.equals(selfAddress)) {
|
|
217
|
+
* // Recursive call detected
|
|
218
|
+
* }
|
|
219
|
+
* ```
|
|
220
|
+
*/
|
|
124
221
|
public get contractAddress(): Address {
|
|
125
222
|
if (!this._contractAddress) {
|
|
126
223
|
throw new Revert('Contract address is required');
|
|
127
224
|
}
|
|
128
|
-
|
|
129
225
|
return this._contractAddress as Address;
|
|
130
226
|
}
|
|
131
227
|
|
|
132
228
|
public _chainId: Potential<Uint8Array> = null;
|
|
133
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Returns the blockchain's unique chain identifier.
|
|
232
|
+
*
|
|
233
|
+
* @returns 32-byte chain ID
|
|
234
|
+
* @throws {Revert} When chain ID is not set
|
|
235
|
+
*
|
|
236
|
+
* @remarks
|
|
237
|
+
* Used for replay protection and cross-chain message verification.
|
|
238
|
+
*/
|
|
134
239
|
public get chainId(): Uint8Array {
|
|
135
240
|
if (!this._chainId) {
|
|
136
241
|
throw new Revert('Chain id is required');
|
|
137
242
|
}
|
|
138
|
-
|
|
139
243
|
return this._chainId as Uint8Array;
|
|
140
244
|
}
|
|
141
245
|
|
|
142
246
|
public _protocolId: Potential<Uint8Array> = null;
|
|
143
247
|
|
|
248
|
+
/**
|
|
249
|
+
* Returns the protocol version identifier.
|
|
250
|
+
*
|
|
251
|
+
* @returns 32-byte protocol ID
|
|
252
|
+
* @throws {Revert} When protocol ID is not set
|
|
253
|
+
*/
|
|
144
254
|
public get protocolId(): Uint8Array {
|
|
145
255
|
if (!this._protocolId) {
|
|
146
256
|
throw new Revert('Protocol id is required');
|
|
147
257
|
}
|
|
148
|
-
|
|
149
258
|
return this._protocolId as Uint8Array;
|
|
150
259
|
}
|
|
151
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Registers a plugin to extend contract functionality.
|
|
263
|
+
*
|
|
264
|
+
* @param plugin - Plugin instance to register
|
|
265
|
+
*
|
|
266
|
+
* @remarks
|
|
267
|
+
* Plugins execute in registration order and have full access to contract state.
|
|
268
|
+
*/
|
|
152
269
|
public registerPlugin(plugin: Plugin): void {
|
|
153
270
|
this._plugins.push(plugin);
|
|
154
271
|
}
|
|
155
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Handles contract deployment initialization.
|
|
275
|
+
*
|
|
276
|
+
* @param calldata - Deployment parameters
|
|
277
|
+
*
|
|
278
|
+
* @remarks
|
|
279
|
+
* Called once during deployment. State changes here are permanent.
|
|
280
|
+
*/
|
|
156
281
|
public onDeployment(calldata: Calldata): void {
|
|
157
282
|
for (let i: i32 = 0; i < this._plugins.length; i++) {
|
|
158
283
|
const plugin = this._plugins[i];
|
|
159
|
-
|
|
160
284
|
plugin.onDeployment(calldata);
|
|
161
285
|
}
|
|
162
|
-
|
|
163
286
|
this.contract.onDeployment(calldata);
|
|
164
287
|
}
|
|
165
288
|
|
|
289
|
+
/**
|
|
290
|
+
* Pre-execution hook called before method execution.
|
|
291
|
+
*
|
|
292
|
+
* @param selector - Method selector being called
|
|
293
|
+
* @param calldata - Method parameters
|
|
294
|
+
*
|
|
295
|
+
* @remarks
|
|
296
|
+
* Used for access control, reentrancy guards, and validation.
|
|
297
|
+
*/
|
|
166
298
|
public onExecutionStarted(selector: Selector, calldata: Calldata): void {
|
|
167
299
|
for (let i: i32 = 0; i < this._plugins.length; i++) {
|
|
168
300
|
const plugin = this._plugins[i];
|
|
169
|
-
|
|
170
301
|
plugin.onExecutionStarted(selector, calldata);
|
|
171
302
|
}
|
|
172
|
-
|
|
173
303
|
this.contract.onExecutionStarted(selector, calldata);
|
|
174
304
|
}
|
|
175
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Post-execution hook called after successful method execution.
|
|
308
|
+
*
|
|
309
|
+
* @param selector - Method selector that was called
|
|
310
|
+
* @param calldata - Method parameters that were passed
|
|
311
|
+
*
|
|
312
|
+
* @remarks
|
|
313
|
+
* Only called on successful execution. Used for cleanup and events.
|
|
314
|
+
*/
|
|
176
315
|
public onExecutionCompleted(selector: Selector, calldata: Calldata): void {
|
|
177
316
|
for (let i: i32 = 0; i < this._plugins.length; i++) {
|
|
178
317
|
const plugin = this._plugins[i];
|
|
179
|
-
|
|
180
318
|
plugin.onExecutionCompleted(selector, calldata);
|
|
181
319
|
}
|
|
182
|
-
|
|
183
320
|
this.contract.onExecutionCompleted(selector, calldata);
|
|
184
321
|
}
|
|
185
322
|
|
|
323
|
+
/**
|
|
324
|
+
* Initializes the blockchain environment with runtime parameters.
|
|
325
|
+
*
|
|
326
|
+
* @param data - Encoded environment data from the runtime
|
|
327
|
+
*
|
|
328
|
+
* @remarks
|
|
329
|
+
* Called automatically by the runtime to set up execution context.
|
|
330
|
+
*/
|
|
186
331
|
public setEnvironmentVariables(data: Uint8Array): void {
|
|
187
332
|
const reader: BytesReader = new BytesReader(data);
|
|
188
333
|
|
|
@@ -199,19 +344,50 @@ export class BlockchainEnvironment {
|
|
|
199
344
|
const protocolId = reader.readBytes(32);
|
|
200
345
|
|
|
201
346
|
this._tx = new Transaction(caller, origin, txId, txHash);
|
|
202
|
-
|
|
203
347
|
this._contractDeployer = contractDeployer;
|
|
204
348
|
this._contractAddress = contractAddress;
|
|
205
349
|
this._chainId = chainId;
|
|
206
350
|
this._protocolId = protocolId;
|
|
207
|
-
|
|
208
351
|
this._network = Network.fromChainId(this.chainId);
|
|
209
|
-
|
|
210
352
|
this._block = new Block(blockHash, blockNumber, blockMedianTime);
|
|
211
353
|
|
|
212
354
|
this.createContractIfNotExists();
|
|
213
355
|
}
|
|
214
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Executes a call to another contract with configurable failure handling.
|
|
359
|
+
*
|
|
360
|
+
* @param destinationContract - Target contract address
|
|
361
|
+
* @param calldata - Encoded function call data
|
|
362
|
+
* @param stopExecutionOnFailure - Whether to revert on call failure (default: true)
|
|
363
|
+
* @returns CallResult with success flag and response data
|
|
364
|
+
*
|
|
365
|
+
* @example
|
|
366
|
+
* ```typescript
|
|
367
|
+
* // Traditional call - reverts on failure
|
|
368
|
+
* const result = Blockchain.call(tokenAddress, calldata);
|
|
369
|
+
* const balance = result.data.readU256();
|
|
370
|
+
*
|
|
371
|
+
* // Try-catch pattern - handles failure gracefully
|
|
372
|
+
* const result = Blockchain.call(unknownContract, calldata, false);
|
|
373
|
+
* if (result.success) {
|
|
374
|
+
* const data = result.data;
|
|
375
|
+
* // Process successful response
|
|
376
|
+
* } else {
|
|
377
|
+
* // Handle failure without reverting
|
|
378
|
+
* this.handleCallFailure();
|
|
379
|
+
* }
|
|
380
|
+
* ```
|
|
381
|
+
*
|
|
382
|
+
* @warning Follow checks-effects-interactions pattern to prevent reentrancy:
|
|
383
|
+
* 1. Check conditions
|
|
384
|
+
* 2. Update state
|
|
385
|
+
* 3. Make external call
|
|
386
|
+
*
|
|
387
|
+
* @remarks
|
|
388
|
+
* The stopExecutionOnFailure parameter enables try-catch style error handling.
|
|
389
|
+
* When false, failed calls return success=false instead of reverting.
|
|
390
|
+
*/
|
|
215
391
|
public call(
|
|
216
392
|
destinationContract: Address,
|
|
217
393
|
calldata: BytesWriter,
|
|
@@ -241,6 +417,22 @@ export class BlockchainEnvironment {
|
|
|
241
417
|
return new CallResult(status === 0, new BytesReader(Uint8Array.wrap(resultBuffer)));
|
|
242
418
|
}
|
|
243
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Emits a log message for debugging.
|
|
422
|
+
*
|
|
423
|
+
* @param data - String message to log
|
|
424
|
+
*
|
|
425
|
+
* @warning ONLY AVAILABLE IN UNIT TESTING FRAMEWORK.
|
|
426
|
+
* NOT available in production or testnet environments.
|
|
427
|
+
* Will fail if called outside of testing context.
|
|
428
|
+
*
|
|
429
|
+
* @example
|
|
430
|
+
* ```typescript
|
|
431
|
+
* // Only in unit tests:
|
|
432
|
+
* Blockchain.log("Debug: Transfer initiated");
|
|
433
|
+
* Blockchain.log(`Amount: ${amount.toString()}`);
|
|
434
|
+
* ```
|
|
435
|
+
*/
|
|
244
436
|
public log(data: string): void {
|
|
245
437
|
const writer = new BytesWriter(String.UTF8.byteLength(data));
|
|
246
438
|
writer.writeString(data);
|
|
@@ -249,6 +441,30 @@ export class BlockchainEnvironment {
|
|
|
249
441
|
log(buffer.buffer, buffer.length);
|
|
250
442
|
}
|
|
251
443
|
|
|
444
|
+
/**
|
|
445
|
+
* Emits a structured event for off-chain monitoring.
|
|
446
|
+
*
|
|
447
|
+
* @param event - NetEvent instance containing event data
|
|
448
|
+
*
|
|
449
|
+
* @remarks
|
|
450
|
+
* Events are the primary mechanism for dApps to track state changes.
|
|
451
|
+
* Events are not accessible within contracts.
|
|
452
|
+
*
|
|
453
|
+
* @example
|
|
454
|
+
* ```typescript
|
|
455
|
+
* class TransferredEvent extends NetEvent {
|
|
456
|
+
* constructor(operator: Address, from: Address, to: Address, amount: u256) {
|
|
457
|
+
* const data: BytesWriter = new BytesWriter(ADDRESS_BYTE_LENGTH * 3 + U256_BYTE_LENGTH);
|
|
458
|
+
* data.writeAddress(operator);
|
|
459
|
+
* data.writeAddress(from);
|
|
460
|
+
* data.writeAddress(to);
|
|
461
|
+
* data.writeU256(amount);
|
|
462
|
+
* super('Transferred', data);
|
|
463
|
+
* }
|
|
464
|
+
* }
|
|
465
|
+
* Blockchain.emit(new TransferredEvent(operator, sender, recipient, value));
|
|
466
|
+
* ```
|
|
467
|
+
*/
|
|
252
468
|
public emit(event: NetEvent): void {
|
|
253
469
|
const data = event.getEventData();
|
|
254
470
|
const writer = new BytesWriter(
|
|
@@ -261,15 +477,54 @@ export class BlockchainEnvironment {
|
|
|
261
477
|
emit(writer.getBuffer().buffer, writer.bufferLength());
|
|
262
478
|
}
|
|
263
479
|
|
|
480
|
+
/**
|
|
481
|
+
* Validates a Bitcoin address format for the current network.
|
|
482
|
+
*
|
|
483
|
+
* @param address - Bitcoin address string to validate
|
|
484
|
+
* @returns true if valid for current network, false otherwise
|
|
485
|
+
*
|
|
486
|
+
* @warning Validation rules are network-specific:
|
|
487
|
+
* - Mainnet: bc1, 1, 3 prefixes
|
|
488
|
+
* - Testnet: tb1, m, n, 2 prefixes
|
|
489
|
+
*
|
|
490
|
+
* @example
|
|
491
|
+
* ```typescript
|
|
492
|
+
* if (!Blockchain.validateBitcoinAddress(userAddress)) {
|
|
493
|
+
* throw new Revert("Invalid Bitcoin address");
|
|
494
|
+
* }
|
|
495
|
+
* ```
|
|
496
|
+
*/
|
|
264
497
|
public validateBitcoinAddress(address: string): bool {
|
|
265
498
|
const writer = new BytesWriter(String.UTF8.byteLength(address));
|
|
266
499
|
writer.writeString(address);
|
|
267
500
|
|
|
268
501
|
const result = validateBitcoinAddress(writer.getBuffer().buffer, address.length);
|
|
269
|
-
|
|
270
502
|
return result === 1;
|
|
271
503
|
}
|
|
272
504
|
|
|
505
|
+
/**
|
|
506
|
+
* Deploys a new contract using an existing contract as template.
|
|
507
|
+
*
|
|
508
|
+
* @param existingAddress - Template contract address
|
|
509
|
+
* @param salt - Unique salt for deterministic addressing
|
|
510
|
+
* @param calldata - Constructor parameters
|
|
511
|
+
* @returns Address of newly deployed contract
|
|
512
|
+
* @throws {Revert} When deployment fails
|
|
513
|
+
*
|
|
514
|
+
* @warning CREATE2 style deployment:
|
|
515
|
+
* Same salt + template = same address.
|
|
516
|
+
* Salt collision will cause deployment to fail.
|
|
517
|
+
*
|
|
518
|
+
* @example
|
|
519
|
+
* ```typescript
|
|
520
|
+
* const salt = u256.fromBytes(sha256("unique-id"));
|
|
521
|
+
* const newToken = Blockchain.deployContractFromExisting(
|
|
522
|
+
* templateAddress,
|
|
523
|
+
* salt,
|
|
524
|
+
* constructorData
|
|
525
|
+
* );
|
|
526
|
+
* ```
|
|
527
|
+
*/
|
|
273
528
|
public deployContractFromExisting(
|
|
274
529
|
existingAddress: Address,
|
|
275
530
|
salt: u256,
|
|
@@ -286,7 +541,6 @@ export class BlockchainEnvironment {
|
|
|
286
541
|
resultAddressBuffer,
|
|
287
542
|
);
|
|
288
543
|
|
|
289
|
-
// TODO: Decode revert data if status is not 0
|
|
290
544
|
if (status !== 0) {
|
|
291
545
|
throw new Revert('Failed to deploy contract');
|
|
292
546
|
}
|
|
@@ -295,6 +549,21 @@ export class BlockchainEnvironment {
|
|
|
295
549
|
return contractAddressReader.readAddress();
|
|
296
550
|
}
|
|
297
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Reads a value from persistent storage.
|
|
554
|
+
*
|
|
555
|
+
* @param pointerHash - 32-byte storage key
|
|
556
|
+
* @returns 32-byte stored value (zeros if unset)
|
|
557
|
+
*
|
|
558
|
+
* @warning Cannot distinguish between unset and explicitly set to zero.
|
|
559
|
+
* Use hasStorageAt() to check existence.
|
|
560
|
+
*
|
|
561
|
+
* @example
|
|
562
|
+
* ```typescript
|
|
563
|
+
* const key = sha256("balance:" + address);
|
|
564
|
+
* const balance = Blockchain.getStorageAt(key);
|
|
565
|
+
* ```
|
|
566
|
+
*/
|
|
298
567
|
public getStorageAt(pointerHash: Uint8Array): Uint8Array {
|
|
299
568
|
this.hasPointerStorageHash(pointerHash);
|
|
300
569
|
|
|
@@ -305,6 +574,27 @@ export class BlockchainEnvironment {
|
|
|
305
574
|
return new Uint8Array(32);
|
|
306
575
|
}
|
|
307
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Reads a value from transient storage (cleared after transaction).
|
|
579
|
+
*
|
|
580
|
+
* @param pointerHash - 32-byte storage key
|
|
581
|
+
* @returns 32-byte stored value (zeros if unset)
|
|
582
|
+
*
|
|
583
|
+
* @warning NOT CURRENTLY ENABLED IN PRODUCTION.
|
|
584
|
+
* Transient storage functionality is experimental and only available in testing.
|
|
585
|
+
* Will fail if called in production or testnet environments.
|
|
586
|
+
* Storage is cleared after each transaction when enabled.
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* ```typescript
|
|
590
|
+
* // Reentrancy guard pattern (when enabled)
|
|
591
|
+
* const GUARD_KEY = sha256("reentrancy");
|
|
592
|
+
* if (Blockchain.hasTransientStorageAt(GUARD_KEY)) {
|
|
593
|
+
* throw new Revert("Reentrancy detected");
|
|
594
|
+
* }
|
|
595
|
+
* Blockchain.setTransientStorageAt(GUARD_KEY, u256.One.toBytes());
|
|
596
|
+
* ```
|
|
597
|
+
*/
|
|
308
598
|
public getTransientStorageAt(pointerHash: Uint8Array): Uint8Array {
|
|
309
599
|
if (this.hasPointerTransientStorageHash(pointerHash)) {
|
|
310
600
|
return this.transientStorage.get(pointerHash);
|
|
@@ -313,54 +603,206 @@ export class BlockchainEnvironment {
|
|
|
313
603
|
return new Uint8Array(32);
|
|
314
604
|
}
|
|
315
605
|
|
|
606
|
+
/**
|
|
607
|
+
* Computes SHA-256 hash of input data.
|
|
608
|
+
*
|
|
609
|
+
* @param buffer - Data to hash
|
|
610
|
+
* @returns 32-byte hash result
|
|
611
|
+
*
|
|
612
|
+
* @example
|
|
613
|
+
* ```typescript
|
|
614
|
+
* const hash = Blockchain.sha256(data);
|
|
615
|
+
* ```
|
|
616
|
+
*/
|
|
617
|
+
@inline
|
|
316
618
|
public sha256(buffer: Uint8Array): Uint8Array {
|
|
317
619
|
return sha256(buffer);
|
|
318
620
|
}
|
|
319
621
|
|
|
622
|
+
/**
|
|
623
|
+
* Computes double SHA-256 (Bitcoin's hash256).
|
|
624
|
+
*
|
|
625
|
+
* @param buffer - Data to hash
|
|
626
|
+
* @returns 32-byte double hash result
|
|
627
|
+
*
|
|
628
|
+
* @remarks
|
|
629
|
+
* Standard Bitcoin hash function used for transaction IDs and block hashes.
|
|
630
|
+
*
|
|
631
|
+
* @example
|
|
632
|
+
* ```typescript
|
|
633
|
+
* const txHash = Blockchain.hash256(transactionData);
|
|
634
|
+
* ```
|
|
635
|
+
*/
|
|
636
|
+
@inline
|
|
320
637
|
public hash256(buffer: Uint8Array): Uint8Array {
|
|
321
638
|
return sha256(sha256(buffer));
|
|
322
639
|
}
|
|
323
640
|
|
|
641
|
+
/**
|
|
642
|
+
* Verifies a Schnorr signature (Bitcoin Taproot).
|
|
643
|
+
*
|
|
644
|
+
* @param publicKey - 32-byte public key
|
|
645
|
+
* @param signature - 64-byte Schnorr signature
|
|
646
|
+
* @param hash - 32-byte message hash
|
|
647
|
+
* @returns true if signature is valid
|
|
648
|
+
*
|
|
649
|
+
* @warning Schnorr signatures differ from ECDSA:
|
|
650
|
+
* - Linear aggregation properties
|
|
651
|
+
* - Used in Taproot (post-2021)
|
|
652
|
+
*
|
|
653
|
+
* @example
|
|
654
|
+
* ```typescript
|
|
655
|
+
* const isValid = Blockchain.verifySchnorrSignature(
|
|
656
|
+
* signer,
|
|
657
|
+
* signature,
|
|
658
|
+
* messageHash
|
|
659
|
+
* );
|
|
660
|
+
* if (!isValid) throw new Revert("Invalid signature");
|
|
661
|
+
* ```
|
|
662
|
+
*/
|
|
324
663
|
public verifySchnorrSignature(
|
|
325
664
|
publicKey: Address,
|
|
326
665
|
signature: Uint8Array,
|
|
327
666
|
hash: Uint8Array,
|
|
328
667
|
): boolean {
|
|
329
668
|
const result: u32 = verifySchnorrSignature(publicKey.buffer, signature.buffer, hash.buffer);
|
|
330
|
-
|
|
331
669
|
return result === 1;
|
|
332
670
|
}
|
|
333
671
|
|
|
672
|
+
/**
|
|
673
|
+
* Checks if an address is a contract (not an EOA).
|
|
674
|
+
*
|
|
675
|
+
* @param address - Address to check
|
|
676
|
+
* @returns true if contract, false if EOA or uninitialized
|
|
677
|
+
*
|
|
678
|
+
* @warning Cannot distinguish between EOA with no transactions
|
|
679
|
+
* and uninitialized address.
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* ```typescript
|
|
683
|
+
* if (!Blockchain.isContract(targetAddress)) {
|
|
684
|
+
* throw new Revert("Must be a contract");
|
|
685
|
+
* }
|
|
686
|
+
* ```
|
|
687
|
+
*/
|
|
688
|
+
@inline
|
|
334
689
|
public isContract(address: Address): boolean {
|
|
335
690
|
return getAccountType(address.buffer) !== 0;
|
|
336
691
|
}
|
|
337
692
|
|
|
693
|
+
/**
|
|
694
|
+
* Checks if persistent storage slot has a non-zero value.
|
|
695
|
+
*
|
|
696
|
+
* @param pointerHash - 32-byte storage key
|
|
697
|
+
* @returns true if slot contains non-zero value
|
|
698
|
+
*
|
|
699
|
+
* @warning Cannot distinguish between never written and explicitly set to zero.
|
|
700
|
+
*
|
|
701
|
+
* @example
|
|
702
|
+
* ```typescript
|
|
703
|
+
* const EXISTS_KEY = sha256("user:exists:" + address);
|
|
704
|
+
* if (!Blockchain.hasStorageAt(EXISTS_KEY)) {
|
|
705
|
+
* // First time user
|
|
706
|
+
* }
|
|
707
|
+
* ```
|
|
708
|
+
*/
|
|
338
709
|
public hasStorageAt(pointerHash: Uint8Array): bool {
|
|
339
|
-
// We mark zero as the default value for the storage, if something is 0, the storage slot get deleted or is non-existent
|
|
340
710
|
const val: Uint8Array = this.getStorageAt(pointerHash);
|
|
341
|
-
|
|
342
711
|
return !eqUint(val, EMPTY_BUFFER);
|
|
343
712
|
}
|
|
344
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Checks if transient storage slot has a non-zero value.
|
|
716
|
+
*
|
|
717
|
+
* @param pointerHash - 32-byte storage key
|
|
718
|
+
* @returns true if slot contains non-zero value
|
|
719
|
+
*
|
|
720
|
+
* @warning NOT CURRENTLY ENABLED IN PRODUCTION.
|
|
721
|
+
* Transient storage functionality is experimental and only available in testing.
|
|
722
|
+
* Will fail if called in production or testnet environments.
|
|
723
|
+
*
|
|
724
|
+
* @remarks
|
|
725
|
+
* Only reliable within single transaction context when enabled.
|
|
726
|
+
*/
|
|
345
727
|
public hasTransientStorageAt(pointerHash: Uint8Array): bool {
|
|
346
|
-
// We mark zero as the default value for the storage, if something is 0, the storage slot get deleted or is non-existent
|
|
347
728
|
const val: Uint8Array = this.getTransientStorageAt(pointerHash);
|
|
348
|
-
|
|
349
729
|
return !eqUint(val, EMPTY_BUFFER);
|
|
350
730
|
}
|
|
351
731
|
|
|
732
|
+
/**
|
|
733
|
+
* Writes a value to persistent storage.
|
|
734
|
+
*
|
|
735
|
+
* @param pointerHash - 32-byte storage key
|
|
736
|
+
* @param value - Value to store (will be padded/truncated to 32 bytes)
|
|
737
|
+
*
|
|
738
|
+
* @warning Storage writes cost significant gas (~20,000).
|
|
739
|
+
* Batch related updates when possible.
|
|
740
|
+
*
|
|
741
|
+
* @example
|
|
742
|
+
* ```typescript
|
|
743
|
+
* const key = sha256("balance:" + address);
|
|
744
|
+
* Blockchain.setStorageAt(key, balance.toBytes());
|
|
745
|
+
* ```
|
|
746
|
+
*/
|
|
747
|
+
@inline
|
|
352
748
|
public setStorageAt(pointerHash: Uint8Array, value: Uint8Array): void {
|
|
353
749
|
this._internalSetStorageAt(pointerHash, value);
|
|
354
750
|
}
|
|
355
751
|
|
|
752
|
+
/**
|
|
753
|
+
* Writes a value to transient storage.
|
|
754
|
+
*
|
|
755
|
+
* @param pointerHash - 32-byte storage key
|
|
756
|
+
* @param value - 32-byte value to store
|
|
757
|
+
* @throws {Revert} If key or value is not exactly 32 bytes
|
|
758
|
+
*
|
|
759
|
+
* @warning NOT CURRENTLY ENABLED IN PRODUCTION.
|
|
760
|
+
* Transient storage functionality is experimental and only available in testing.
|
|
761
|
+
* Will fail if called in production or testnet environments.
|
|
762
|
+
* Value must be exactly 32 bytes (no auto-padding).
|
|
763
|
+
* Storage is cleared after transaction completes when enabled.
|
|
764
|
+
*
|
|
765
|
+
* @example
|
|
766
|
+
* ```typescript
|
|
767
|
+
* // Reentrancy lock (when enabled)
|
|
768
|
+
* Blockchain.setTransientStorageAt(LOCK_KEY, u256.One.toBytes());
|
|
769
|
+
* // Lock automatically clears after transaction
|
|
770
|
+
* ```
|
|
771
|
+
*/
|
|
772
|
+
@inline
|
|
356
773
|
public setTransientStorageAt(pointerHash: Uint8Array, value: Uint8Array): void {
|
|
357
774
|
this._internalSetTransientStorageAt(pointerHash, value);
|
|
358
775
|
}
|
|
359
776
|
|
|
777
|
+
/**
|
|
778
|
+
* Gets the account type identifier for an address.
|
|
779
|
+
*
|
|
780
|
+
* @param address - Address to query
|
|
781
|
+
* @returns Account type code (0 = EOA/uninitialized, >0 = contract type)
|
|
782
|
+
*
|
|
783
|
+
* @remarks
|
|
784
|
+
* Different contract types may have different codes.
|
|
785
|
+
*/
|
|
360
786
|
public getAccountType(address: Address): u32 {
|
|
361
787
|
return getAccountType(address.buffer);
|
|
362
788
|
}
|
|
363
789
|
|
|
790
|
+
/**
|
|
791
|
+
* Retrieves a historical block hash.
|
|
792
|
+
*
|
|
793
|
+
* @param blockNumber - Block number to query
|
|
794
|
+
* @returns 32-byte block hash
|
|
795
|
+
*
|
|
796
|
+
* @warning Only recent blocks available (~256 blocks).
|
|
797
|
+
* Older blocks return zero hash.
|
|
798
|
+
* Do not use for randomness generation.
|
|
799
|
+
*
|
|
800
|
+
* @example
|
|
801
|
+
* ```typescript
|
|
802
|
+
* const oldBlock = Blockchain.block.number - 10;
|
|
803
|
+
* const hash = Blockchain.getBlockHash(oldBlock);
|
|
804
|
+
* ```
|
|
805
|
+
*/
|
|
364
806
|
public getBlockHash(blockNumber: u64): Uint8Array {
|
|
365
807
|
const hash = new ArrayBuffer(32);
|
|
366
808
|
getBlockHash(blockNumber, hash);
|
|
@@ -378,13 +820,21 @@ export class BlockchainEnvironment {
|
|
|
378
820
|
}
|
|
379
821
|
|
|
380
822
|
private _internalSetStorageAt(pointerHash: Uint8Array, value: Uint8Array): void {
|
|
381
|
-
|
|
823
|
+
if (pointerHash.buffer.byteLength !== 32) {
|
|
824
|
+
throw new Revert('Pointer must be 32 bytes long');
|
|
825
|
+
}
|
|
382
826
|
|
|
383
|
-
|
|
384
|
-
|
|
827
|
+
let finalValue: Uint8Array = value;
|
|
828
|
+
if (value.buffer.byteLength !== 32) {
|
|
829
|
+
// Auto-pad values shorter than 32 bytes
|
|
830
|
+
finalValue = new Uint8Array(32);
|
|
831
|
+
for (let i = 0; i < value.buffer.byteLength && i < 32; i++) {
|
|
832
|
+
finalValue[i] = value[i];
|
|
833
|
+
}
|
|
385
834
|
}
|
|
386
835
|
|
|
387
|
-
|
|
836
|
+
this.storage.set(pointerHash, finalValue);
|
|
837
|
+
storePointer(pointerHash.buffer, finalValue.buffer);
|
|
388
838
|
}
|
|
389
839
|
|
|
390
840
|
private _internalSetTransientStorageAt(pointerHash: Uint8Array, value: Uint8Array): void {
|
|
@@ -398,39 +848,37 @@ export class BlockchainEnvironment {
|
|
|
398
848
|
}
|
|
399
849
|
|
|
400
850
|
private hasPointerStorageHash(pointer: Uint8Array): bool {
|
|
401
|
-
if (this.storage.has(pointer)) {
|
|
402
|
-
return true;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
851
|
if (pointer.buffer.byteLength !== 32) {
|
|
406
852
|
throw new Revert('Pointer must be 32 bytes long');
|
|
407
853
|
}
|
|
408
854
|
|
|
409
|
-
|
|
855
|
+
if (this.storage.has(pointer)) {
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
|
|
410
859
|
const resultBuffer = new ArrayBuffer(32);
|
|
411
860
|
loadPointer(pointer.buffer, resultBuffer);
|
|
412
861
|
|
|
413
862
|
const value: Uint8Array = Uint8Array.wrap(resultBuffer);
|
|
414
|
-
this.storage.set(pointer, value); //
|
|
863
|
+
this.storage.set(pointer, value); // Cache for future reads
|
|
415
864
|
|
|
416
865
|
return !eqUint(value, EMPTY_BUFFER);
|
|
417
866
|
}
|
|
418
867
|
|
|
419
868
|
private hasPointerTransientStorageHash(pointer: Uint8Array): bool {
|
|
420
|
-
if (this.transientStorage.has(pointer)) {
|
|
421
|
-
return true;
|
|
422
|
-
}
|
|
423
|
-
|
|
424
869
|
if (pointer.buffer.byteLength !== 32) {
|
|
425
870
|
throw new Revert('Transient pointer must be 32 bytes long');
|
|
426
871
|
}
|
|
427
872
|
|
|
428
|
-
|
|
873
|
+
if (this.transientStorage.has(pointer)) {
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
876
|
+
|
|
429
877
|
const resultBuffer = new ArrayBuffer(32);
|
|
430
878
|
tLoadPointer(pointer.buffer, resultBuffer);
|
|
431
879
|
|
|
432
880
|
const value: Uint8Array = Uint8Array.wrap(resultBuffer);
|
|
433
|
-
this.transientStorage.set(pointer, value); //
|
|
881
|
+
this.transientStorage.set(pointer, value); // Cache for future reads
|
|
434
882
|
|
|
435
883
|
return !eqUint(value, EMPTY_BUFFER);
|
|
436
884
|
}
|