@btc-vision/btc-runtime 1.10.8 → 1.10.11
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/LICENSE +190 -0
- package/README.md +258 -137
- package/SECURITY.md +226 -0
- package/docs/README.md +614 -0
- package/docs/advanced/bitcoin-scripts.md +939 -0
- package/docs/advanced/cross-contract-calls.md +579 -0
- package/docs/advanced/plugins.md +1006 -0
- package/docs/advanced/quantum-resistance.md +660 -0
- package/docs/advanced/signature-verification.md +715 -0
- package/docs/api-reference/blockchain.md +729 -0
- package/docs/api-reference/events.md +642 -0
- package/docs/api-reference/op20.md +902 -0
- package/docs/api-reference/op721.md +819 -0
- package/docs/api-reference/safe-math.md +510 -0
- package/docs/api-reference/storage.md +840 -0
- package/docs/contracts/op-net-base.md +786 -0
- package/docs/contracts/op20-token.md +687 -0
- package/docs/contracts/op20s-signatures.md +614 -0
- package/docs/contracts/op721-nft.md +785 -0
- package/docs/contracts/reentrancy-guard.md +787 -0
- package/docs/core-concepts/blockchain-environment.md +724 -0
- package/docs/core-concepts/decorators.md +466 -0
- package/docs/core-concepts/events.md +652 -0
- package/docs/core-concepts/pointers.md +391 -0
- package/docs/core-concepts/security.md +473 -0
- package/docs/core-concepts/storage-system.md +969 -0
- package/docs/examples/basic-token.md +745 -0
- package/docs/examples/nft-with-reservations.md +1440 -0
- package/docs/examples/oracle-integration.md +1212 -0
- package/docs/examples/stablecoin.md +1180 -0
- package/docs/getting-started/first-contract.md +575 -0
- package/docs/getting-started/installation.md +384 -0
- package/docs/getting-started/project-structure.md +630 -0
- package/docs/storage/memory-maps.md +764 -0
- package/docs/storage/stored-arrays.md +778 -0
- package/docs/storage/stored-maps.md +758 -0
- package/docs/storage/stored-primitives.md +655 -0
- package/docs/types/address.md +773 -0
- package/docs/types/bytes-writer-reader.md +938 -0
- package/docs/types/calldata.md +744 -0
- package/docs/types/safe-math.md +446 -0
- package/package.json +52 -27
- package/runtime/memory/MapOfMap.ts +1 -0
- package/LICENSE.md +0 -21
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
# Stored Arrays
|
|
2
|
+
|
|
3
|
+
Stored arrays persist ordered collections of values on-chain. They support push, pop, get, set, and length operations with automatic bounds checking.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {
|
|
9
|
+
StoredU256Array,
|
|
10
|
+
StoredU128Array,
|
|
11
|
+
StoredU64Array,
|
|
12
|
+
StoredAddressArray,
|
|
13
|
+
StoredBooleanArray,
|
|
14
|
+
Blockchain,
|
|
15
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
16
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
17
|
+
|
|
18
|
+
// Allocate storage pointer
|
|
19
|
+
private holdersPointer: u16 = Blockchain.nextPointer;
|
|
20
|
+
|
|
21
|
+
// Create stored array with subPointer
|
|
22
|
+
private holders: StoredAddressArray;
|
|
23
|
+
|
|
24
|
+
constructor() {
|
|
25
|
+
super();
|
|
26
|
+
this.holders = new StoredAddressArray(this.holdersPointer, EMPTY_POINTER);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Operations
|
|
30
|
+
this.holders.push(newAddress);
|
|
31
|
+
const first = this.holders.get(0);
|
|
32
|
+
const length = this.holders.getLength();
|
|
33
|
+
this.holders.deleteLast(); // removes last element
|
|
34
|
+
this.holders.save(); // commit changes to storage
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Available Types
|
|
38
|
+
|
|
39
|
+
| Type | Element Type | Description |
|
|
40
|
+
|------|-------------|-------------|
|
|
41
|
+
| `StoredU256Array` | `u256` | Array of 256-bit unsigned |
|
|
42
|
+
| `StoredU128Array` | `u128` | Array of 128-bit unsigned |
|
|
43
|
+
| `StoredU64Array` | `u64` | Array of 64-bit unsigned |
|
|
44
|
+
| `StoredU32Array` | `u32` | Array of 32-bit unsigned |
|
|
45
|
+
| `StoredU16Array` | `u16` | Array of 16-bit unsigned |
|
|
46
|
+
| `StoredU8Array` | `u8` | Array of bytes |
|
|
47
|
+
| `StoredAddressArray` | `Address` | Array of addresses |
|
|
48
|
+
| `StoredBooleanArray` | `bool` | Array of booleans |
|
|
49
|
+
|
|
50
|
+
## Storage Structure
|
|
51
|
+
|
|
52
|
+
Arrays use multiple storage slots with sequential subPointers for each element:
|
|
53
|
+
|
|
54
|
+
```mermaid
|
|
55
|
+
flowchart LR
|
|
56
|
+
subgraph instance["StoredArray Instance"]
|
|
57
|
+
A["pointer: u16<br/>e.g., 0x0005"]
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
subgraph layout["Storage Layout"]
|
|
61
|
+
B["Length Slot<br/>pointer base"]
|
|
62
|
+
C["Element 0<br/>subPointer = 0"]
|
|
63
|
+
D["Element 1<br/>subPointer = 1"]
|
|
64
|
+
E["Element 2<br/>subPointer = 2"]
|
|
65
|
+
F["Element 3<br/>subPointer = 3"]
|
|
66
|
+
G["..."]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
subgraph keys["Storage Keys (SHA256)"]
|
|
70
|
+
H["pointer + 0<br/>-> length: u64"]
|
|
71
|
+
I["pointer + 1<br/>-> element 0"]
|
|
72
|
+
J["pointer + 2<br/>-> element 1"]
|
|
73
|
+
K["pointer + 3<br/>-> element 2"]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
A --> B
|
|
77
|
+
B --> H
|
|
78
|
+
C --> I
|
|
79
|
+
D --> J
|
|
80
|
+
E --> K
|
|
81
|
+
|
|
82
|
+
H --> L[("Blockchain Storage")]
|
|
83
|
+
I --> L
|
|
84
|
+
J --> L
|
|
85
|
+
K --> L
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Size Limits
|
|
89
|
+
|
|
90
|
+
Maximum array length: **4,294,967,294 elements** (u32.MAX_VALUE - 1), though practical limits depend on gas costs.
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Check before adding
|
|
94
|
+
if (this.holders.getLength() >= MAX_ALLOWED) {
|
|
95
|
+
throw new Revert('Array full');
|
|
96
|
+
}
|
|
97
|
+
this.holders.push(newHolder);
|
|
98
|
+
this.holders.save(); // Don't forget to save!
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Operations
|
|
102
|
+
|
|
103
|
+
### Push
|
|
104
|
+
|
|
105
|
+
Add element to end. The push operation follows this flow:
|
|
106
|
+
|
|
107
|
+
```mermaid
|
|
108
|
+
---
|
|
109
|
+
config:
|
|
110
|
+
theme: dark
|
|
111
|
+
---
|
|
112
|
+
flowchart LR
|
|
113
|
+
A["array.push(value)"] --> B["Read length"]
|
|
114
|
+
B --> C{"length < 65535?"}
|
|
115
|
+
C -->|"No"| D["Throw error"]
|
|
116
|
+
C -->|"Yes"| E["Calculate key"]
|
|
117
|
+
E --> F["setStorageAt()"]
|
|
118
|
+
F --> G["Increment length"]
|
|
119
|
+
G --> H["Save length"]
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```typescript
|
|
123
|
+
// Add new element
|
|
124
|
+
this.holders.push(newHolder);
|
|
125
|
+
this.holders.save(); // Commit changes
|
|
126
|
+
|
|
127
|
+
// Length increases by 1
|
|
128
|
+
const newLength = this.holders.getLength();
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### deleteLast / shift
|
|
132
|
+
|
|
133
|
+
Remove elements from the array. `deleteLast()` removes from the end, `shift()` removes from the beginning.
|
|
134
|
+
|
|
135
|
+
```mermaid
|
|
136
|
+
---
|
|
137
|
+
config:
|
|
138
|
+
theme: dark
|
|
139
|
+
---
|
|
140
|
+
flowchart LR
|
|
141
|
+
A["array.deleteLast()"] --> B["Read length"]
|
|
142
|
+
B --> C{"length > 0?"}
|
|
143
|
+
C -->|"No"| D["Throw error"]
|
|
144
|
+
C -->|"Yes"| E["Zero last element"]
|
|
145
|
+
E --> F["Decrement length"]
|
|
146
|
+
F --> G["Mark as changed"]
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// Remove last element
|
|
151
|
+
this.holders.deleteLast();
|
|
152
|
+
this.holders.save(); // Commit changes
|
|
153
|
+
|
|
154
|
+
// Remove first element and return it
|
|
155
|
+
const removed: Address = this.holders.shift();
|
|
156
|
+
this.holders.save(); // Commit changes
|
|
157
|
+
|
|
158
|
+
// Reverts if array is empty
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Get
|
|
162
|
+
|
|
163
|
+
Read element at index:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Get element at index
|
|
167
|
+
const holder: Address = this.holders.get(index);
|
|
168
|
+
|
|
169
|
+
// Reverts if index >= length
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### Set
|
|
173
|
+
|
|
174
|
+
Write element at index:
|
|
175
|
+
|
|
176
|
+
```typescript
|
|
177
|
+
// Set element at index
|
|
178
|
+
this.holders.set(index, newValue);
|
|
179
|
+
this.holders.save(); // Commit changes
|
|
180
|
+
|
|
181
|
+
// Reverts if index >= MAX_LENGTH
|
|
182
|
+
// Can set beyond current length (use push for proper length tracking)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### Length
|
|
186
|
+
|
|
187
|
+
Get current array length:
|
|
188
|
+
|
|
189
|
+
```typescript
|
|
190
|
+
// Get length
|
|
191
|
+
const count: u32 = this.holders.getLength();
|
|
192
|
+
|
|
193
|
+
// Check if empty
|
|
194
|
+
if (this.holders.getLength() === 0) {
|
|
195
|
+
throw new Revert('No holders');
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Solidity vs OPNet Comparison
|
|
200
|
+
|
|
201
|
+
### Quick Reference Table
|
|
202
|
+
|
|
203
|
+
| Solidity Array Type | OPNet Equivalent | Elements per Slot | Default Max |
|
|
204
|
+
|---------------------|------------------|-------------------|-------------|
|
|
205
|
+
| `uint256[]` | `StoredU256Array` | 1 | u32.MAX_VALUE - 1 |
|
|
206
|
+
| `uint128[]` | `StoredU128Array` | 2 | u32.MAX_VALUE - 1 |
|
|
207
|
+
| `uint64[]` | `StoredU64Array` | 4 | u32.MAX_VALUE - 1 |
|
|
208
|
+
| `uint32[]` | `StoredU32Array` | 8 | u32.MAX_VALUE - 1 |
|
|
209
|
+
| `uint16[]` | `StoredU16Array` | 16 | u32.MAX_VALUE - 1 |
|
|
210
|
+
| `uint8[]` / `bytes` | `StoredU8Array` | 32 | u32.MAX_VALUE - 1 |
|
|
211
|
+
| `address[]` | `StoredAddressArray` | 1 | u32.MAX_VALUE - 1 |
|
|
212
|
+
| `bool[]` | `StoredBooleanArray` | 256 (bit-packed) | u32.MAX_VALUE - 1 |
|
|
213
|
+
|
|
214
|
+
### Operations Comparison
|
|
215
|
+
|
|
216
|
+
| Operation | Solidity | OPNet |
|
|
217
|
+
|-----------|----------|-------|
|
|
218
|
+
| Declare array | `address[] public holders;` | `private holders: StoredAddressArray;` |
|
|
219
|
+
| Initialize | Automatic | `this.holders = new StoredAddressArray(this.holdersPointer, EMPTY_POINTER);` |
|
|
220
|
+
| Push element | `holders.push(addr);` | `holders.push(addr); holders.save();` |
|
|
221
|
+
| Pop element | `holders.pop();` | `holders.deleteLast(); holders.save();` |
|
|
222
|
+
| Shift element | N/A | `holders.shift(); holders.save();` |
|
|
223
|
+
| Get element | `holders[i]` | `holders.get(i)` |
|
|
224
|
+
| Set element | `holders[i] = addr;` | `holders.set(i, addr); holders.save();` |
|
|
225
|
+
| Get length | `holders.length` | `holders.getLength()` |
|
|
226
|
+
| Delete at index | `delete holders[i];` | `holders.delete(i); holders.save();` |
|
|
227
|
+
| Check bounds | Runtime revert | Runtime revert |
|
|
228
|
+
| Clear array | `delete holders;` | `holders.deleteAll();` |
|
|
229
|
+
| Reset array | N/A | `holders.reset();` |
|
|
230
|
+
|
|
231
|
+
### Common Patterns
|
|
232
|
+
|
|
233
|
+
| Pattern | Solidity | OPNet |
|
|
234
|
+
|---------|----------|-------|
|
|
235
|
+
| Loop through array | `for (uint i = 0; i < arr.length; i++)` | `for (let i: u32 = 0; i < arr.getLength(); i++)` |
|
|
236
|
+
| Remove at index (swap) | `arr[i] = arr[arr.length-1]; arr.pop();` | `arr.set(i, arr.get(arr.getLength()-1)); arr.deleteLast(); arr.save();` |
|
|
237
|
+
| Check if empty | `arr.length == 0` | `arr.getLength() === 0` |
|
|
238
|
+
| Get last element | `arr[arr.length - 1]` | `arr.get(arr.getLength() - 1)` |
|
|
239
|
+
| Initialize with values | `arr = [1, 2, 3];` | Multiple `arr.push()` calls in `onDeployment`, then `arr.save()` |
|
|
240
|
+
|
|
241
|
+
### Full Example Comparison
|
|
242
|
+
|
|
243
|
+
```solidity
|
|
244
|
+
// Solidity
|
|
245
|
+
contract Registry {
|
|
246
|
+
address[] public members;
|
|
247
|
+
|
|
248
|
+
function addMember(address member) external {
|
|
249
|
+
members.push(member);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function removeMember(uint256 index) external {
|
|
253
|
+
members[index] = members[members.length - 1];
|
|
254
|
+
members.pop();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function getMemberCount() external view returns (uint256) {
|
|
258
|
+
return members.length;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// OPNet
|
|
265
|
+
@final
|
|
266
|
+
export class Registry extends OP_NET {
|
|
267
|
+
private membersPointer: u16 = Blockchain.nextPointer;
|
|
268
|
+
private members: StoredAddressArray;
|
|
269
|
+
|
|
270
|
+
constructor() {
|
|
271
|
+
super();
|
|
272
|
+
this.members = new StoredAddressArray(this.membersPointer, EMPTY_POINTER);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public addMember(calldata: Calldata): BytesWriter {
|
|
276
|
+
const member = calldata.readAddress();
|
|
277
|
+
this.members.push(member);
|
|
278
|
+
this.members.save();
|
|
279
|
+
return new BytesWriter(0);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
public removeMember(calldata: Calldata): BytesWriter {
|
|
283
|
+
const index = calldata.readU32();
|
|
284
|
+
const length = this.members.getLength();
|
|
285
|
+
|
|
286
|
+
if (index >= length) {
|
|
287
|
+
throw new Revert('Index out of bounds');
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (index < length - 1) {
|
|
291
|
+
this.members.set(index, this.members.get(length - 1));
|
|
292
|
+
}
|
|
293
|
+
this.members.deleteLast();
|
|
294
|
+
this.members.save();
|
|
295
|
+
|
|
296
|
+
return new BytesWriter(0);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
public getMemberCount(_calldata: Calldata): BytesWriter {
|
|
300
|
+
const writer = new BytesWriter(4);
|
|
301
|
+
writer.writeU32(this.members.getLength());
|
|
302
|
+
return writer;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
## Side-by-Side Code Examples
|
|
308
|
+
|
|
309
|
+
### Simple Address List
|
|
310
|
+
|
|
311
|
+
**Solidity:**
|
|
312
|
+
```solidity
|
|
313
|
+
contract AddressList {
|
|
314
|
+
address[] public addresses;
|
|
315
|
+
|
|
316
|
+
function add(address addr) external {
|
|
317
|
+
addresses.push(addr);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function remove(uint256 index) external {
|
|
321
|
+
require(index < addresses.length, "Out of bounds");
|
|
322
|
+
addresses[index] = addresses[addresses.length - 1];
|
|
323
|
+
addresses.pop();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function get(uint256 index) external view returns (address) {
|
|
327
|
+
return addresses[index];
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function count() external view returns (uint256) {
|
|
331
|
+
return addresses.length;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function contains(address addr) external view returns (bool) {
|
|
335
|
+
for (uint i = 0; i < addresses.length; i++) {
|
|
336
|
+
if (addresses[i] == addr) return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**OPNet:**
|
|
344
|
+
```typescript
|
|
345
|
+
@final
|
|
346
|
+
export class AddressList extends OP_NET {
|
|
347
|
+
private addressesPointer: u16 = Blockchain.nextPointer;
|
|
348
|
+
private addresses: StoredAddressArray;
|
|
349
|
+
|
|
350
|
+
constructor() {
|
|
351
|
+
super();
|
|
352
|
+
this.addresses = new StoredAddressArray(this.addressesPointer, EMPTY_POINTER);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
public add(calldata: Calldata): BytesWriter {
|
|
356
|
+
const addr = calldata.readAddress();
|
|
357
|
+
this.addresses.push(addr);
|
|
358
|
+
this.addresses.save();
|
|
359
|
+
return new BytesWriter(0);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
public remove(calldata: Calldata): BytesWriter {
|
|
363
|
+
const index = calldata.readU32();
|
|
364
|
+
const length = this.addresses.getLength();
|
|
365
|
+
if (index >= length) {
|
|
366
|
+
throw new Revert('Out of bounds');
|
|
367
|
+
}
|
|
368
|
+
if (index < length - 1) {
|
|
369
|
+
this.addresses.set(index, this.addresses.get(length - 1));
|
|
370
|
+
}
|
|
371
|
+
this.addresses.deleteLast();
|
|
372
|
+
this.addresses.save();
|
|
373
|
+
return new BytesWriter(0);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
public get(calldata: Calldata): BytesWriter {
|
|
377
|
+
const index = calldata.readU32();
|
|
378
|
+
const writer = new BytesWriter(32);
|
|
379
|
+
writer.writeAddress(this.addresses.get(index));
|
|
380
|
+
return writer;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
public count(_calldata: Calldata): BytesWriter {
|
|
384
|
+
const writer = new BytesWriter(4);
|
|
385
|
+
writer.writeU32(this.addresses.getLength());
|
|
386
|
+
return writer;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
public contains(calldata: Calldata): BytesWriter {
|
|
390
|
+
const addr = calldata.readAddress();
|
|
391
|
+
let found = false;
|
|
392
|
+
const length = this.addresses.getLength();
|
|
393
|
+
for (let i: u32 = 0; i < length; i++) {
|
|
394
|
+
if (this.addresses.get(i).equals(addr)) {
|
|
395
|
+
found = true;
|
|
396
|
+
break;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const writer = new BytesWriter(1);
|
|
400
|
+
writer.writeBoolean(found);
|
|
401
|
+
return writer;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
### Value Queue (FIFO-like with array)
|
|
407
|
+
|
|
408
|
+
**Solidity:**
|
|
409
|
+
```solidity
|
|
410
|
+
contract ValueQueue {
|
|
411
|
+
uint256[] public values;
|
|
412
|
+
|
|
413
|
+
function enqueue(uint256 value) external {
|
|
414
|
+
values.push(value);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Note: This is O(n) - not efficient for large queues
|
|
418
|
+
function dequeue() external returns (uint256) {
|
|
419
|
+
require(values.length > 0, "Empty queue");
|
|
420
|
+
uint256 first = values[0];
|
|
421
|
+
for (uint i = 0; i < values.length - 1; i++) {
|
|
422
|
+
values[i] = values[i + 1];
|
|
423
|
+
}
|
|
424
|
+
values.pop();
|
|
425
|
+
return first;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
function peek() external view returns (uint256) {
|
|
429
|
+
require(values.length > 0, "Empty queue");
|
|
430
|
+
return values[0];
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
function size() external view returns (uint256) {
|
|
434
|
+
return values.length;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
**OPNet:**
|
|
440
|
+
```typescript
|
|
441
|
+
@final
|
|
442
|
+
export class ValueQueue extends OP_NET {
|
|
443
|
+
private valuesPointer: u16 = Blockchain.nextPointer;
|
|
444
|
+
private values: StoredU256Array;
|
|
445
|
+
|
|
446
|
+
constructor() {
|
|
447
|
+
super();
|
|
448
|
+
this.values = new StoredU256Array(this.valuesPointer, EMPTY_POINTER);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
public enqueue(calldata: Calldata): BytesWriter {
|
|
452
|
+
const value = calldata.readU256();
|
|
453
|
+
this.values.push(value);
|
|
454
|
+
this.values.save();
|
|
455
|
+
return new BytesWriter(0);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Note: Use shift() for O(1) dequeue - it uses circular buffer with startIndex
|
|
459
|
+
public dequeue(_calldata: Calldata): BytesWriter {
|
|
460
|
+
const length = this.values.getLength();
|
|
461
|
+
if (length === 0) {
|
|
462
|
+
throw new Revert('Empty queue');
|
|
463
|
+
}
|
|
464
|
+
const first = this.values.shift(); // O(1) operation
|
|
465
|
+
this.values.save();
|
|
466
|
+
|
|
467
|
+
const writer = new BytesWriter(32);
|
|
468
|
+
writer.writeU256(first);
|
|
469
|
+
return writer;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
public peek(_calldata: Calldata): BytesWriter {
|
|
473
|
+
if (this.values.getLength() === 0) {
|
|
474
|
+
throw new Revert('Empty queue');
|
|
475
|
+
}
|
|
476
|
+
const writer = new BytesWriter(32);
|
|
477
|
+
writer.writeU256(this.values.get(0));
|
|
478
|
+
return writer;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
public size(_calldata: Calldata): BytesWriter {
|
|
482
|
+
const writer = new BytesWriter(4);
|
|
483
|
+
writer.writeU32(this.values.getLength());
|
|
484
|
+
return writer;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Common Patterns
|
|
490
|
+
|
|
491
|
+
### Iterating
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
// Forward iteration
|
|
495
|
+
const length = this.holders.getLength();
|
|
496
|
+
for (let i: u32 = 0; i < length; i++) {
|
|
497
|
+
const holder = this.holders.get(i);
|
|
498
|
+
// Process holder...
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// Batch retrieval for efficiency
|
|
502
|
+
const batchSize: u32 = 100;
|
|
503
|
+
const allValues = this.values.getAll(0, batchSize);
|
|
504
|
+
for (let i = 0; i < allValues.length; i++) {
|
|
505
|
+
// Process allValues[i]...
|
|
506
|
+
}
|
|
507
|
+
```
|
|
508
|
+
|
|
509
|
+
### Finding Elements
|
|
510
|
+
|
|
511
|
+
```typescript
|
|
512
|
+
// Find index of element
|
|
513
|
+
private indexOf(array: StoredAddressArray, target: Address): i32 {
|
|
514
|
+
const length = array.getLength();
|
|
515
|
+
for (let i: u32 = 0; i < length; i++) {
|
|
516
|
+
if (array.get(i).equals(target)) {
|
|
517
|
+
return i32(i);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return -1; // Not found
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// Check if element exists
|
|
524
|
+
private contains(array: StoredAddressArray, target: Address): bool {
|
|
525
|
+
return this.indexOf(array, target) >= 0;
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### Removing Elements
|
|
530
|
+
|
|
531
|
+
```typescript
|
|
532
|
+
// Remove at index (swap with last, then deleteLast)
|
|
533
|
+
private removeAt(array: StoredAddressArray, index: u32): void {
|
|
534
|
+
const length = array.getLength();
|
|
535
|
+
|
|
536
|
+
if (index >= length) {
|
|
537
|
+
throw new Revert('Index out of bounds');
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// If not last element, swap with last
|
|
541
|
+
if (index < length - 1) {
|
|
542
|
+
const last = array.get(length - 1);
|
|
543
|
+
array.set(index, last);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// Remove last element
|
|
547
|
+
array.deleteLast();
|
|
548
|
+
array.save();
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Remove by value
|
|
552
|
+
private removeValue(array: StoredAddressArray, value: Address): bool {
|
|
553
|
+
const idx = this.indexOf(array, value);
|
|
554
|
+
if (idx < 0) {
|
|
555
|
+
return false;
|
|
556
|
+
}
|
|
557
|
+
this.removeAt(array, u32(idx));
|
|
558
|
+
return true;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Alternative: Use delete(index) to remove at specific index
|
|
562
|
+
// This sets the element to zero but doesn't shift other elements
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
### Unique Elements Set
|
|
566
|
+
|
|
567
|
+
```typescript
|
|
568
|
+
// Add only if not present
|
|
569
|
+
private addUnique(array: StoredAddressArray, value: Address): bool {
|
|
570
|
+
if (this.contains(array, value)) {
|
|
571
|
+
return false; // Already exists
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
array.push(value);
|
|
575
|
+
array.save();
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
## Use Cases
|
|
581
|
+
|
|
582
|
+
### Token Holder Tracking
|
|
583
|
+
|
|
584
|
+
```typescript
|
|
585
|
+
@final
|
|
586
|
+
export class Token extends OP20 {
|
|
587
|
+
private holdersPointer: u16 = Blockchain.nextPointer;
|
|
588
|
+
private holders: StoredAddressArray;
|
|
589
|
+
|
|
590
|
+
constructor() {
|
|
591
|
+
super();
|
|
592
|
+
this.holders = new StoredAddressArray(this.holdersPointer, EMPTY_POINTER);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
public override _transfer(from: Address, to: Address, amount: u256): void {
|
|
596
|
+
// Track new holders
|
|
597
|
+
if (this.balanceOf(to).isZero() && !amount.isZero()) {
|
|
598
|
+
this.holders.push(to);
|
|
599
|
+
this.holders.save();
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
super._transfer(from, to, amount);
|
|
603
|
+
|
|
604
|
+
// Note: Removing holders when balance becomes zero
|
|
605
|
+
// requires additional logic (holder index mapping)
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
public getHolderCount(_calldata: Calldata): BytesWriter {
|
|
609
|
+
const writer = new BytesWriter(4);
|
|
610
|
+
writer.writeU32(this.holders.getLength());
|
|
611
|
+
return writer;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
### Order Queue
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
@final
|
|
620
|
+
export class OrderBook extends OP_NET {
|
|
621
|
+
private ordersPointer: u16 = Blockchain.nextPointer;
|
|
622
|
+
private orders: StoredU256Array;
|
|
623
|
+
|
|
624
|
+
constructor() {
|
|
625
|
+
super();
|
|
626
|
+
this.orders = new StoredU256Array(this.ordersPointer, EMPTY_POINTER);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
public addOrder(calldata: Calldata): BytesWriter {
|
|
630
|
+
const orderId = calldata.readU256();
|
|
631
|
+
this.orders.push(orderId);
|
|
632
|
+
this.orders.save();
|
|
633
|
+
return new BytesWriter(0);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
public processNextOrder(_calldata: Calldata): BytesWriter {
|
|
637
|
+
if (this.orders.getLength() === 0) {
|
|
638
|
+
throw new Revert('No orders');
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// FIFO: Use shift() to get first order (O(1) with circular buffer)
|
|
642
|
+
const orderId = this.orders.shift();
|
|
643
|
+
this.orders.save();
|
|
644
|
+
|
|
645
|
+
// Process order...
|
|
646
|
+
|
|
647
|
+
const writer = new BytesWriter(32);
|
|
648
|
+
writer.writeU256(orderId);
|
|
649
|
+
return writer;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Whitelist Management
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
@final
|
|
658
|
+
export class Whitelist extends OP_NET {
|
|
659
|
+
private addressesPointer: u16 = Blockchain.nextPointer;
|
|
660
|
+
private addresses: StoredAddressArray;
|
|
661
|
+
|
|
662
|
+
constructor() {
|
|
663
|
+
super();
|
|
664
|
+
this.addresses = new StoredAddressArray(this.addressesPointer, EMPTY_POINTER);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
public add(calldata: Calldata): BytesWriter {
|
|
668
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
669
|
+
|
|
670
|
+
const addr = calldata.readAddress();
|
|
671
|
+
|
|
672
|
+
// Check not already in list
|
|
673
|
+
const length = this.addresses.getLength();
|
|
674
|
+
for (let i: u32 = 0; i < length; i++) {
|
|
675
|
+
if (this.addresses.get(i).equals(addr)) {
|
|
676
|
+
throw new Revert('Already whitelisted');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
this.addresses.push(addr);
|
|
681
|
+
this.addresses.save();
|
|
682
|
+
return new BytesWriter(0);
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
public isWhitelisted(calldata: Calldata): BytesWriter {
|
|
686
|
+
const addr = calldata.readAddress();
|
|
687
|
+
|
|
688
|
+
let found = false;
|
|
689
|
+
const length = this.addresses.getLength();
|
|
690
|
+
for (let i: u32 = 0; i < length; i++) {
|
|
691
|
+
if (this.addresses.get(i).equals(addr)) {
|
|
692
|
+
found = true;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
const writer = new BytesWriter(1);
|
|
698
|
+
writer.writeBoolean(found);
|
|
699
|
+
return writer;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
```
|
|
703
|
+
|
|
704
|
+
## Best Practices
|
|
705
|
+
|
|
706
|
+
### 1. Limit Array Size
|
|
707
|
+
|
|
708
|
+
```typescript
|
|
709
|
+
const MAX_ARRAY_SIZE: u32 = 1000;
|
|
710
|
+
|
|
711
|
+
public addItem(calldata: Calldata): BytesWriter {
|
|
712
|
+
if (this.items.getLength() >= MAX_ARRAY_SIZE) {
|
|
713
|
+
throw new Revert('Array size limit reached');
|
|
714
|
+
}
|
|
715
|
+
this.items.push(calldata.readU256());
|
|
716
|
+
this.items.save();
|
|
717
|
+
return new BytesWriter(0);
|
|
718
|
+
}
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### 2. Cache Length in Loops
|
|
722
|
+
|
|
723
|
+
```typescript
|
|
724
|
+
// Good: Cache length
|
|
725
|
+
const length = this.items.getLength();
|
|
726
|
+
for (let i: u32 = 0; i < length; i++) {
|
|
727
|
+
// ...
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
// Even better: Use getAll() for batch operations
|
|
731
|
+
const items = this.items.getAll(0, 100);
|
|
732
|
+
for (let i = 0; i < items.length; i++) {
|
|
733
|
+
// Process items[i]
|
|
734
|
+
}
|
|
735
|
+
```
|
|
736
|
+
|
|
737
|
+
### 3. Use Maps for Lookup-Heavy Cases
|
|
738
|
+
|
|
739
|
+
If you frequently check "is X in array?", consider using a map alongside the array:
|
|
740
|
+
|
|
741
|
+
```typescript
|
|
742
|
+
private itemsPointer: u16 = Blockchain.nextPointer;
|
|
743
|
+
private itemExistsPointer: u16 = Blockchain.nextPointer;
|
|
744
|
+
|
|
745
|
+
private items: StoredU256Array;
|
|
746
|
+
private itemExists: StoredMapU256;
|
|
747
|
+
|
|
748
|
+
constructor() {
|
|
749
|
+
super();
|
|
750
|
+
this.items = new StoredU256Array(this.itemsPointer, EMPTY_POINTER);
|
|
751
|
+
this.itemExists = new StoredMapU256(this.itemExistsPointer);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
public addItem(item: u256): void {
|
|
755
|
+
if (!this.itemExists.get(item).isZero()) {
|
|
756
|
+
throw new Revert('Already exists');
|
|
757
|
+
}
|
|
758
|
+
this.items.push(item);
|
|
759
|
+
this.items.save();
|
|
760
|
+
this.itemExists.set(item, u256.One);
|
|
761
|
+
}
|
|
762
|
+
```
|
|
763
|
+
|
|
764
|
+
### 4. Always Call save() After Modifications
|
|
765
|
+
|
|
766
|
+
```typescript
|
|
767
|
+
// Multiple modifications, single save
|
|
768
|
+
this.items.push(value1);
|
|
769
|
+
this.items.push(value2);
|
|
770
|
+
this.items.set(0, value3);
|
|
771
|
+
this.items.save(); // Commit all changes at once
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
---
|
|
775
|
+
|
|
776
|
+
**Navigation:**
|
|
777
|
+
- Previous: [Stored Primitives](./stored-primitives.md)
|
|
778
|
+
- Next: [Stored Maps](./stored-maps.md)
|