@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,764 @@
|
|
|
1
|
+
# Memory Maps
|
|
2
|
+
|
|
3
|
+
Memory maps provide a convenient interface for address-keyed storage with automatic type handling. They're the recommended way to implement Solidity-style `mapping(address => T)` patterns.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import {
|
|
9
|
+
AddressMemoryMap,
|
|
10
|
+
Blockchain,
|
|
11
|
+
Address,
|
|
12
|
+
} from '@btc-vision/btc-runtime/runtime';
|
|
13
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
14
|
+
|
|
15
|
+
// Allocate storage pointer
|
|
16
|
+
private balancesPointer: u16 = Blockchain.nextPointer;
|
|
17
|
+
|
|
18
|
+
// Create memory map
|
|
19
|
+
private balances: AddressMemoryMap;
|
|
20
|
+
|
|
21
|
+
constructor() {
|
|
22
|
+
super();
|
|
23
|
+
this.balances = new AddressMemoryMap(this.balancesPointer);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Usage
|
|
27
|
+
const balance = this.balances.get(userAddress);
|
|
28
|
+
this.balances.set(userAddress, newBalance);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## AddressMemoryMap
|
|
32
|
+
|
|
33
|
+
The primary memory map type for address-keyed storage. It stores and returns u256 values directly.
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
class AddressMemoryMap
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Constructor Pattern
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
private balancesPointer: u16 = Blockchain.nextPointer;
|
|
43
|
+
private balances: AddressMemoryMap;
|
|
44
|
+
|
|
45
|
+
constructor() {
|
|
46
|
+
super();
|
|
47
|
+
this.balances = new AddressMemoryMap(this.balancesPointer);
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### Methods
|
|
52
|
+
|
|
53
|
+
```typescript
|
|
54
|
+
// Get value for address
|
|
55
|
+
public get(key: Address): u256
|
|
56
|
+
|
|
57
|
+
// Set value for address (returns this for chaining)
|
|
58
|
+
public set(key: Address, value: u256): this
|
|
59
|
+
|
|
60
|
+
// Get raw bytes
|
|
61
|
+
public getAsUint8Array(key: Address): Uint8Array
|
|
62
|
+
|
|
63
|
+
// Set raw bytes
|
|
64
|
+
public setAsUint8Array(key: Address, value: Uint8Array): this
|
|
65
|
+
|
|
66
|
+
// Check if key has non-default value
|
|
67
|
+
public has(key: Address): bool
|
|
68
|
+
|
|
69
|
+
// Delete (set to default, returns true if key existed)
|
|
70
|
+
public delete(key: Address): bool
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Storage Flow
|
|
74
|
+
|
|
75
|
+
When you access an AddressMemoryMap, the address is converted to a storage key via SHA256:
|
|
76
|
+
|
|
77
|
+
```mermaid
|
|
78
|
+
---
|
|
79
|
+
config:
|
|
80
|
+
theme: dark
|
|
81
|
+
---
|
|
82
|
+
flowchart LR
|
|
83
|
+
subgraph map["AddressMemoryMap Instance"]
|
|
84
|
+
A["pointer: u16<br/>Storage base pointer"]
|
|
85
|
+
B["Internal Map<br/>Address -> u256"]
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
subgraph addrops["Address Key Operations"]
|
|
89
|
+
C["User Address"]
|
|
90
|
+
D["Address.toBytes()"]
|
|
91
|
+
E["32-byte key"]
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
subgraph keygen["Storage Key Generation"]
|
|
95
|
+
F["encodePointer()"]
|
|
96
|
+
G["pointer + address bytes"]
|
|
97
|
+
H["SHA256 hash"]
|
|
98
|
+
I["32-byte storage key"]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
C --> D
|
|
102
|
+
D --> E
|
|
103
|
+
A --> F
|
|
104
|
+
E --> F
|
|
105
|
+
F --> G
|
|
106
|
+
G --> H
|
|
107
|
+
H --> I
|
|
108
|
+
I --> J[("Blockchain Storage")]
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Address to Storage Key
|
|
112
|
+
|
|
113
|
+
The complete flow from address to storage access:
|
|
114
|
+
|
|
115
|
+
```mermaid
|
|
116
|
+
flowchart LR
|
|
117
|
+
subgraph input["Input"]
|
|
118
|
+
A["Address<br/>0x1234...abcd"]
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
subgraph getaddr["AddressMemoryMap.get(address)"]
|
|
122
|
+
B["toBytes()"]
|
|
123
|
+
C["32-byte Uint8Array"]
|
|
124
|
+
D["encodePointer()<br/>pointer + addressBytes"]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
subgraph storage["Storage Access"]
|
|
128
|
+
E["32-byte storage key"]
|
|
129
|
+
F["Blockchain.getStorageAt()"]
|
|
130
|
+
G["Raw bytes from storage"]
|
|
131
|
+
H["u256.fromUint8ArrayBE()"]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
subgraph output["Output"]
|
|
135
|
+
I["u256 value<br/>or u256.Zero if not set"]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
A --> B
|
|
139
|
+
B --> C
|
|
140
|
+
C --> D
|
|
141
|
+
D --> E
|
|
142
|
+
E --> F
|
|
143
|
+
F --> G
|
|
144
|
+
G --> H
|
|
145
|
+
H --> I
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
## Solidity vs OPNet Comparison
|
|
149
|
+
|
|
150
|
+
### Quick Reference Table
|
|
151
|
+
|
|
152
|
+
| Solidity | OPNet AddressMemoryMap |
|
|
153
|
+
|----------|------------------------|
|
|
154
|
+
| `mapping(address => uint256)` | `AddressMemoryMap` |
|
|
155
|
+
| `balances[addr]` | `balances.get(addr)` |
|
|
156
|
+
| `balances[addr] = val` | `balances.set(addr, val)` |
|
|
157
|
+
| Default value: `0` | Default value: `u256.Zero` |
|
|
158
|
+
| Implicit initialization | Explicit constructor initialization |
|
|
159
|
+
| No existence check | `balances.has(addr)` available |
|
|
160
|
+
| `delete balances[addr]` | `balances.delete(addr)` |
|
|
161
|
+
|
|
162
|
+
### Operations Comparison
|
|
163
|
+
|
|
164
|
+
| Operation | Solidity | OPNet |
|
|
165
|
+
|-----------|----------|-------|
|
|
166
|
+
| Declare | `mapping(address => uint256) public balances;` | `private balances: AddressMemoryMap;` |
|
|
167
|
+
| Initialize | Automatic | `this.balances = new AddressMemoryMap(this.balancesPointer);` |
|
|
168
|
+
| Read | `balances[addr]` | `balances.get(addr)` |
|
|
169
|
+
| Write | `balances[addr] = amount;` | `balances.set(addr, amount)` |
|
|
170
|
+
| Add to value | `balances[addr] += amount;` | `balances.set(addr, SafeMath.add(balances.get(addr), amount))` |
|
|
171
|
+
| Subtract | `balances[addr] -= amount;` | `balances.set(addr, SafeMath.sub(balances.get(addr), amount))` |
|
|
172
|
+
| Check non-zero | `balances[addr] > 0` | `!balances.get(addr).isZero()` |
|
|
173
|
+
| Delete/reset | `delete balances[addr];` | `balances.delete(addr)` or `balances.set(addr, u256.Zero)` |
|
|
174
|
+
| Check exists | N/A (always 0 default) | `balances.has(addr)` |
|
|
175
|
+
|
|
176
|
+
### Common Patterns
|
|
177
|
+
|
|
178
|
+
| Pattern | Solidity | OPNet |
|
|
179
|
+
|---------|----------|-------|
|
|
180
|
+
| Transfer balance | `balances[from] -= amt; balances[to] += amt;` | `balances.set(from, SafeMath.sub(balances.get(from), amt)); balances.set(to, SafeMath.add(balances.get(to), amt));` |
|
|
181
|
+
| Check sufficient | `require(balances[addr] >= amount);` | `if (balances.get(addr) < amount) throw new Revert("Insufficient");` |
|
|
182
|
+
| Mint tokens | `balances[to] += amount;` | `balances.set(to, SafeMath.add(balances.get(to), amount));` |
|
|
183
|
+
| Burn tokens | `balances[from] -= amount;` | `balances.set(from, SafeMath.sub(balances.get(from), amount));` |
|
|
184
|
+
| Zero balance check | `balances[addr] == 0` | `balances.get(addr).isZero()` |
|
|
185
|
+
| Get sender balance | `balances[msg.sender]` | `balances.get(Blockchain.tx.sender)` |
|
|
186
|
+
|
|
187
|
+
### Key Differences from Solidity
|
|
188
|
+
|
|
189
|
+
| Aspect | Solidity | OPNet |
|
|
190
|
+
|--------|----------|-------|
|
|
191
|
+
| Key type | `address` (20 bytes) | `Address` (32 bytes) |
|
|
192
|
+
| Value type | Any | `u256` only |
|
|
193
|
+
| Storage slot | `keccak256(key . slot)` | `SHA256(pointer + address)` |
|
|
194
|
+
| Reentrancy safe | Developer responsibility | Developer responsibility |
|
|
195
|
+
| Arithmetic | Native operators | `SafeMath` required |
|
|
196
|
+
|
|
197
|
+
### ERC-20 Style Comparison
|
|
198
|
+
|
|
199
|
+
| ERC-20 Function | Solidity | OPNet |
|
|
200
|
+
|-----------------|----------|-------|
|
|
201
|
+
| `balanceOf(address)` | `return balances[owner];` | `return this.balances.get(owner);` |
|
|
202
|
+
| `transfer(to, amount)` | `balances[msg.sender] -= amount; balances[to] += amount;` | `this.balances.set(sender, SafeMath.sub(...)); this.balances.set(to, SafeMath.add(...));` |
|
|
203
|
+
| `approve(spender, amount)` | `allowances[msg.sender][spender] = amount;` | Use `MapOfMap<u256>` for nested mapping |
|
|
204
|
+
|
|
205
|
+
### Full Comparison Example
|
|
206
|
+
|
|
207
|
+
```solidity
|
|
208
|
+
// Solidity
|
|
209
|
+
contract Token {
|
|
210
|
+
mapping(address => uint256) public balances;
|
|
211
|
+
|
|
212
|
+
function transfer(address to, uint256 amount) external {
|
|
213
|
+
require(balances[msg.sender] >= amount, "Insufficient");
|
|
214
|
+
balances[msg.sender] -= amount;
|
|
215
|
+
balances[to] += amount;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
// OPNet
|
|
222
|
+
@final
|
|
223
|
+
export class Token extends OP_NET {
|
|
224
|
+
private balancesPointer: u16 = Blockchain.nextPointer;
|
|
225
|
+
private balances: AddressMemoryMap;
|
|
226
|
+
|
|
227
|
+
constructor() {
|
|
228
|
+
super();
|
|
229
|
+
this.balances = new AddressMemoryMap(this.balancesPointer);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
public transfer(calldata: Calldata): BytesWriter {
|
|
233
|
+
const to = calldata.readAddress();
|
|
234
|
+
const amount = calldata.readU256();
|
|
235
|
+
const sender = Blockchain.tx.sender;
|
|
236
|
+
|
|
237
|
+
const senderBalance = this.balances.get(sender);
|
|
238
|
+
if (senderBalance < amount) {
|
|
239
|
+
throw new Revert('Insufficient balance');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
this.balances.set(sender, SafeMath.sub(senderBalance, amount));
|
|
243
|
+
this.balances.set(to, SafeMath.add(this.balances.get(to), amount));
|
|
244
|
+
|
|
245
|
+
return new BytesWriter(0);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
## Side-by-Side Code Examples
|
|
251
|
+
|
|
252
|
+
### Basic Token Balance Tracking
|
|
253
|
+
|
|
254
|
+
**Solidity:**
|
|
255
|
+
```solidity
|
|
256
|
+
contract TokenBalances {
|
|
257
|
+
mapping(address => uint256) public balances;
|
|
258
|
+
uint256 public totalSupply;
|
|
259
|
+
|
|
260
|
+
function mint(address to, uint256 amount) external {
|
|
261
|
+
balances[to] += amount;
|
|
262
|
+
totalSupply += amount;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function burn(address from, uint256 amount) external {
|
|
266
|
+
require(balances[from] >= amount, "Insufficient balance");
|
|
267
|
+
balances[from] -= amount;
|
|
268
|
+
totalSupply -= amount;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function transfer(address from, address to, uint256 amount) external {
|
|
272
|
+
require(balances[from] >= amount, "Insufficient balance");
|
|
273
|
+
balances[from] -= amount;
|
|
274
|
+
balances[to] += amount;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function balanceOf(address account) external view returns (uint256) {
|
|
278
|
+
return balances[account];
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**OPNet:**
|
|
284
|
+
```typescript
|
|
285
|
+
@final
|
|
286
|
+
export class TokenBalances extends OP_NET {
|
|
287
|
+
private balancesPointer: u16 = Blockchain.nextPointer;
|
|
288
|
+
private totalSupplyPointer: u16 = Blockchain.nextPointer;
|
|
289
|
+
|
|
290
|
+
private balances: AddressMemoryMap;
|
|
291
|
+
private _totalSupply: StoredU256 = new StoredU256(this.totalSupplyPointer, EMPTY_POINTER);
|
|
292
|
+
|
|
293
|
+
constructor() {
|
|
294
|
+
super();
|
|
295
|
+
this.balances = new AddressMemoryMap(this.balancesPointer);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
public mint(calldata: Calldata): BytesWriter {
|
|
299
|
+
const to = calldata.readAddress();
|
|
300
|
+
const amount = calldata.readU256();
|
|
301
|
+
|
|
302
|
+
this.balances.set(to, SafeMath.add(this.balances.get(to), amount));
|
|
303
|
+
this._totalSupply.value = SafeMath.add(this._totalSupply.value, amount);
|
|
304
|
+
|
|
305
|
+
return new BytesWriter(0);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
public burn(calldata: Calldata): BytesWriter {
|
|
309
|
+
const from = calldata.readAddress();
|
|
310
|
+
const amount = calldata.readU256();
|
|
311
|
+
|
|
312
|
+
const balance = this.balances.get(from);
|
|
313
|
+
if (balance < amount) {
|
|
314
|
+
throw new Revert('Insufficient balance');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
this.balances.set(from, SafeMath.sub(balance, amount));
|
|
318
|
+
this._totalSupply.value = SafeMath.sub(this._totalSupply.value, amount);
|
|
319
|
+
|
|
320
|
+
return new BytesWriter(0);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
public transfer(calldata: Calldata): BytesWriter {
|
|
324
|
+
const from = calldata.readAddress();
|
|
325
|
+
const to = calldata.readAddress();
|
|
326
|
+
const amount = calldata.readU256();
|
|
327
|
+
|
|
328
|
+
const fromBalance = this.balances.get(from);
|
|
329
|
+
if (fromBalance < amount) {
|
|
330
|
+
throw new Revert('Insufficient balance');
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
this.balances.set(from, SafeMath.sub(fromBalance, amount));
|
|
334
|
+
this.balances.set(to, SafeMath.add(this.balances.get(to), amount));
|
|
335
|
+
|
|
336
|
+
return new BytesWriter(0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
public balanceOf(calldata: Calldata): BytesWriter {
|
|
340
|
+
const account = calldata.readAddress();
|
|
341
|
+
const writer = new BytesWriter(32);
|
|
342
|
+
writer.writeU256(this.balances.get(account));
|
|
343
|
+
return writer;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
public totalSupply(_calldata: Calldata): BytesWriter {
|
|
347
|
+
const writer = new BytesWriter(32);
|
|
348
|
+
writer.writeU256(this._totalSupply.value);
|
|
349
|
+
return writer;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### Staking Contract
|
|
355
|
+
|
|
356
|
+
**Solidity:**
|
|
357
|
+
```solidity
|
|
358
|
+
contract Staking {
|
|
359
|
+
mapping(address => uint256) public stakedAmount;
|
|
360
|
+
mapping(address => uint256) public stakedTimestamp;
|
|
361
|
+
mapping(address => uint256) public rewards;
|
|
362
|
+
|
|
363
|
+
function stake(uint256 amount) external {
|
|
364
|
+
stakedAmount[msg.sender] += amount;
|
|
365
|
+
stakedTimestamp[msg.sender] = block.timestamp;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function unstake(uint256 amount) external {
|
|
369
|
+
require(stakedAmount[msg.sender] >= amount, "Not enough staked");
|
|
370
|
+
stakedAmount[msg.sender] -= amount;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function claimRewards() external {
|
|
374
|
+
uint256 reward = calculateReward(msg.sender);
|
|
375
|
+
rewards[msg.sender] = 0;
|
|
376
|
+
// Transfer reward...
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function calculateReward(address user) public view returns (uint256) {
|
|
380
|
+
uint256 duration = block.timestamp - stakedTimestamp[user];
|
|
381
|
+
return stakedAmount[user] * duration / 365 days;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function getStakeInfo(address user) external view returns (uint256, uint256, uint256) {
|
|
385
|
+
return (stakedAmount[user], stakedTimestamp[user], rewards[user]);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
**OPNet:**
|
|
391
|
+
```typescript
|
|
392
|
+
@final
|
|
393
|
+
export class Staking extends OP_NET {
|
|
394
|
+
private stakedAmountPointer: u16 = Blockchain.nextPointer;
|
|
395
|
+
private stakedTimestampPointer: u16 = Blockchain.nextPointer;
|
|
396
|
+
private rewardsPointer: u16 = Blockchain.nextPointer;
|
|
397
|
+
|
|
398
|
+
private stakedAmount: AddressMemoryMap;
|
|
399
|
+
private stakedTimestamp: AddressMemoryMap;
|
|
400
|
+
private rewards: AddressMemoryMap;
|
|
401
|
+
|
|
402
|
+
constructor() {
|
|
403
|
+
super();
|
|
404
|
+
this.stakedAmount = new AddressMemoryMap(this.stakedAmountPointer);
|
|
405
|
+
this.stakedTimestamp = new AddressMemoryMap(this.stakedTimestampPointer);
|
|
406
|
+
this.rewards = new AddressMemoryMap(this.rewardsPointer);
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
public stake(calldata: Calldata): BytesWriter {
|
|
410
|
+
const amount = calldata.readU256();
|
|
411
|
+
const sender = Blockchain.tx.sender;
|
|
412
|
+
|
|
413
|
+
this.stakedAmount.set(sender, SafeMath.add(this.stakedAmount.get(sender), amount));
|
|
414
|
+
this.stakedTimestamp.set(sender, u256.fromU64(Blockchain.block.medianTime));
|
|
415
|
+
|
|
416
|
+
return new BytesWriter(0);
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
public unstake(calldata: Calldata): BytesWriter {
|
|
420
|
+
const amount = calldata.readU256();
|
|
421
|
+
const sender = Blockchain.tx.sender;
|
|
422
|
+
|
|
423
|
+
const staked = this.stakedAmount.get(sender);
|
|
424
|
+
if (staked < amount) {
|
|
425
|
+
throw new Revert('Not enough staked');
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
this.stakedAmount.set(sender, SafeMath.sub(staked, amount));
|
|
429
|
+
|
|
430
|
+
return new BytesWriter(0);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
public claimRewards(_calldata: Calldata): BytesWriter {
|
|
434
|
+
const sender = Blockchain.tx.sender;
|
|
435
|
+
const reward = this.calculateReward(sender);
|
|
436
|
+
|
|
437
|
+
this.rewards.set(sender, u256.Zero);
|
|
438
|
+
// Transfer reward...
|
|
439
|
+
|
|
440
|
+
const writer = new BytesWriter(32);
|
|
441
|
+
writer.writeU256(reward);
|
|
442
|
+
return writer;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
private calculateReward(user: Address): u256 {
|
|
446
|
+
const timestamp = this.stakedTimestamp.get(user);
|
|
447
|
+
const currentTime = u256.fromU64(Blockchain.block.medianTime);
|
|
448
|
+
const duration = SafeMath.sub(currentTime, timestamp);
|
|
449
|
+
const staked = this.stakedAmount.get(user);
|
|
450
|
+
|
|
451
|
+
// Simplified: staked * duration / YEAR_IN_SECONDS
|
|
452
|
+
const YEAR_SECONDS = u256.fromU64(31536000);
|
|
453
|
+
return SafeMath.div(SafeMath.mul(staked, duration), YEAR_SECONDS);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
public getStakeInfo(calldata: Calldata): BytesWriter {
|
|
457
|
+
const user = calldata.readAddress();
|
|
458
|
+
|
|
459
|
+
const writer = new BytesWriter(96);
|
|
460
|
+
writer.writeU256(this.stakedAmount.get(user));
|
|
461
|
+
writer.writeU256(this.stakedTimestamp.get(user));
|
|
462
|
+
writer.writeU256(this.rewards.get(user));
|
|
463
|
+
return writer;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
## Usage Examples
|
|
469
|
+
|
|
470
|
+
### Basic Balance Tracking
|
|
471
|
+
|
|
472
|
+
```typescript
|
|
473
|
+
@final
|
|
474
|
+
export class Token extends OP_NET {
|
|
475
|
+
private balancesPointer: u16 = Blockchain.nextPointer;
|
|
476
|
+
private balances: AddressMemoryMap;
|
|
477
|
+
|
|
478
|
+
constructor() {
|
|
479
|
+
super();
|
|
480
|
+
this.balances = new AddressMemoryMap(this.balancesPointer);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
public balanceOf(calldata: Calldata): BytesWriter {
|
|
484
|
+
const account = calldata.readAddress();
|
|
485
|
+
const balance = this.balances.get(account);
|
|
486
|
+
|
|
487
|
+
const writer = new BytesWriter(32);
|
|
488
|
+
writer.writeU256(balance);
|
|
489
|
+
return writer;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
public _transfer(from: Address, to: Address, amount: u256): void {
|
|
493
|
+
const fromBalance = this.balances.get(from);
|
|
494
|
+
if (fromBalance < amount) {
|
|
495
|
+
throw new Revert('Insufficient balance');
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
this.balances.set(from, SafeMath.sub(fromBalance, amount));
|
|
499
|
+
this.balances.set(to, SafeMath.add(this.balances.get(to), amount));
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
### Approval Tracking
|
|
505
|
+
|
|
506
|
+
```typescript
|
|
507
|
+
import { encodePointer } from '@btc-vision/btc-runtime/runtime';
|
|
508
|
+
|
|
509
|
+
// Nested mapping: owner => (spender => amount)
|
|
510
|
+
// Using composite storage
|
|
511
|
+
|
|
512
|
+
private allowancesPointer: u16 = Blockchain.nextPointer;
|
|
513
|
+
|
|
514
|
+
// For nested maps, create helper methods
|
|
515
|
+
private getAllowance(owner: Address, spender: Address): u256 {
|
|
516
|
+
const subPointer = this.computeAllowanceKey(owner, spender);
|
|
517
|
+
const pointerHash = encodePointer(this.allowancesPointer, subPointer.toUint8Array(true));
|
|
518
|
+
const stored = Blockchain.getStorageAt(pointerHash);
|
|
519
|
+
return u256.fromUint8ArrayBE(stored);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
private setAllowance(owner: Address, spender: Address, amount: u256): void {
|
|
523
|
+
const subPointer = this.computeAllowanceKey(owner, spender);
|
|
524
|
+
const pointerHash = encodePointer(this.allowancesPointer, subPointer.toUint8Array(true));
|
|
525
|
+
Blockchain.setStorageAt(pointerHash, amount.toUint8Array(true));
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
private computeAllowanceKey(owner: Address, spender: Address): u256 {
|
|
529
|
+
const combined = new Uint8Array(64);
|
|
530
|
+
combined.set(owner.toBytes(), 0);
|
|
531
|
+
combined.set(spender.toBytes(), 32);
|
|
532
|
+
return u256.fromBytes(Blockchain.sha256(combined));
|
|
533
|
+
}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
### Staking with Multiple Values
|
|
537
|
+
|
|
538
|
+
```typescript
|
|
539
|
+
// Track staked amount and timestamp per user
|
|
540
|
+
private stakedAmountPointer: u16 = Blockchain.nextPointer;
|
|
541
|
+
private stakedTimePointer: u16 = Blockchain.nextPointer;
|
|
542
|
+
|
|
543
|
+
private stakedAmount: AddressMemoryMap;
|
|
544
|
+
private stakedTime: AddressMemoryMap;
|
|
545
|
+
|
|
546
|
+
constructor() {
|
|
547
|
+
super();
|
|
548
|
+
this.stakedAmount = new AddressMemoryMap(this.stakedAmountPointer);
|
|
549
|
+
this.stakedTime = new AddressMemoryMap(this.stakedTimePointer);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
public stake(calldata: Calldata): BytesWriter {
|
|
553
|
+
const amount = calldata.readU256();
|
|
554
|
+
const sender = Blockchain.tx.sender;
|
|
555
|
+
|
|
556
|
+
// Update staked amount
|
|
557
|
+
const current = this.stakedAmount.get(sender);
|
|
558
|
+
this.stakedAmount.set(sender, SafeMath.add(current, amount));
|
|
559
|
+
|
|
560
|
+
// Update stake time
|
|
561
|
+
this.stakedTime.set(sender, u256.fromU64(Blockchain.block.medianTime));
|
|
562
|
+
|
|
563
|
+
return new BytesWriter(0);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
public getStakeInfo(calldata: Calldata): BytesWriter {
|
|
567
|
+
const user = calldata.readAddress();
|
|
568
|
+
|
|
569
|
+
const writer = new BytesWriter(64);
|
|
570
|
+
writer.writeU256(this.stakedAmount.get(user));
|
|
571
|
+
writer.writeU256(this.stakedTime.get(user));
|
|
572
|
+
return writer;
|
|
573
|
+
}
|
|
574
|
+
```
|
|
575
|
+
|
|
576
|
+
## Storage vs Memory
|
|
577
|
+
|
|
578
|
+
### Storage (Persistent)
|
|
579
|
+
|
|
580
|
+
```typescript
|
|
581
|
+
// AddressMemoryMap wraps persistent storage
|
|
582
|
+
// Changes persist across transactions
|
|
583
|
+
|
|
584
|
+
public deposit(calldata: Calldata): BytesWriter {
|
|
585
|
+
const amount = calldata.readU256();
|
|
586
|
+
const sender = Blockchain.tx.sender;
|
|
587
|
+
|
|
588
|
+
const current = this.deposits.get(sender); // Reads from storage
|
|
589
|
+
this.deposits.set(sender, SafeMath.add(current, amount)); // Writes to storage
|
|
590
|
+
|
|
591
|
+
return new BytesWriter(0);
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
### In-Memory Collections
|
|
596
|
+
|
|
597
|
+
```typescript
|
|
598
|
+
// For temporary collections within a single call
|
|
599
|
+
// Use standard AssemblyScript Map
|
|
600
|
+
|
|
601
|
+
public processAddresses(calldata: Calldata): BytesWriter {
|
|
602
|
+
const addresses = calldata.readAddressArray();
|
|
603
|
+
|
|
604
|
+
// Temporary map for deduplication
|
|
605
|
+
const seen = new Map<string, bool>();
|
|
606
|
+
|
|
607
|
+
for (let i = 0; i < addresses.length; i++) {
|
|
608
|
+
const addrStr = addresses[i].toBytes().toString();
|
|
609
|
+
if (seen.has(addrStr)) {
|
|
610
|
+
continue; // Skip duplicate
|
|
611
|
+
}
|
|
612
|
+
seen.set(addrStr, true);
|
|
613
|
+
|
|
614
|
+
// Process unique address...
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
return new BytesWriter(0);
|
|
618
|
+
}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
## Warning: AssemblyScript Map vs btc-runtime Map
|
|
622
|
+
|
|
623
|
+
When working with persistent storage, always use the btc-runtime Map:
|
|
624
|
+
|
|
625
|
+
```mermaid
|
|
626
|
+
---
|
|
627
|
+
config:
|
|
628
|
+
theme: dark
|
|
629
|
+
---
|
|
630
|
+
flowchart LR
|
|
631
|
+
A["WRONG:<br/>AssemblyScript Map"] --> B["NOT blockchain-optimized"]
|
|
632
|
+
B --> C["Broken comparisons"]
|
|
633
|
+
C --> D["Data corruption"]
|
|
634
|
+
E["CORRECT:<br/>btc-runtime Map"] --> F["Blockchain-optimized"]
|
|
635
|
+
F --> G["Proper equality"]
|
|
636
|
+
G --> H["Safe operations"]
|
|
637
|
+
```
|
|
638
|
+
|
|
639
|
+
## Patterns
|
|
640
|
+
|
|
641
|
+
### Enumerable Map
|
|
642
|
+
|
|
643
|
+
To track all keys in a map:
|
|
644
|
+
|
|
645
|
+
```typescript
|
|
646
|
+
// Combine map with array for enumeration
|
|
647
|
+
private balancesPointer: u16 = Blockchain.nextPointer;
|
|
648
|
+
private holdersPointer: u16 = Blockchain.nextPointer;
|
|
649
|
+
|
|
650
|
+
private balances: AddressMemoryMap;
|
|
651
|
+
private holders: StoredAddressArray;
|
|
652
|
+
|
|
653
|
+
constructor() {
|
|
654
|
+
super();
|
|
655
|
+
this.balances = new AddressMemoryMap(this.balancesPointer);
|
|
656
|
+
this.holders = new StoredAddressArray(this.holdersPointer);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
public _mint(to: Address, amount: u256): void {
|
|
660
|
+
// Track new holder
|
|
661
|
+
if (this.balances.get(to).isZero()) {
|
|
662
|
+
this.holders.push(to);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Update balance
|
|
666
|
+
this.balances.set(to, SafeMath.add(this.balances.get(to), amount));
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
public getHolders(_calldata: Calldata): BytesWriter {
|
|
670
|
+
const count = this.holders.length;
|
|
671
|
+
const writer = new BytesWriter(32 * i32(count) + 4);
|
|
672
|
+
|
|
673
|
+
writer.writeU32(u32(count));
|
|
674
|
+
for (let i: u64 = 0; i < count; i++) {
|
|
675
|
+
writer.writeAddress(this.holders.get(i));
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return writer;
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### Lazy Initialization
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
// Values initialize to default when first accessed
|
|
686
|
+
public ensureAccount(addr: Address): void {
|
|
687
|
+
// get() returns default (u256.Zero) if not set
|
|
688
|
+
// No explicit initialization needed
|
|
689
|
+
const balance = this.balances.get(addr);
|
|
690
|
+
|
|
691
|
+
// First set creates the storage entry
|
|
692
|
+
if (balance.isZero()) {
|
|
693
|
+
// Optional: Initialize with some value
|
|
694
|
+
this.balances.set(addr, u256.One); // e.g., welcome bonus
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
```
|
|
698
|
+
|
|
699
|
+
### Read-Modify-Write Pattern
|
|
700
|
+
|
|
701
|
+
```typescript
|
|
702
|
+
public addToBalance(addr: Address, amount: u256): void {
|
|
703
|
+
// Read current
|
|
704
|
+
const current = this.balances.get(addr);
|
|
705
|
+
|
|
706
|
+
// Modify
|
|
707
|
+
const newBalance = SafeMath.add(current, amount);
|
|
708
|
+
|
|
709
|
+
// Write back
|
|
710
|
+
this.balances.set(addr, newBalance);
|
|
711
|
+
}
|
|
712
|
+
```
|
|
713
|
+
|
|
714
|
+
## Best Practices
|
|
715
|
+
|
|
716
|
+
### 1. Initialize in Constructor
|
|
717
|
+
|
|
718
|
+
```typescript
|
|
719
|
+
constructor() {
|
|
720
|
+
super();
|
|
721
|
+
// Always initialize maps in constructor
|
|
722
|
+
this.balances = new AddressMemoryMap(this.balancesPointer);
|
|
723
|
+
this.stakes = new AddressMemoryMap(this.stakesPointer);
|
|
724
|
+
}
|
|
725
|
+
```
|
|
726
|
+
|
|
727
|
+
### 2. Default Values
|
|
728
|
+
|
|
729
|
+
```typescript
|
|
730
|
+
// AddressMemoryMap always uses u256.Zero as the default value
|
|
731
|
+
// Unset addresses will return u256.Zero when queried
|
|
732
|
+
new AddressMemoryMap(ptr);
|
|
733
|
+
```
|
|
734
|
+
|
|
735
|
+
### 3. Validate Addresses
|
|
736
|
+
|
|
737
|
+
```typescript
|
|
738
|
+
public transfer(calldata: Calldata): BytesWriter {
|
|
739
|
+
const to = calldata.readAddress();
|
|
740
|
+
|
|
741
|
+
// Validate before map operations
|
|
742
|
+
if (to.equals(Address.zero())) {
|
|
743
|
+
throw new Revert('Invalid recipient');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Then use map
|
|
747
|
+
this.balances.set(to, amount);
|
|
748
|
+
}
|
|
749
|
+
```
|
|
750
|
+
|
|
751
|
+
### 4. Consider Overflow
|
|
752
|
+
|
|
753
|
+
```typescript
|
|
754
|
+
// Always use SafeMath when updating values
|
|
755
|
+
const current = this.balances.get(addr);
|
|
756
|
+
const newValue = SafeMath.add(current, amount); // Checks overflow
|
|
757
|
+
this.balances.set(addr, newValue);
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
---
|
|
761
|
+
|
|
762
|
+
**Navigation:**
|
|
763
|
+
- Previous: [Stored Maps](./stored-maps.md)
|
|
764
|
+
- Next: [Cross-Contract Calls](../advanced/cross-contract-calls.md)
|