@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,758 @@
|
|
|
1
|
+
# Stored Maps
|
|
2
|
+
|
|
3
|
+
Stored maps provide key-value storage on-chain, similar to Solidity's mappings. They support various key and value types with efficient storage access.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {
|
|
9
|
+
StoredMapU256,
|
|
10
|
+
Blockchain,
|
|
11
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
12
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
13
|
+
|
|
14
|
+
// Allocate storage pointer
|
|
15
|
+
private dataPointer: u16 = Blockchain.nextPointer;
|
|
16
|
+
|
|
17
|
+
// Create stored map
|
|
18
|
+
private data: StoredMapU256;
|
|
19
|
+
|
|
20
|
+
constructor() {
|
|
21
|
+
super();
|
|
22
|
+
this.data = new StoredMapU256(this.dataPointer);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Operations
|
|
26
|
+
this.data.set(key, value);
|
|
27
|
+
const value = this.data.get(key);
|
|
28
|
+
const exists = this.data.has(key);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## CRITICAL: Map Implementation Warning
|
|
32
|
+
|
|
33
|
+
> **DO NOT USE AssemblyScript's Built-in Map**
|
|
34
|
+
>
|
|
35
|
+
> When creating custom map implementations or extending map functionality, you **MUST** use the Map class from `@btc-vision/btc-runtime/runtime`, NOT the built-in AssemblyScript Map.
|
|
36
|
+
>
|
|
37
|
+
> **Why the AssemblyScript Map is broken for blockchain:**
|
|
38
|
+
> - NOT optimized for blockchain storage patterns
|
|
39
|
+
> - Does NOT handle Uint8Array buffers as keys correctly
|
|
40
|
+
> - Does NOT work properly with Address key comparisons
|
|
41
|
+
> - Will cause silent data corruption or key collisions
|
|
42
|
+
>
|
|
43
|
+
> **CORRECT:**
|
|
44
|
+
> ```typescript
|
|
45
|
+
> import { Map } from '@btc-vision/btc-runtime/runtime';
|
|
46
|
+
>
|
|
47
|
+
> export class MyCustomMap<V> extends Map<Address, V> {
|
|
48
|
+
> // Your implementation
|
|
49
|
+
> }
|
|
50
|
+
> ```
|
|
51
|
+
>
|
|
52
|
+
> **WRONG:**
|
|
53
|
+
> ```typescript
|
|
54
|
+
> // DO NOT DO THIS - will break!
|
|
55
|
+
> const map = new Map<Uint8Array, u256>(); // AssemblyScript Map
|
|
56
|
+
> ```
|
|
57
|
+
>
|
|
58
|
+
> The btc-runtime Map is specifically designed to:
|
|
59
|
+
> - Handle Address and Uint8Array key comparisons correctly
|
|
60
|
+
> - Optimize for blockchain storage access patterns
|
|
61
|
+
> - Support proper serialization for persistent storage
|
|
62
|
+
> - Prevent key collisions with custom equality logic
|
|
63
|
+
|
|
64
|
+
## StoredMapU256
|
|
65
|
+
|
|
66
|
+
Basic key-value map with `u256` keys and values. Each key-value pair is stored at a unique storage slot computed via SHA256:
|
|
67
|
+
|
|
68
|
+
```mermaid
|
|
69
|
+
---
|
|
70
|
+
config:
|
|
71
|
+
theme: dark
|
|
72
|
+
---
|
|
73
|
+
flowchart LR
|
|
74
|
+
subgraph instance["StoredMapU256 Instance"]
|
|
75
|
+
A["pointer: u16"]
|
|
76
|
+
B["Storage Operations"]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
subgraph kvstore["Key-Value Storage"]
|
|
80
|
+
C["key1: u256"] --> D["value1: u256"]
|
|
81
|
+
E["key2: u256"] --> F["value2: u256"]
|
|
82
|
+
G["key3: u256"] --> H["value3: u256"]
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
subgraph keycalc["Storage Key Calculation (SHA256)"]
|
|
86
|
+
I["map.pointer"]
|
|
87
|
+
J["user key (u256)"]
|
|
88
|
+
K["SHA256(pointer + key)"]
|
|
89
|
+
L["32-byte storage key"]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
A --> I
|
|
93
|
+
B --> K
|
|
94
|
+
J --> K
|
|
95
|
+
K --> L
|
|
96
|
+
L --> M[("Blockchain Storage")]
|
|
97
|
+
|
|
98
|
+
C -.stored at.- M
|
|
99
|
+
E -.stored at.- M
|
|
100
|
+
G -.stored at.- M
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
```typescript
|
|
104
|
+
private balancesPointer: u16 = Blockchain.nextPointer;
|
|
105
|
+
private balances: StoredMapU256;
|
|
106
|
+
|
|
107
|
+
constructor() {
|
|
108
|
+
super();
|
|
109
|
+
this.balances = new StoredMapU256(this.balancesPointer);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Set value
|
|
113
|
+
this.balances.set(userId, balance);
|
|
114
|
+
|
|
115
|
+
// Get value (returns u256.Zero if not set)
|
|
116
|
+
const balance: u256 = this.balances.get(userId);
|
|
117
|
+
|
|
118
|
+
// Check existence (StoredMapU256 doesn't have has() - compare with zero)
|
|
119
|
+
const exists: bool = !this.balances.get(userId).isZero();
|
|
120
|
+
|
|
121
|
+
// Delete (set to zero)
|
|
122
|
+
this.balances.set(userId, u256.Zero);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### Storage Layout
|
|
126
|
+
|
|
127
|
+
```
|
|
128
|
+
Map Pointer + SHA256(key) -> value
|
|
129
|
+
|
|
130
|
+
For key=5, pointer=3:
|
|
131
|
+
Storage key = SHA256(3 || 5)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Using Address Keys
|
|
135
|
+
|
|
136
|
+
For address-keyed mappings, convert address to `u256`:
|
|
137
|
+
|
|
138
|
+
```typescript
|
|
139
|
+
// Address to u256 conversion
|
|
140
|
+
private addressToKey(addr: Address): u256 {
|
|
141
|
+
const bytes = addr.toBytes();
|
|
142
|
+
return u256.fromBytes(bytes);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Get balance for address
|
|
146
|
+
public getBalance(addr: Address): u256 {
|
|
147
|
+
const key = this.addressToKey(addr);
|
|
148
|
+
return this.balances.get(key);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Set balance for address
|
|
152
|
+
public setBalance(addr: Address, amount: u256): void {
|
|
153
|
+
const key = this.addressToKey(addr);
|
|
154
|
+
this.balances.set(key, amount);
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## Nested Maps
|
|
159
|
+
|
|
160
|
+
For `mapping(key1 => mapping(key2 => value))`:
|
|
161
|
+
|
|
162
|
+
```typescript
|
|
163
|
+
private allowancesPointer: u16 = Blockchain.nextPointer;
|
|
164
|
+
private allowances: StoredMapU256;
|
|
165
|
+
|
|
166
|
+
constructor() {
|
|
167
|
+
super();
|
|
168
|
+
this.allowances = new StoredMapU256(this.allowancesPointer);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Create composite key
|
|
172
|
+
private allowanceKey(owner: Address, spender: Address): u256 {
|
|
173
|
+
const ownerBytes = owner.toBytes();
|
|
174
|
+
const spenderBytes = spender.toBytes();
|
|
175
|
+
|
|
176
|
+
// Combine and hash
|
|
177
|
+
const combined = new Uint8Array(64);
|
|
178
|
+
combined.set(ownerBytes, 0);
|
|
179
|
+
combined.set(spenderBytes, 32);
|
|
180
|
+
|
|
181
|
+
return u256.fromBytes(Blockchain.sha256(combined));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Get allowance
|
|
185
|
+
public getAllowance(owner: Address, spender: Address): u256 {
|
|
186
|
+
const key = this.allowanceKey(owner, spender);
|
|
187
|
+
return this.allowances.get(key);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Set allowance
|
|
191
|
+
public setAllowance(owner: Address, spender: Address, amount: u256): void {
|
|
192
|
+
const key = this.allowanceKey(owner, spender);
|
|
193
|
+
this.allowances.set(key, amount);
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## MapOfMap
|
|
198
|
+
|
|
199
|
+
For complex nested mappings, use `MapOfMap`. This provides a two-level structure:
|
|
200
|
+
|
|
201
|
+
```mermaid
|
|
202
|
+
---
|
|
203
|
+
config:
|
|
204
|
+
theme: dark
|
|
205
|
+
---
|
|
206
|
+
flowchart LR
|
|
207
|
+
A["MapOfMap instance<br/>pointer: u16"] --> B["owner1"]
|
|
208
|
+
A --> C["owner2"]
|
|
209
|
+
A --> D["owner3"]
|
|
210
|
+
B --> E1["Nested<u256>"]
|
|
211
|
+
C --> E2["Nested<u256>"]
|
|
212
|
+
D --> E3["Nested<u256>"]
|
|
213
|
+
E1 --> F1["spender1 -> u256"]
|
|
214
|
+
E1 --> F2["spender2 -> u256"]
|
|
215
|
+
E2 --> F3["spender1 -> u256"]
|
|
216
|
+
E3 --> F4["spender3 -> u256"]
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
```typescript
|
|
220
|
+
import { MapOfMap, Nested } from '@btc-vision/btc-runtime/runtime';
|
|
221
|
+
|
|
222
|
+
// mapping(address => mapping(address => uint256))
|
|
223
|
+
private allowancesPointer: u16 = Blockchain.nextPointer;
|
|
224
|
+
private allowances: MapOfMap<u256>;
|
|
225
|
+
|
|
226
|
+
constructor() {
|
|
227
|
+
super();
|
|
228
|
+
this.allowances = new MapOfMap<u256>(this.allowancesPointer);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Get nested value - two-step process
|
|
232
|
+
public getAllowance(owner: Address, spender: Address): u256 {
|
|
233
|
+
const ownerMap = this.allowances.get(owner); // Returns Nested<u256>
|
|
234
|
+
return ownerMap.get(spender); // Returns u256
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Set nested value - get, modify, commit back
|
|
238
|
+
public setAllowance(owner: Address, spender: Address, amount: u256): void {
|
|
239
|
+
const ownerMap = this.allowances.get(owner); // Get the nested map
|
|
240
|
+
ownerMap.set(spender, amount); // Modify it
|
|
241
|
+
this.allowances.set(owner, ownerMap); // Commit back
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### MapOfMap Get/Set Pattern
|
|
246
|
+
|
|
247
|
+
> **Important:** `MapOfMap.get(key)` returns a `Nested<T>` object, not the final value. You must call `.get()` on the nested object to retrieve the actual value. Similarly, when setting values, you must retrieve the nested map, modify it, then commit it back to the parent map.
|
|
248
|
+
|
|
249
|
+
```mermaid
|
|
250
|
+
---
|
|
251
|
+
config:
|
|
252
|
+
theme: dark
|
|
253
|
+
---
|
|
254
|
+
flowchart LR
|
|
255
|
+
A["allowances.get(owner)"] --> B["Returns Nested<u256>"]
|
|
256
|
+
B --> C["nested.get(spender)"]
|
|
257
|
+
C --> D["Returns u256 value"]
|
|
258
|
+
E["allowances.get(owner)"] --> F["nested.set(spender, amount)"]
|
|
259
|
+
F --> G["allowances.set(owner, nested)"]
|
|
260
|
+
G --> H["Commit to storage"]
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Solidity vs OPNet Comparison
|
|
264
|
+
|
|
265
|
+
### Quick Reference Table
|
|
266
|
+
|
|
267
|
+
| Solidity Mapping Type | OPNet Equivalent | Notes |
|
|
268
|
+
|-----------------------|------------------|-------|
|
|
269
|
+
| `mapping(uint256 => uint256)` | `StoredMapU256` | u256 keys and values |
|
|
270
|
+
| `mapping(address => uint256)` | `AddressMemoryMap` | Recommended for address keys |
|
|
271
|
+
| `mapping(address => uint256)` | `StoredMapU256` with `addressToKey()` | Alternative approach |
|
|
272
|
+
| `mapping(K => mapping(K2 => V))` | `MapOfMap<V>` | Two-level nesting |
|
|
273
|
+
| `mapping(K => mapping(K2 => V))` | `StoredMapU256` with composite key | Hash-based approach |
|
|
274
|
+
|
|
275
|
+
### Operations Comparison
|
|
276
|
+
|
|
277
|
+
| Operation | Solidity | OPNet (StoredMapU256) |
|
|
278
|
+
|-----------|----------|----------------------|
|
|
279
|
+
| Declare | `mapping(uint256 => uint256) data;` | `private data: StoredMapU256;` |
|
|
280
|
+
| Initialize | Automatic | `this.data = new StoredMapU256(this.dataPointer);` |
|
|
281
|
+
| Read value | `data[key]` | `data.get(key)` |
|
|
282
|
+
| Write value | `data[key] = value;` | `data.set(key, value)` |
|
|
283
|
+
| Check exists | `data[key] != 0` | `!data.get(key).isZero()` |
|
|
284
|
+
| Delete entry | `delete data[key];` | `data.set(key, u256.Zero)` |
|
|
285
|
+
| Default value | `0` | `u256.Zero` |
|
|
286
|
+
|
|
287
|
+
### Nested Mapping Comparison
|
|
288
|
+
|
|
289
|
+
| Operation | Solidity | OPNet (MapOfMap) |
|
|
290
|
+
|-----------|----------|------------------|
|
|
291
|
+
| Declare | `mapping(address => mapping(address => uint256)) allowances;` | `private allowances: MapOfMap<u256>;` |
|
|
292
|
+
| Read nested | `allowances[owner][spender]` | `allowances.get(owner).get(spender)` |
|
|
293
|
+
| Write nested | `allowances[owner][spender] = amount;` | `const m = allowances.get(owner); m.set(spender, amount); allowances.set(owner, m);` |
|
|
294
|
+
|
|
295
|
+
### Address Key Patterns
|
|
296
|
+
|
|
297
|
+
| Solidity Pattern | OPNet Equivalent |
|
|
298
|
+
|------------------|------------------|
|
|
299
|
+
| `mapping(address => uint256) balances;` | `private balances: AddressMemoryMap;` (preferred) |
|
|
300
|
+
| `balances[msg.sender]` | `balances.get(Blockchain.tx.sender)` |
|
|
301
|
+
| `balances[addr] = x;` | `balances.set(addr, x)` |
|
|
302
|
+
| `balances[addr] += amount;` | `balances.set(addr, SafeMath.add(balances.get(addr), amount))` |
|
|
303
|
+
|
|
304
|
+
### Common Use Cases
|
|
305
|
+
|
|
306
|
+
| Use Case | Solidity | OPNet |
|
|
307
|
+
|----------|----------|-------|
|
|
308
|
+
| Token balances | `mapping(address => uint256) balances;` | `AddressMemoryMap` |
|
|
309
|
+
| Approvals | `mapping(address => mapping(address => uint256))` | `MapOfMap<u256>` |
|
|
310
|
+
| Nonces | `mapping(address => uint256) nonces;` | `AddressMemoryMap` or `StoredMapU256` |
|
|
311
|
+
| Roles/permissions | `mapping(bytes32 => mapping(address => bool))` | `MapOfMap<u256>` with role hash |
|
|
312
|
+
| Token metadata | `mapping(uint256 => string)` | `StoredMapU256` with encoded strings |
|
|
313
|
+
| Checkpoints | `mapping(address => mapping(uint256 => uint256))` | `MapOfMap<u256>` or composite keys |
|
|
314
|
+
|
|
315
|
+
### Full Example Comparison
|
|
316
|
+
|
|
317
|
+
```solidity
|
|
318
|
+
// Solidity
|
|
319
|
+
contract Token {
|
|
320
|
+
mapping(address => uint256) public balances;
|
|
321
|
+
mapping(address => mapping(address => uint256)) public allowances;
|
|
322
|
+
|
|
323
|
+
function transfer(address to, uint256 amount) external {
|
|
324
|
+
require(balances[msg.sender] >= amount);
|
|
325
|
+
balances[msg.sender] -= amount;
|
|
326
|
+
balances[to] += amount;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function approve(address spender, uint256 amount) external {
|
|
330
|
+
allowances[msg.sender][spender] = amount;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
```typescript
|
|
336
|
+
// OPNet
|
|
337
|
+
@final
|
|
338
|
+
export class Token extends OP_NET {
|
|
339
|
+
private balancesPointer: u16 = Blockchain.nextPointer;
|
|
340
|
+
private allowancesPointer: u16 = Blockchain.nextPointer;
|
|
341
|
+
|
|
342
|
+
private balances: StoredMapU256;
|
|
343
|
+
private allowances: MapOfMap<u256>;
|
|
344
|
+
|
|
345
|
+
constructor() {
|
|
346
|
+
super();
|
|
347
|
+
this.balances = new StoredMapU256(this.balancesPointer);
|
|
348
|
+
this.allowances = new MapOfMap<u256>(this.allowancesPointer);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
private addressKey(addr: Address): u256 {
|
|
352
|
+
return u256.fromBytes(addr.toBytes());
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
public transfer(calldata: Calldata): BytesWriter {
|
|
356
|
+
const to = calldata.readAddress();
|
|
357
|
+
const amount = calldata.readU256();
|
|
358
|
+
const sender = Blockchain.tx.sender;
|
|
359
|
+
|
|
360
|
+
const senderKey = this.addressKey(sender);
|
|
361
|
+
const toKey = this.addressKey(to);
|
|
362
|
+
|
|
363
|
+
const senderBalance = this.balances.get(senderKey);
|
|
364
|
+
if (senderBalance < amount) {
|
|
365
|
+
throw new Revert('Insufficient balance');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this.balances.set(senderKey, SafeMath.sub(senderBalance, amount));
|
|
369
|
+
this.balances.set(toKey, SafeMath.add(this.balances.get(toKey), amount));
|
|
370
|
+
|
|
371
|
+
return new BytesWriter(0);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
public approve(calldata: Calldata): BytesWriter {
|
|
375
|
+
const spender = calldata.readAddress();
|
|
376
|
+
const amount = calldata.readU256();
|
|
377
|
+
const sender = Blockchain.tx.sender;
|
|
378
|
+
|
|
379
|
+
// Correct two-step MapOfMap pattern
|
|
380
|
+
const senderMap = this.allowances.get(sender);
|
|
381
|
+
senderMap.set(spender, amount);
|
|
382
|
+
this.allowances.set(sender, senderMap);
|
|
383
|
+
|
|
384
|
+
return new BytesWriter(0);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
## Side-by-Side Code Examples
|
|
390
|
+
|
|
391
|
+
### Simple Key-Value Store
|
|
392
|
+
|
|
393
|
+
**Solidity:**
|
|
394
|
+
```solidity
|
|
395
|
+
contract KeyValueStore {
|
|
396
|
+
mapping(uint256 => uint256) public data;
|
|
397
|
+
|
|
398
|
+
function set(uint256 key, uint256 value) external {
|
|
399
|
+
data[key] = value;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function get(uint256 key) external view returns (uint256) {
|
|
403
|
+
return data[key];
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function remove(uint256 key) external {
|
|
407
|
+
delete data[key];
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function increment(uint256 key) external {
|
|
411
|
+
data[key]++;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
**OPNet:**
|
|
417
|
+
```typescript
|
|
418
|
+
@final
|
|
419
|
+
export class KeyValueStore extends OP_NET {
|
|
420
|
+
private dataPointer: u16 = Blockchain.nextPointer;
|
|
421
|
+
private data: StoredMapU256;
|
|
422
|
+
|
|
423
|
+
constructor() {
|
|
424
|
+
super();
|
|
425
|
+
this.data = new StoredMapU256(this.dataPointer);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
public set(calldata: Calldata): BytesWriter {
|
|
429
|
+
const key = calldata.readU256();
|
|
430
|
+
const value = calldata.readU256();
|
|
431
|
+
this.data.set(key, value);
|
|
432
|
+
return new BytesWriter(0);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
public get(calldata: Calldata): BytesWriter {
|
|
436
|
+
const key = calldata.readU256();
|
|
437
|
+
const writer = new BytesWriter(32);
|
|
438
|
+
writer.writeU256(this.data.get(key));
|
|
439
|
+
return writer;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
public remove(calldata: Calldata): BytesWriter {
|
|
443
|
+
const key = calldata.readU256();
|
|
444
|
+
this.data.set(key, u256.Zero);
|
|
445
|
+
return new BytesWriter(0);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
public increment(calldata: Calldata): BytesWriter {
|
|
449
|
+
const key = calldata.readU256();
|
|
450
|
+
this.data.set(key, SafeMath.add(this.data.get(key), u256.One));
|
|
451
|
+
return new BytesWriter(0);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
### Approval System with Nested Mapping
|
|
457
|
+
|
|
458
|
+
**Solidity:**
|
|
459
|
+
```solidity
|
|
460
|
+
contract ApprovalSystem {
|
|
461
|
+
mapping(address => mapping(address => uint256)) public allowances;
|
|
462
|
+
|
|
463
|
+
function approve(address spender, uint256 amount) external {
|
|
464
|
+
allowances[msg.sender][spender] = amount;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function allowance(address owner, address spender) external view returns (uint256) {
|
|
468
|
+
return allowances[owner][spender];
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function increaseAllowance(address spender, uint256 addedValue) external {
|
|
472
|
+
allowances[msg.sender][spender] += addedValue;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function decreaseAllowance(address spender, uint256 subtractedValue) external {
|
|
476
|
+
uint256 currentAllowance = allowances[msg.sender][spender];
|
|
477
|
+
require(currentAllowance >= subtractedValue, "Decreased below zero");
|
|
478
|
+
allowances[msg.sender][spender] = currentAllowance - subtractedValue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
function spend(address owner, uint256 amount) external {
|
|
482
|
+
uint256 currentAllowance = allowances[owner][msg.sender];
|
|
483
|
+
require(currentAllowance >= amount, "Insufficient allowance");
|
|
484
|
+
allowances[owner][msg.sender] = currentAllowance - amount;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**OPNet:**
|
|
490
|
+
```typescript
|
|
491
|
+
@final
|
|
492
|
+
export class ApprovalSystem extends OP_NET {
|
|
493
|
+
private allowancesPointer: u16 = Blockchain.nextPointer;
|
|
494
|
+
private allowances: MapOfMap<u256>;
|
|
495
|
+
|
|
496
|
+
constructor() {
|
|
497
|
+
super();
|
|
498
|
+
this.allowances = new MapOfMap<u256>(this.allowancesPointer);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
public approve(calldata: Calldata): BytesWriter {
|
|
502
|
+
const spender = calldata.readAddress();
|
|
503
|
+
const amount = calldata.readU256();
|
|
504
|
+
const sender = Blockchain.tx.sender;
|
|
505
|
+
|
|
506
|
+
const senderAllowances = this.allowances.get(sender);
|
|
507
|
+
senderAllowances.set(spender, amount);
|
|
508
|
+
this.allowances.set(sender, senderAllowances);
|
|
509
|
+
|
|
510
|
+
return new BytesWriter(0);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
public allowance(calldata: Calldata): BytesWriter {
|
|
514
|
+
const owner = calldata.readAddress();
|
|
515
|
+
const spender = calldata.readAddress();
|
|
516
|
+
|
|
517
|
+
const ownerAllowances = this.allowances.get(owner);
|
|
518
|
+
const writer = new BytesWriter(32);
|
|
519
|
+
writer.writeU256(ownerAllowances.get(spender));
|
|
520
|
+
return writer;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
public increaseAllowance(calldata: Calldata): BytesWriter {
|
|
524
|
+
const spender = calldata.readAddress();
|
|
525
|
+
const addedValue = calldata.readU256();
|
|
526
|
+
const sender = Blockchain.tx.sender;
|
|
527
|
+
|
|
528
|
+
const senderAllowances = this.allowances.get(sender);
|
|
529
|
+
const current = senderAllowances.get(spender);
|
|
530
|
+
senderAllowances.set(spender, SafeMath.add(current, addedValue));
|
|
531
|
+
this.allowances.set(sender, senderAllowances);
|
|
532
|
+
|
|
533
|
+
return new BytesWriter(0);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
public decreaseAllowance(calldata: Calldata): BytesWriter {
|
|
537
|
+
const spender = calldata.readAddress();
|
|
538
|
+
const subtractedValue = calldata.readU256();
|
|
539
|
+
const sender = Blockchain.tx.sender;
|
|
540
|
+
|
|
541
|
+
const senderAllowances = this.allowances.get(sender);
|
|
542
|
+
const currentAllowance = senderAllowances.get(spender);
|
|
543
|
+
|
|
544
|
+
if (currentAllowance < subtractedValue) {
|
|
545
|
+
throw new Revert('Decreased below zero');
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
senderAllowances.set(spender, SafeMath.sub(currentAllowance, subtractedValue));
|
|
549
|
+
this.allowances.set(sender, senderAllowances);
|
|
550
|
+
|
|
551
|
+
return new BytesWriter(0);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
public spend(calldata: Calldata): BytesWriter {
|
|
555
|
+
const owner = calldata.readAddress();
|
|
556
|
+
const amount = calldata.readU256();
|
|
557
|
+
const sender = Blockchain.tx.sender;
|
|
558
|
+
|
|
559
|
+
const ownerAllowances = this.allowances.get(owner);
|
|
560
|
+
const currentAllowance = ownerAllowances.get(sender);
|
|
561
|
+
|
|
562
|
+
if (currentAllowance < amount) {
|
|
563
|
+
throw new Revert('Insufficient allowance');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
ownerAllowances.set(sender, SafeMath.sub(currentAllowance, amount));
|
|
567
|
+
this.allowances.set(owner, ownerAllowances);
|
|
568
|
+
|
|
569
|
+
return new BytesWriter(0);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
## Common Patterns
|
|
575
|
+
|
|
576
|
+
### Counter/Nonce Tracking
|
|
577
|
+
|
|
578
|
+
```typescript
|
|
579
|
+
private noncesPointer: u16 = Blockchain.nextPointer;
|
|
580
|
+
private nonces: StoredMapU256;
|
|
581
|
+
|
|
582
|
+
constructor() {
|
|
583
|
+
super();
|
|
584
|
+
this.nonces = new StoredMapU256(this.noncesPointer);
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
public getNonce(addr: Address): u256 {
|
|
588
|
+
return this.nonces.get(this.addressKey(addr));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
public incrementNonce(addr: Address): u256 {
|
|
592
|
+
const key = this.addressKey(addr);
|
|
593
|
+
const current = this.nonces.get(key);
|
|
594
|
+
const next = SafeMath.add(current, u256.One);
|
|
595
|
+
this.nonces.set(key, next);
|
|
596
|
+
return current; // Return old nonce
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private addressKey(addr: Address): u256 {
|
|
600
|
+
return u256.fromBytes(addr.toBytes());
|
|
601
|
+
}
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
### Role Management
|
|
605
|
+
|
|
606
|
+
```typescript
|
|
607
|
+
private rolesPointer: u16 = Blockchain.nextPointer;
|
|
608
|
+
private roles: StoredMapU256;
|
|
609
|
+
|
|
610
|
+
constructor() {
|
|
611
|
+
super();
|
|
612
|
+
this.roles = new StoredMapU256(this.rolesPointer);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private readonly ADMIN_ROLE: u256 = u256.One;
|
|
616
|
+
private readonly MINTER_ROLE: u256 = u256.fromU64(2);
|
|
617
|
+
|
|
618
|
+
public hasRole(addr: Address, role: u256): bool {
|
|
619
|
+
const key = this.roleKey(addr, role);
|
|
620
|
+
return !this.roles.get(key).isZero();
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
public grantRole(calldata: Calldata): BytesWriter {
|
|
624
|
+
this.onlyDeployer(Blockchain.tx.sender);
|
|
625
|
+
|
|
626
|
+
const addr = calldata.readAddress();
|
|
627
|
+
const role = calldata.readU256();
|
|
628
|
+
|
|
629
|
+
const key = this.roleKey(addr, role);
|
|
630
|
+
this.roles.set(key, u256.One);
|
|
631
|
+
|
|
632
|
+
return new BytesWriter(0);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
private roleKey(addr: Address, role: u256): u256 {
|
|
636
|
+
// Combine address and role into unique key
|
|
637
|
+
const bytes = new Uint8Array(64);
|
|
638
|
+
bytes.set(addr.toBytes(), 0);
|
|
639
|
+
bytes.set(role.toBytes(), 32);
|
|
640
|
+
return u256.fromBytes(Blockchain.sha256(bytes));
|
|
641
|
+
}
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
### Token Metadata Storage
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
// Store arbitrary metadata per token ID
|
|
648
|
+
private metadataPointer: u16 = Blockchain.nextPointer;
|
|
649
|
+
private metadata: StoredMapU256;
|
|
650
|
+
|
|
651
|
+
constructor() {
|
|
652
|
+
super();
|
|
653
|
+
this.metadata = new StoredMapU256(this.metadataPointer);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Each token can have multiple metadata fields
|
|
657
|
+
// Use composite keys: tokenId + fieldId
|
|
658
|
+
|
|
659
|
+
private readonly FIELD_NAME: u256 = u256.One;
|
|
660
|
+
private readonly FIELD_LEVEL: u256 = u256.fromU64(2);
|
|
661
|
+
private readonly FIELD_RARITY: u256 = u256.fromU64(3);
|
|
662
|
+
|
|
663
|
+
public getMetadata(tokenId: u256, field: u256): u256 {
|
|
664
|
+
const key = this.metadataKey(tokenId, field);
|
|
665
|
+
return this.metadata.get(key);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
public setMetadata(tokenId: u256, field: u256, value: u256): void {
|
|
669
|
+
const key = this.metadataKey(tokenId, field);
|
|
670
|
+
this.metadata.set(key, value);
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
private metadataKey(tokenId: u256, field: u256): u256 {
|
|
674
|
+
const bytes = new Uint8Array(64);
|
|
675
|
+
bytes.set(tokenId.toBytes(), 0);
|
|
676
|
+
bytes.set(field.toBytes(), 32);
|
|
677
|
+
return u256.fromBytes(Blockchain.sha256(bytes));
|
|
678
|
+
}
|
|
679
|
+
```
|
|
680
|
+
|
|
681
|
+
### Snapshot/Checkpoint Pattern
|
|
682
|
+
|
|
683
|
+
```typescript
|
|
684
|
+
// Store values at specific block numbers
|
|
685
|
+
private checkpointsPointer: u16 = Blockchain.nextPointer;
|
|
686
|
+
private checkpoints: StoredMapU256;
|
|
687
|
+
|
|
688
|
+
constructor() {
|
|
689
|
+
super();
|
|
690
|
+
this.checkpoints = new StoredMapU256(this.checkpointsPointer);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
public checkpoint(addr: Address, value: u256): void {
|
|
694
|
+
const blockNumber = Blockchain.block.number;
|
|
695
|
+
const key = this.checkpointKey(addr, blockNumber);
|
|
696
|
+
this.checkpoints.set(key, value);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
public getCheckpoint(addr: Address, blockNumber: u64): u256 {
|
|
700
|
+
const key = this.checkpointKey(addr, blockNumber);
|
|
701
|
+
return this.checkpoints.get(key);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
private checkpointKey(addr: Address, blockNumber: u64): u256 {
|
|
705
|
+
const bytes = new Uint8Array(40);
|
|
706
|
+
bytes.set(addr.toBytes(), 0);
|
|
707
|
+
// Encode block number in remaining bytes
|
|
708
|
+
const blockBytes = new Uint8Array(8);
|
|
709
|
+
// ... encode blockNumber
|
|
710
|
+
bytes.set(blockBytes, 32);
|
|
711
|
+
return u256.fromBytes(Blockchain.sha256(bytes));
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
## Best Practices
|
|
716
|
+
|
|
717
|
+
### 1. Use Consistent Key Functions
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
// Define key functions once
|
|
721
|
+
private addressKey(addr: Address): u256 {
|
|
722
|
+
return u256.fromBytes(addr.toBytes());
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Use consistently throughout contract
|
|
726
|
+
const key = this.addressKey(user);
|
|
727
|
+
```
|
|
728
|
+
|
|
729
|
+
### 2. Handle Default Values
|
|
730
|
+
|
|
731
|
+
```typescript
|
|
732
|
+
// Map returns u256.Zero for unset keys
|
|
733
|
+
const balance = this.balances.get(key);
|
|
734
|
+
|
|
735
|
+
// Check if actually set vs zero balance
|
|
736
|
+
// Option 1: Use separate "exists" tracking
|
|
737
|
+
// Option 2: Use non-zero sentinel for "set"
|
|
738
|
+
// Option 3: Accept that zero and unset are equivalent
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
### 3. Document Key Structures
|
|
742
|
+
|
|
743
|
+
```typescript
|
|
744
|
+
/**
|
|
745
|
+
* Storage layout:
|
|
746
|
+
* - balances: address -> u256
|
|
747
|
+
* Key: SHA256(balancesPointer || SHA256(address))
|
|
748
|
+
*
|
|
749
|
+
* - allowances: (owner, spender) -> u256
|
|
750
|
+
* Key: SHA256(allowancesPointer || SHA256(owner || spender))
|
|
751
|
+
*/
|
|
752
|
+
```
|
|
753
|
+
|
|
754
|
+
---
|
|
755
|
+
|
|
756
|
+
**Navigation:**
|
|
757
|
+
- Previous: [Stored Arrays](./stored-arrays.md)
|
|
758
|
+
- Next: [Memory Maps](./memory-maps.md)
|