@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,938 @@
|
|
|
1
|
+
# BytesWriter and BytesReader
|
|
2
|
+
|
|
3
|
+
`BytesWriter` and `BytesReader` handle binary serialization and deserialization. They're used for encoding return values, event data, and cross-contract communication.
|
|
4
|
+
|
|
5
|
+
## BytesWriter
|
|
6
|
+
|
|
7
|
+
### Overview
|
|
8
|
+
|
|
9
|
+
```typescript
|
|
10
|
+
import { BytesWriter, Address } from '@btc-vision/btc-runtime/runtime';
|
|
11
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
12
|
+
|
|
13
|
+
// Create writer with initial capacity
|
|
14
|
+
const writer = new BytesWriter(64);
|
|
15
|
+
|
|
16
|
+
// Write data
|
|
17
|
+
writer.writeAddress(recipient);
|
|
18
|
+
writer.writeU256(amount);
|
|
19
|
+
writer.writeBoolean(true);
|
|
20
|
+
|
|
21
|
+
// Get encoded bytes
|
|
22
|
+
const data: Uint8Array = writer.getBuffer();
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### BytesWriter/Reader Architecture
|
|
26
|
+
|
|
27
|
+
```mermaid
|
|
28
|
+
classDiagram
|
|
29
|
+
class BytesWriter {
|
|
30
|
+
-DataView buffer
|
|
31
|
+
-Uint8Array typedArray
|
|
32
|
+
-u32 currentOffset
|
|
33
|
+
+BytesWriter(length)
|
|
34
|
+
+writeU8(value)
|
|
35
|
+
+writeU16(value, be)
|
|
36
|
+
+writeU32(value, be)
|
|
37
|
+
+writeU64(value, be)
|
|
38
|
+
+writeU128(value, be)
|
|
39
|
+
+writeU256(value, be)
|
|
40
|
+
+writeAddress(addr)
|
|
41
|
+
+writeString(str)
|
|
42
|
+
+writeBytes(data)
|
|
43
|
+
+writeBoolean(bool)
|
|
44
|
+
+getBuffer() Uint8Array
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class BytesReader {
|
|
48
|
+
-DataView buffer
|
|
49
|
+
-i32 currentOffset
|
|
50
|
+
+BytesReader(bytes)
|
|
51
|
+
+readU8() u8
|
|
52
|
+
+readU16(be) u16
|
|
53
|
+
+readU32(be) u32
|
|
54
|
+
+readU64(be) u64
|
|
55
|
+
+readU128(be) u128
|
|
56
|
+
+readU256(be) u256
|
|
57
|
+
+readAddress() Address
|
|
58
|
+
+readString() string
|
|
59
|
+
+readBytes(length) Uint8Array
|
|
60
|
+
+readBoolean() bool
|
|
61
|
+
+hasMoreData() bool
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class DataView {
|
|
65
|
+
<<built-in>>
|
|
66
|
+
+getUint8()
|
|
67
|
+
+setUint8()
|
|
68
|
+
+getUint32()
|
|
69
|
+
+setUint32()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
BytesWriter --> DataView : uses
|
|
73
|
+
BytesReader --> DataView : uses
|
|
74
|
+
|
|
75
|
+
note for BytesWriter "Sequential write\nfixed-size buffer"
|
|
76
|
+
note for BytesReader "Sequential read\nwith position tracking"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Creating a BytesWriter
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
// With initial capacity (recommended - buffer does NOT auto-grow)
|
|
83
|
+
const writer = new BytesWriter(128);
|
|
84
|
+
|
|
85
|
+
// Note: Buffer does NOT grow dynamically - will throw Revert if exceeded
|
|
86
|
+
// Always pre-calculate required size or use generous initial capacity
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Writing Primitives
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
// Boolean
|
|
93
|
+
writer.writeBoolean(true); // 1 byte
|
|
94
|
+
|
|
95
|
+
// Unsigned integers
|
|
96
|
+
writer.writeU8(255); // 1 byte
|
|
97
|
+
writer.writeU16(65535); // 2 bytes
|
|
98
|
+
writer.writeU32(4294967295); // 4 bytes
|
|
99
|
+
writer.writeU64(value); // 8 bytes
|
|
100
|
+
writer.writeU128(value); // 16 bytes
|
|
101
|
+
writer.writeU256(value); // 32 bytes
|
|
102
|
+
|
|
103
|
+
// Signed integers
|
|
104
|
+
writer.writeI8(-128);
|
|
105
|
+
writer.writeI16(-32768);
|
|
106
|
+
writer.writeI32(-2147483648);
|
|
107
|
+
writer.writeI64(value);
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Writing Complex Types
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
// Address (32 bytes)
|
|
114
|
+
writer.writeAddress(address);
|
|
115
|
+
|
|
116
|
+
// String without length prefix
|
|
117
|
+
writer.writeString('Hello, World!');
|
|
118
|
+
|
|
119
|
+
// String with u32 length prefix
|
|
120
|
+
writer.writeStringWithLength('Hello, World!');
|
|
121
|
+
|
|
122
|
+
// Bytes without length prefix
|
|
123
|
+
writer.writeBytes(data);
|
|
124
|
+
|
|
125
|
+
// Bytes with u32 length prefix
|
|
126
|
+
writer.writeBytesWithLength(data);
|
|
127
|
+
|
|
128
|
+
// Selector (4 bytes, big-endian)
|
|
129
|
+
writer.writeSelector(selector);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Writing Arrays
|
|
133
|
+
|
|
134
|
+
All array methods use a u16 length prefix (max 65535 elements):
|
|
135
|
+
|
|
136
|
+
```typescript
|
|
137
|
+
// Address array (u16 length prefix + addresses)
|
|
138
|
+
writer.writeAddressArray(addresses);
|
|
139
|
+
|
|
140
|
+
// Numeric arrays (u16 length prefix + values)
|
|
141
|
+
writer.writeU8Array(u8Values);
|
|
142
|
+
writer.writeU16Array(u16Values);
|
|
143
|
+
writer.writeU32Array(u32Values);
|
|
144
|
+
writer.writeU64Array(u64Values);
|
|
145
|
+
writer.writeU128Array(u128Values);
|
|
146
|
+
writer.writeU256Array(u256Values);
|
|
147
|
+
|
|
148
|
+
// Array of variable-length buffers (u16 count, then u32 length + data for each)
|
|
149
|
+
writer.writeArrayOfBuffer(buffers);
|
|
150
|
+
|
|
151
|
+
// AddressMap<u256> (u16 count, then address + u256 pairs)
|
|
152
|
+
writer.writeAddressMapU256(addressMap);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Getting Results
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
// Get the full buffer
|
|
159
|
+
const buffer: Uint8Array = writer.getBuffer();
|
|
160
|
+
|
|
161
|
+
// Get current write offset (bytes written so far)
|
|
162
|
+
const offset: u32 = writer.getOffset();
|
|
163
|
+
|
|
164
|
+
// Get total buffer capacity
|
|
165
|
+
const capacity: u32 = writer.bufferLength();
|
|
166
|
+
|
|
167
|
+
// Convert to BytesReader for reading
|
|
168
|
+
const reader: BytesReader = writer.toBytesReader();
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
## BytesReader
|
|
172
|
+
|
|
173
|
+
### Overview
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { BytesReader, Address } from '@btc-vision/btc-runtime/runtime';
|
|
177
|
+
import { u256 } from '@btc-vision/as-bignum/assembly';
|
|
178
|
+
|
|
179
|
+
// Create reader from buffer
|
|
180
|
+
const reader = new BytesReader(data);
|
|
181
|
+
|
|
182
|
+
// Read data in same order it was written
|
|
183
|
+
const recipient: Address = reader.readAddress();
|
|
184
|
+
const amount: u256 = reader.readU256();
|
|
185
|
+
const flag: bool = reader.readBoolean();
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Creating a BytesReader
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
// From Uint8Array
|
|
192
|
+
const reader = new BytesReader(buffer);
|
|
193
|
+
|
|
194
|
+
// From BytesWriter
|
|
195
|
+
const reader = writer.toBytesReader();
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Reading Primitives
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
// Boolean
|
|
202
|
+
const flag: bool = reader.readBoolean();
|
|
203
|
+
|
|
204
|
+
// Unsigned integers
|
|
205
|
+
const u8val: u8 = reader.readU8();
|
|
206
|
+
const u16val: u16 = reader.readU16();
|
|
207
|
+
const u32val: u32 = reader.readU32();
|
|
208
|
+
const u64val: u64 = reader.readU64();
|
|
209
|
+
const u128val: u128 = reader.readU128();
|
|
210
|
+
const u256val: u256 = reader.readU256();
|
|
211
|
+
|
|
212
|
+
// Signed integers
|
|
213
|
+
const i8val: i8 = reader.readI8();
|
|
214
|
+
const i16val: i16 = reader.readI16();
|
|
215
|
+
const i32val: i32 = reader.readI32();
|
|
216
|
+
const i64val: i64 = reader.readI64();
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### Reading Complex Types
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
// Address (32 bytes)
|
|
223
|
+
const addr: Address = reader.readAddress();
|
|
224
|
+
|
|
225
|
+
// String with known length (bytes read, zeroStop = true)
|
|
226
|
+
const name: string = reader.readString(32); // reads up to 32 bytes, stops at null
|
|
227
|
+
|
|
228
|
+
// String with u32 length prefix
|
|
229
|
+
const name2: string = reader.readStringWithLength();
|
|
230
|
+
|
|
231
|
+
// Bytes with known length
|
|
232
|
+
const data: Uint8Array = reader.readBytes(64);
|
|
233
|
+
|
|
234
|
+
// Bytes with u32 length prefix
|
|
235
|
+
const data2: Uint8Array = reader.readBytesWithLength();
|
|
236
|
+
|
|
237
|
+
// Selector (4 bytes, big-endian)
|
|
238
|
+
const selector: Selector = reader.readSelector();
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Reading Arrays
|
|
242
|
+
|
|
243
|
+
All array methods expect a u16 length prefix:
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
// Address array
|
|
247
|
+
const addresses: Address[] = reader.readAddressArray();
|
|
248
|
+
|
|
249
|
+
// Numeric arrays
|
|
250
|
+
const u8Values: u8[] = reader.readU8Array();
|
|
251
|
+
const u16Values: u16[] = reader.readU16Array();
|
|
252
|
+
const u32Values: u32[] = reader.readU32Array();
|
|
253
|
+
const u64Values: u64[] = reader.readU64Array();
|
|
254
|
+
const u128Values: u128[] = reader.readU128Array();
|
|
255
|
+
const u256Values: u256[] = reader.readU256Array();
|
|
256
|
+
|
|
257
|
+
// Array of variable-length buffers
|
|
258
|
+
const buffers: Uint8Array[] = reader.readArrayOfBuffer();
|
|
259
|
+
|
|
260
|
+
// AddressMap<u256>
|
|
261
|
+
const addressMap: AddressMap<u256> = reader.readAddressMapU256();
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Position Management
|
|
265
|
+
|
|
266
|
+
```typescript
|
|
267
|
+
// Get current read offset
|
|
268
|
+
const offset: i32 = reader.getOffset();
|
|
269
|
+
|
|
270
|
+
// Set read position (must be within buffer bounds)
|
|
271
|
+
reader.setOffset(32);
|
|
272
|
+
|
|
273
|
+
// Get total buffer length
|
|
274
|
+
const total: i32 = reader.byteLength;
|
|
275
|
+
|
|
276
|
+
// Verify enough bytes remain for next read
|
|
277
|
+
reader.verifyEnd(offset + 32); // Throws Revert if not enough bytes
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
## Data Format
|
|
281
|
+
|
|
282
|
+
### Encoding Summary
|
|
283
|
+
|
|
284
|
+
| Type | Size | Format |
|
|
285
|
+
|------|------|--------|
|
|
286
|
+
| `bool` | 1 | 0 or 1 |
|
|
287
|
+
| `u8`/`i8` | 1 | Raw byte |
|
|
288
|
+
| `u16`/`i16` | 2 | Big-endian (default) |
|
|
289
|
+
| `u32`/`i32` | 4 | Big-endian (default) |
|
|
290
|
+
| `u64`/`i64` | 8 | Big-endian (default) |
|
|
291
|
+
| `u128`/`i128` | 16 | Big-endian (default) |
|
|
292
|
+
| `u256` | 32 | Big-endian (default) |
|
|
293
|
+
| `Address` | 32 | Raw bytes |
|
|
294
|
+
| `Selector` | 4 | Big-endian (u32) |
|
|
295
|
+
| `string` | 4 + n | Length prefix (u32 BE) + UTF-8 |
|
|
296
|
+
| `bytes` | 4 + n | Length prefix (u32 BE) + raw |
|
|
297
|
+
| `arrays` | 2 + n | Length prefix (u16 BE) + elements |
|
|
298
|
+
|
|
299
|
+
**Note:** All multi-byte integers default to big-endian. Pass `be = false` for little-endian:
|
|
300
|
+
```typescript
|
|
301
|
+
writer.writeU32(value, false); // Little-endian
|
|
302
|
+
reader.readU32(false); // Little-endian
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Data Encoding Flow
|
|
306
|
+
|
|
307
|
+
```mermaid
|
|
308
|
+
---
|
|
309
|
+
config:
|
|
310
|
+
theme: dark
|
|
311
|
+
---
|
|
312
|
+
flowchart LR
|
|
313
|
+
A["Data"] --> B{"Type?"}
|
|
314
|
+
B -->|"Primitive"| C["Fixed Size"]
|
|
315
|
+
B -->|"String/Array"| D["Length Prefix + Data"]
|
|
316
|
+
C --> E["Write to Buffer"]
|
|
317
|
+
D --> E
|
|
318
|
+
E --> F["Increment Offset"]
|
|
319
|
+
F --> G["Final Buffer"]
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### String Encoding Example
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
"Hello" encoded:
|
|
326
|
+
| 05 00 00 00 | 48 65 6c 6c 6f |
|
|
327
|
+
| length=5 | "Hello" UTF-8 |
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### Array Encoding Example
|
|
331
|
+
|
|
332
|
+
```
|
|
333
|
+
[1, 2, 3] as u256 encoded:
|
|
334
|
+
| 03 00 00 00 | 01 00...00 | 02 00...00 | 03 00...00 |
|
|
335
|
+
| length=3 | 1 (32 bytes)| 2 (32 bytes)| 3 (32 bytes)|
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
## Common Patterns
|
|
339
|
+
|
|
340
|
+
### Return Values
|
|
341
|
+
|
|
342
|
+
```typescript
|
|
343
|
+
// Simple return
|
|
344
|
+
public getValue(_calldata: Calldata): BytesWriter {
|
|
345
|
+
const writer = new BytesWriter(32);
|
|
346
|
+
writer.writeU256(this._value.value);
|
|
347
|
+
return writer;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Multiple return values
|
|
351
|
+
public getState(_calldata: Calldata): BytesWriter {
|
|
352
|
+
const writer = new BytesWriter(96);
|
|
353
|
+
writer.writeU256(this._value1.value);
|
|
354
|
+
writer.writeU256(this._value2.value);
|
|
355
|
+
writer.writeAddress(this._owner.value);
|
|
356
|
+
return writer;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// No return
|
|
360
|
+
public setState(calldata: Calldata): BytesWriter {
|
|
361
|
+
// ... do work ...
|
|
362
|
+
return new BytesWriter(0); // Empty response
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Event Encoding
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
export class SwapEvent extends NetEvent {
|
|
370
|
+
constructor(
|
|
371
|
+
public readonly user: Address,
|
|
372
|
+
public readonly tokenIn: Address,
|
|
373
|
+
public readonly tokenOut: Address,
|
|
374
|
+
public readonly amountIn: u256,
|
|
375
|
+
public readonly amountOut: u256
|
|
376
|
+
) {
|
|
377
|
+
super('Swap');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
protected override encodeData(writer: BytesWriter): void {
|
|
381
|
+
writer.writeAddress(this.user);
|
|
382
|
+
writer.writeAddress(this.tokenIn);
|
|
383
|
+
writer.writeAddress(this.tokenOut);
|
|
384
|
+
writer.writeU256(this.amountIn);
|
|
385
|
+
writer.writeU256(this.amountOut);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Cross-Contract Call Data
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// Define method selectors (sha256 first 4 bytes of method signature)
|
|
394
|
+
const TRANSFER_SELECTOR: u32 = 0xa9059cbb; // transfer(address,uint256)
|
|
395
|
+
|
|
396
|
+
// Encode call to another contract
|
|
397
|
+
private encodeTransfer(to: Address, amount: u256): Uint8Array {
|
|
398
|
+
const writer = new BytesWriter(68);
|
|
399
|
+
writer.writeSelector(TRANSFER_SELECTOR);
|
|
400
|
+
writer.writeAddress(to);
|
|
401
|
+
writer.writeU256(amount);
|
|
402
|
+
return writer.getBuffer();
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Make the call
|
|
406
|
+
const calldata = this.encodeTransfer(recipient, tokenAmount);
|
|
407
|
+
const result = Blockchain.call(tokenContract, calldata, true);
|
|
408
|
+
|
|
409
|
+
// Decode result
|
|
410
|
+
if (result.success) {
|
|
411
|
+
const reader = new BytesReader(result.data);
|
|
412
|
+
const success: bool = reader.readBoolean();
|
|
413
|
+
}
|
|
414
|
+
```
|
|
415
|
+
|
|
416
|
+
### Cross-Contract Call Sequence
|
|
417
|
+
|
|
418
|
+
```mermaid
|
|
419
|
+
sequenceDiagram
|
|
420
|
+
participant C1 as Calling Contract
|
|
421
|
+
participant W as BytesWriter
|
|
422
|
+
participant BC as Blockchain
|
|
423
|
+
participant C2 as Target Contract
|
|
424
|
+
participant R as BytesReader
|
|
425
|
+
|
|
426
|
+
C1->>W: new BytesWriter(68)
|
|
427
|
+
C1->>W: writeSelector(0xa9059cbb)
|
|
428
|
+
Note over W: [4 bytes] Selector
|
|
429
|
+
C1->>W: writeAddress(recipient)
|
|
430
|
+
Note over W: [4 + 32 bytes] Selector + Address
|
|
431
|
+
C1->>W: writeU256(amount)
|
|
432
|
+
Note over W: [4 + 32 + 32 bytes] Complete calldata
|
|
433
|
+
W-->>C1: getBuffer()
|
|
434
|
+
|
|
435
|
+
C1->>BC: call(tokenContract, calldata, true)
|
|
436
|
+
BC->>C2: Execute with calldata
|
|
437
|
+
C2->>R: new BytesReader(calldata)
|
|
438
|
+
R->>C2: readSelector() → 0xa9059cbb
|
|
439
|
+
R->>C2: readAddress() → recipient
|
|
440
|
+
R->>C2: readU256() → amount
|
|
441
|
+
C2->>C2: Execute transfer logic
|
|
442
|
+
C2->>W: new BytesWriter(1)
|
|
443
|
+
C2->>W: writeBoolean(true)
|
|
444
|
+
W-->>C2: getBuffer()
|
|
445
|
+
C2-->>BC: Return encoded result
|
|
446
|
+
BC-->>C1: result.data
|
|
447
|
+
|
|
448
|
+
C1->>R: new BytesReader(result.data)
|
|
449
|
+
R-->>C1: readBoolean() → true
|
|
450
|
+
|
|
451
|
+
Note over C1,C2: Total calldata: 68 bytes<br/>Selector(4) + Address(32) + u256(32)
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Struct-like Encoding
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// Define a struct-like type
|
|
458
|
+
class Order {
|
|
459
|
+
maker: Address;
|
|
460
|
+
taker: Address;
|
|
461
|
+
makerAmount: u256;
|
|
462
|
+
takerAmount: u256;
|
|
463
|
+
expiry: u64;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Encode a "struct"
|
|
467
|
+
private encodeOrder(
|
|
468
|
+
maker: Address,
|
|
469
|
+
taker: Address,
|
|
470
|
+
makerAmount: u256,
|
|
471
|
+
takerAmount: u256,
|
|
472
|
+
expiry: u64
|
|
473
|
+
): Uint8Array {
|
|
474
|
+
const writer = new BytesWriter(104);
|
|
475
|
+
writer.writeAddress(maker);
|
|
476
|
+
writer.writeAddress(taker);
|
|
477
|
+
writer.writeU256(makerAmount);
|
|
478
|
+
writer.writeU256(takerAmount);
|
|
479
|
+
writer.writeU64(expiry);
|
|
480
|
+
return writer.getBuffer();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Decode a "struct"
|
|
484
|
+
private decodeOrder(data: Uint8Array): Order {
|
|
485
|
+
const reader = new BytesReader(data);
|
|
486
|
+
const order = new Order();
|
|
487
|
+
order.maker = reader.readAddress();
|
|
488
|
+
order.taker = reader.readAddress();
|
|
489
|
+
order.makerAmount = reader.readU256();
|
|
490
|
+
order.takerAmount = reader.readU256();
|
|
491
|
+
order.expiry = reader.readU64();
|
|
492
|
+
return order;
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
### Struct Encoding Memory Layout
|
|
497
|
+
|
|
498
|
+
```mermaid
|
|
499
|
+
graph LR
|
|
500
|
+
subgraph Order["Order Struct Encoding (104 bytes)"]
|
|
501
|
+
direction LR
|
|
502
|
+
A["Offset 0-31<br/>maker Address<br/>32 bytes"]
|
|
503
|
+
B["Offset 32-63<br/>taker Address<br/>32 bytes"]
|
|
504
|
+
C["Offset 64-95<br/>makerAmount u256<br/>32 bytes"]
|
|
505
|
+
D["Offset 96-127<br/>takerAmount u256<br/>32 bytes"]
|
|
506
|
+
E["Offset 128-135<br/>expiry u64<br/>8 bytes"]
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
A --> B --> C --> D --> E
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
## Size Limits
|
|
513
|
+
|
|
514
|
+
### Array Limits
|
|
515
|
+
|
|
516
|
+
Maximum array length: **65,535 elements**
|
|
517
|
+
|
|
518
|
+
```typescript
|
|
519
|
+
// Writing large arrays
|
|
520
|
+
if (items.length > 65535) {
|
|
521
|
+
throw new Revert('Array too large');
|
|
522
|
+
}
|
|
523
|
+
writer.writeU256Array(items);
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
### Event Size Limit
|
|
527
|
+
|
|
528
|
+
Maximum event data: **352 bytes**
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
protected override encodeData(writer: BytesWriter): void {
|
|
532
|
+
// Calculate total size
|
|
533
|
+
// 32 + 32 + 32 + 32 + 8 = 136 bytes - OK
|
|
534
|
+
writer.writeAddress(this.addr1); // 32
|
|
535
|
+
writer.writeAddress(this.addr2); // 32
|
|
536
|
+
writer.writeU256(this.amount1); // 32
|
|
537
|
+
writer.writeU256(this.amount2); // 32
|
|
538
|
+
writer.writeU64(this.timestamp); // 8
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
## Solidity vs OPNet Comparison
|
|
543
|
+
|
|
544
|
+
### Encoding/Decoding Comparison Table
|
|
545
|
+
|
|
546
|
+
| Feature | Solidity | OPNet |
|
|
547
|
+
|---------|----------|-------|
|
|
548
|
+
| **Encode function** | `abi.encode(...)` | `BytesWriter` methods |
|
|
549
|
+
| **Decode function** | `abi.decode(data, (types))` | `BytesReader` methods |
|
|
550
|
+
| **Packed encoding** | `abi.encodePacked(...)` | Default behavior (no padding) |
|
|
551
|
+
| **Encode with selector** | `abi.encodeWithSelector(sel, ...)` | `writer.writeSelector(sel); writer.write...()` |
|
|
552
|
+
| **Encode with signature** | `abi.encodeWithSignature(sig, ...)` | Manual selector + params |
|
|
553
|
+
| **Byte order** | Big-endian | Big-endian (default) |
|
|
554
|
+
| **Padding** | 32-byte aligned | No padding (native sizes) |
|
|
555
|
+
| **Return encoding** | Automatic | Manual via BytesWriter |
|
|
556
|
+
|
|
557
|
+
### Type Encoding Comparison
|
|
558
|
+
|
|
559
|
+
| Solidity Encoding | OPNet Encoding |
|
|
560
|
+
|-------------------|----------------|
|
|
561
|
+
| `abi.encode(uint256)` | `writer.writeU256(value)` |
|
|
562
|
+
| `abi.encode(uint128)` | `writer.writeU128(value)` |
|
|
563
|
+
| `abi.encode(uint64)` | `writer.writeU64(value)` |
|
|
564
|
+
| `abi.encode(uint32)` | `writer.writeU32(value)` |
|
|
565
|
+
| `abi.encode(uint16)` | `writer.writeU16(value)` |
|
|
566
|
+
| `abi.encode(uint8)` | `writer.writeU8(value)` |
|
|
567
|
+
| `abi.encode(bool)` | `writer.writeBoolean(value)` |
|
|
568
|
+
| `abi.encode(address)` | `writer.writeAddress(addr)` |
|
|
569
|
+
| `abi.encode(bytes32)` | `writer.writeBytes(data)` |
|
|
570
|
+
| `abi.encode(string)` | `writer.writeString(str)` |
|
|
571
|
+
| `abi.encode(bytes)` | `writer.writeBytes(data)` |
|
|
572
|
+
| `abi.encode(address[])` | `writer.writeAddressArray(addrs)` |
|
|
573
|
+
| `abi.encode(uint256[])` | `writer.writeU256Array(values)` |
|
|
574
|
+
|
|
575
|
+
### Side-by-Side Code Examples
|
|
576
|
+
|
|
577
|
+
#### Basic Encoding
|
|
578
|
+
|
|
579
|
+
```solidity
|
|
580
|
+
// Solidity
|
|
581
|
+
bytes memory data = abi.encode(recipient, amount);
|
|
582
|
+
bytes memory packed = abi.encodePacked(recipient, amount);
|
|
583
|
+
```
|
|
584
|
+
|
|
585
|
+
```typescript
|
|
586
|
+
// OPNet
|
|
587
|
+
const writer = new BytesWriter(64); // 32 + 32 bytes
|
|
588
|
+
writer.writeAddress(recipient);
|
|
589
|
+
writer.writeU256(amount);
|
|
590
|
+
const data: Uint8Array = writer.getBuffer();
|
|
591
|
+
// OPNet encoding is similar to encodePacked (no padding)
|
|
592
|
+
```
|
|
593
|
+
|
|
594
|
+
#### Basic Decoding
|
|
595
|
+
|
|
596
|
+
```solidity
|
|
597
|
+
// Solidity
|
|
598
|
+
(address to, uint256 amount) = abi.decode(data, (address, uint256));
|
|
599
|
+
```
|
|
600
|
+
|
|
601
|
+
```typescript
|
|
602
|
+
// OPNet
|
|
603
|
+
const reader = new BytesReader(data);
|
|
604
|
+
const to: Address = reader.readAddress();
|
|
605
|
+
const amount: u256 = reader.readU256();
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
#### Encoding Multiple Values
|
|
609
|
+
|
|
610
|
+
```solidity
|
|
611
|
+
// Solidity
|
|
612
|
+
function encodeTransferData(
|
|
613
|
+
address from,
|
|
614
|
+
address to,
|
|
615
|
+
uint256 amount,
|
|
616
|
+
string memory memo
|
|
617
|
+
) public pure returns (bytes memory) {
|
|
618
|
+
return abi.encode(from, to, amount, memo);
|
|
619
|
+
}
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
```typescript
|
|
623
|
+
// OPNet
|
|
624
|
+
function encodeTransferData(
|
|
625
|
+
from: Address,
|
|
626
|
+
to: Address,
|
|
627
|
+
amount: u256,
|
|
628
|
+
memo: string
|
|
629
|
+
): Uint8Array {
|
|
630
|
+
// Calculate size: 32 + 32 + 32 + (4 + memo.length)
|
|
631
|
+
const writer = new BytesWriter(100 + memo.length);
|
|
632
|
+
writer.writeAddress(from);
|
|
633
|
+
writer.writeAddress(to);
|
|
634
|
+
writer.writeU256(amount);
|
|
635
|
+
writer.writeString(memo);
|
|
636
|
+
return writer.getBuffer();
|
|
637
|
+
}
|
|
638
|
+
```
|
|
639
|
+
|
|
640
|
+
#### Encoding with Function Selector
|
|
641
|
+
|
|
642
|
+
```solidity
|
|
643
|
+
// Solidity
|
|
644
|
+
bytes4 selector = bytes4(keccak256("transfer(address,uint256)"));
|
|
645
|
+
bytes memory callData = abi.encodeWithSelector(selector, to, amount);
|
|
646
|
+
// Or
|
|
647
|
+
bytes memory callData = abi.encodeWithSignature("transfer(address,uint256)", to, amount);
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
```typescript
|
|
651
|
+
// OPNet
|
|
652
|
+
const TRANSFER_SELECTOR: Selector = Selector.from("transfer(address,uint256)");
|
|
653
|
+
const writer = new BytesWriter(68); // 4 + 32 + 32
|
|
654
|
+
writer.writeSelector(TRANSFER_SELECTOR);
|
|
655
|
+
writer.writeAddress(to);
|
|
656
|
+
writer.writeU256(amount);
|
|
657
|
+
const callData: Uint8Array = writer.getBuffer();
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
#### Return Value Encoding
|
|
661
|
+
|
|
662
|
+
```solidity
|
|
663
|
+
// Solidity - Automatic return encoding
|
|
664
|
+
function getInfo() public view returns (address, uint256, bool) {
|
|
665
|
+
return (owner, balance, isActive);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// ABI automatically encodes the return tuple
|
|
669
|
+
```
|
|
670
|
+
|
|
671
|
+
```typescript
|
|
672
|
+
// OPNet - Manual return encoding
|
|
673
|
+
public getInfo(_calldata: Calldata): BytesWriter {
|
|
674
|
+
const writer = new BytesWriter(65); // 32 + 32 + 1
|
|
675
|
+
writer.writeAddress(this._owner.value);
|
|
676
|
+
writer.writeU256(this._balance.value);
|
|
677
|
+
writer.writeBoolean(this._isActive.value);
|
|
678
|
+
return writer;
|
|
679
|
+
}
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
#### Decoding Return Values
|
|
683
|
+
|
|
684
|
+
```solidity
|
|
685
|
+
// Solidity
|
|
686
|
+
(bool success, bytes memory returnData) = target.call(callData);
|
|
687
|
+
if (success) {
|
|
688
|
+
(address addr, uint256 amount) = abi.decode(returnData, (address, uint256));
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
// OPNet
|
|
694
|
+
const result = Blockchain.call(target, callData, true);
|
|
695
|
+
if (result.success) {
|
|
696
|
+
const reader = new BytesReader(result.data);
|
|
697
|
+
const addr: Address = reader.readAddress();
|
|
698
|
+
const amount: u256 = reader.readU256();
|
|
699
|
+
}
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
#### Encoding Structs
|
|
703
|
+
|
|
704
|
+
```solidity
|
|
705
|
+
// Solidity
|
|
706
|
+
struct Order {
|
|
707
|
+
address maker;
|
|
708
|
+
address taker;
|
|
709
|
+
uint256 makerAmount;
|
|
710
|
+
uint256 takerAmount;
|
|
711
|
+
uint64 expiry;
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function encodeOrder(Order memory order) public pure returns (bytes memory) {
|
|
715
|
+
return abi.encode(order.maker, order.taker, order.makerAmount, order.takerAmount, order.expiry);
|
|
716
|
+
}
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
```typescript
|
|
720
|
+
// OPNet
|
|
721
|
+
class Order {
|
|
722
|
+
maker: Address;
|
|
723
|
+
taker: Address;
|
|
724
|
+
makerAmount: u256;
|
|
725
|
+
takerAmount: u256;
|
|
726
|
+
expiry: u64;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function encodeOrder(order: Order): Uint8Array {
|
|
730
|
+
const writer = new BytesWriter(136); // 32+32+32+32+8
|
|
731
|
+
writer.writeAddress(order.maker);
|
|
732
|
+
writer.writeAddress(order.taker);
|
|
733
|
+
writer.writeU256(order.makerAmount);
|
|
734
|
+
writer.writeU256(order.takerAmount);
|
|
735
|
+
writer.writeU64(order.expiry);
|
|
736
|
+
return writer.getBuffer();
|
|
737
|
+
}
|
|
738
|
+
```
|
|
739
|
+
|
|
740
|
+
#### Event Data Encoding
|
|
741
|
+
|
|
742
|
+
```solidity
|
|
743
|
+
// Solidity - Events are indexed/non-indexed
|
|
744
|
+
event Transfer(address indexed from, address indexed to, uint256 amount);
|
|
745
|
+
|
|
746
|
+
function _transfer(address from, address to, uint256 amount) internal {
|
|
747
|
+
// ... transfer logic
|
|
748
|
+
emit Transfer(from, to, amount); // Automatic encoding
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
```typescript
|
|
753
|
+
// OPNet - Custom event class with manual encoding
|
|
754
|
+
export class TransferEvent extends NetEvent {
|
|
755
|
+
constructor(
|
|
756
|
+
public readonly from: Address,
|
|
757
|
+
public readonly to: Address,
|
|
758
|
+
public readonly amount: u256
|
|
759
|
+
) {
|
|
760
|
+
super('Transfer');
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
protected override encodeData(writer: BytesWriter): void {
|
|
764
|
+
writer.writeAddress(this.from);
|
|
765
|
+
writer.writeAddress(this.to);
|
|
766
|
+
writer.writeU256(this.amount);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Usage
|
|
771
|
+
this.emitEvent(new TransferEvent(from, to, amount));
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
#### Low-Level Bytes Manipulation
|
|
775
|
+
|
|
776
|
+
```solidity
|
|
777
|
+
// Solidity
|
|
778
|
+
function extractSelector(bytes calldata data) public pure returns (bytes4) {
|
|
779
|
+
return bytes4(data[:4]);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function extractAddress(bytes calldata data, uint offset) public pure returns (address) {
|
|
783
|
+
return abi.decode(data[offset:offset+32], (address));
|
|
784
|
+
}
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
```typescript
|
|
788
|
+
// OPNet
|
|
789
|
+
function extractSelector(data: Uint8Array): Selector {
|
|
790
|
+
const reader = new BytesReader(data);
|
|
791
|
+
return reader.readSelector();
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
function extractAddress(data: Uint8Array, offset: u32): Address {
|
|
795
|
+
const reader = new BytesReader(data);
|
|
796
|
+
// Skip to offset by reading and discarding bytes
|
|
797
|
+
for (let i: u32 = 0; i < offset; i++) {
|
|
798
|
+
reader.readU8();
|
|
799
|
+
}
|
|
800
|
+
return reader.readAddress();
|
|
801
|
+
}
|
|
802
|
+
```
|
|
803
|
+
|
|
804
|
+
### Size Comparison (Encoding Overhead)
|
|
805
|
+
|
|
806
|
+
| Data | Solidity abi.encode | Solidity encodePacked | OPNet |
|
|
807
|
+
|------|---------------------|----------------------|-------|
|
|
808
|
+
| `bool` | 32 bytes | 1 byte | 1 byte |
|
|
809
|
+
| `uint8` | 32 bytes | 1 byte | 1 byte |
|
|
810
|
+
| `uint16` | 32 bytes | 2 bytes | 2 bytes |
|
|
811
|
+
| `uint32` | 32 bytes | 4 bytes | 4 bytes |
|
|
812
|
+
| `uint64` | 32 bytes | 8 bytes | 8 bytes |
|
|
813
|
+
| `uint128` | 32 bytes | 16 bytes | 16 bytes |
|
|
814
|
+
| `uint256` | 32 bytes | 32 bytes | 32 bytes |
|
|
815
|
+
| `address` | 32 bytes | 20 bytes | 32 bytes |
|
|
816
|
+
| `(addr, u256, bool)` | 96 bytes | 53 bytes | 65 bytes |
|
|
817
|
+
|
|
818
|
+
### Key Differences Summary
|
|
819
|
+
|
|
820
|
+
| Aspect | Solidity | OPNet |
|
|
821
|
+
|--------|----------|-------|
|
|
822
|
+
| **Encoding approach** | `abi.encode()` function | BytesWriter object methods |
|
|
823
|
+
| **Decoding approach** | `abi.decode()` with type tuple | BytesReader sequential reads |
|
|
824
|
+
| **Packed encoding** | `abi.encodePacked()` (separate) | Default (always packed) |
|
|
825
|
+
| **Selector encoding** | `abi.encodeWithSelector()` | `writeSelector()` + params |
|
|
826
|
+
| **Dynamic sizing** | Automatic | Pre-calculate or auto-grow |
|
|
827
|
+
| **Return values** | Automatic encoding | Manual BytesWriter construction |
|
|
828
|
+
| **Error on overflow** | Reverts | Reverts |
|
|
829
|
+
| **Endianness** | Big-endian | Big-endian (default) |
|
|
830
|
+
|
|
831
|
+
### Migration Patterns
|
|
832
|
+
|
|
833
|
+
#### From Solidity `abi.encode`
|
|
834
|
+
|
|
835
|
+
```solidity
|
|
836
|
+
// Solidity
|
|
837
|
+
bytes memory encoded = abi.encode(addr, amount, flag);
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
```typescript
|
|
841
|
+
// OPNet equivalent
|
|
842
|
+
const writer = new BytesWriter(65); // 32 + 32 + 1
|
|
843
|
+
writer.writeAddress(addr);
|
|
844
|
+
writer.writeU256(amount);
|
|
845
|
+
writer.writeBoolean(flag);
|
|
846
|
+
const encoded = writer.getBuffer();
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
#### From Solidity `abi.decode`
|
|
850
|
+
|
|
851
|
+
```solidity
|
|
852
|
+
// Solidity
|
|
853
|
+
(address addr, uint256 amount, bool flag) = abi.decode(data, (address, uint256, bool));
|
|
854
|
+
```
|
|
855
|
+
|
|
856
|
+
```typescript
|
|
857
|
+
// OPNet equivalent
|
|
858
|
+
const reader = new BytesReader(data);
|
|
859
|
+
const addr = reader.readAddress();
|
|
860
|
+
const amount = reader.readU256();
|
|
861
|
+
const flag = reader.readBoolean();
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
#### From Solidity `abi.encodeWithSelector`
|
|
865
|
+
|
|
866
|
+
```solidity
|
|
867
|
+
// Solidity
|
|
868
|
+
bytes memory data = abi.encodeWithSelector(
|
|
869
|
+
IERC20.transfer.selector,
|
|
870
|
+
recipient,
|
|
871
|
+
amount
|
|
872
|
+
);
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
```typescript
|
|
876
|
+
// OPNet equivalent
|
|
877
|
+
const TRANSFER_SELECTOR: Selector = Selector.from("transfer(address,uint256)");
|
|
878
|
+
const writer = new BytesWriter(68);
|
|
879
|
+
writer.writeSelector(TRANSFER_SELECTOR);
|
|
880
|
+
writer.writeAddress(recipient);
|
|
881
|
+
writer.writeU256(amount);
|
|
882
|
+
const data = writer.getBuffer();
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
## Best Practices
|
|
886
|
+
|
|
887
|
+
### 1. Pre-calculate Capacity
|
|
888
|
+
|
|
889
|
+
```typescript
|
|
890
|
+
// Calculate required size
|
|
891
|
+
const size = 32 + 32 + 4; // address + u256 + u32
|
|
892
|
+
const writer = new BytesWriter(size);
|
|
893
|
+
|
|
894
|
+
// Better than growing the buffer
|
|
895
|
+
```
|
|
896
|
+
|
|
897
|
+
### 2. Consistent Order
|
|
898
|
+
|
|
899
|
+
```typescript
|
|
900
|
+
// Always read in the same order as written
|
|
901
|
+
// Document the order clearly
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Encoded format:
|
|
905
|
+
* - to: Address (32 bytes)
|
|
906
|
+
* - amount: u256 (32 bytes)
|
|
907
|
+
* - data: bytes (4 + n bytes)
|
|
908
|
+
*/
|
|
909
|
+
```
|
|
910
|
+
|
|
911
|
+
### 3. Validate Before Reading
|
|
912
|
+
|
|
913
|
+
```typescript
|
|
914
|
+
// Check if enough data remains
|
|
915
|
+
if (reader.remaining < 32) {
|
|
916
|
+
throw new Revert('Insufficient data');
|
|
917
|
+
}
|
|
918
|
+
const value = reader.readU256();
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### 4. Handle Variable-Length Data Last
|
|
922
|
+
|
|
923
|
+
```typescript
|
|
924
|
+
// Fixed-size fields first
|
|
925
|
+
writer.writeAddress(addr);
|
|
926
|
+
writer.writeU256(amount);
|
|
927
|
+
writer.writeU64(timestamp);
|
|
928
|
+
|
|
929
|
+
// Variable-size fields last
|
|
930
|
+
writer.writeString(message);
|
|
931
|
+
writer.writeBytes(data);
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
---
|
|
935
|
+
|
|
936
|
+
**Navigation:**
|
|
937
|
+
- Previous: [Calldata](./calldata.md)
|
|
938
|
+
- Next: [Stored Primitives](../storage/stored-primitives.md)
|