@polkadot-api/forklift 0.1.0
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/README.md +394 -0
- package/bin/cli.js +388 -0
- package/bin/cli.js.map +1 -0
- package/dist/.papi/descriptors/dist/descriptors-CVixQzDI.js +27 -0
- package/dist/.papi/descriptors/dist/descriptors-CVixQzDI.js.map +1 -0
- package/dist/.papi/descriptors/dist/index.js +40 -0
- package/dist/.papi/descriptors/dist/index.js.map +1 -0
- package/dist/.papi/descriptors/dist/metadataTypes-OmVFeQs5.js +4 -0
- package/dist/.papi/descriptors/dist/metadataTypes-OmVFeQs5.js.map +1 -0
- package/dist/.papi/descriptors/dist/parachain_metadata-CQQZadL1.js +4 -0
- package/dist/.papi/descriptors/dist/parachain_metadata-CQQZadL1.js.map +1 -0
- package/dist/.papi/descriptors/dist/relay_metadata-BAI7pjXf.js +4 -0
- package/dist/.papi/descriptors/dist/relay_metadata-BAI7pjXf.js.map +1 -0
- package/dist/index.d.ts +64 -0
- package/dist/src/block-builder/create-block.js +232 -0
- package/dist/src/block-builder/create-block.js.map +1 -0
- package/dist/src/block-builder/para-enter.js +21 -0
- package/dist/src/block-builder/para-enter.js.map +1 -0
- package/dist/src/block-builder/set-validation-data.js +284 -0
- package/dist/src/block-builder/set-validation-data.js.map +1 -0
- package/dist/src/block-builder/slot-utils.js +68 -0
- package/dist/src/block-builder/slot-utils.js.map +1 -0
- package/dist/src/block-builder/timestamp.js +20 -0
- package/dist/src/block-builder/timestamp.js.map +1 -0
- package/dist/src/chain.js +334 -0
- package/dist/src/chain.js.map +1 -0
- package/dist/src/codecs.js +103 -0
- package/dist/src/codecs.js.map +1 -0
- package/dist/src/executor.js +87 -0
- package/dist/src/executor.js.map +1 -0
- package/dist/src/forklift.js +177 -0
- package/dist/src/forklift.js.map +1 -0
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/logger.js +11 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/prequeries.js +19 -0
- package/dist/src/prequeries.js.map +1 -0
- package/dist/src/rpc/archive_v1.js +223 -0
- package/dist/src/rpc/archive_v1.js.map +1 -0
- package/dist/src/rpc/chainHead_v1.js +383 -0
- package/dist/src/rpc/chainHead_v1.js.map +1 -0
- package/dist/src/rpc/chainSpec_v1.js +14 -0
- package/dist/src/rpc/chainSpec_v1.js.map +1 -0
- package/dist/src/rpc/dev.js +32 -0
- package/dist/src/rpc/dev.js.map +1 -0
- package/dist/src/rpc/forklift_xcm.js +99 -0
- package/dist/src/rpc/forklift_xcm.js.map +1 -0
- package/dist/src/rpc/rpc_utils.js +20 -0
- package/dist/src/rpc/rpc_utils.js.map +1 -0
- package/dist/src/rpc/transaction_v1.js +13 -0
- package/dist/src/rpc/transaction_v1.js.map +1 -0
- package/dist/src/serve.js +88 -0
- package/dist/src/serve.js.map +1 -0
- package/dist/src/source.js +125 -0
- package/dist/src/source.js.map +1 -0
- package/dist/src/storage.js +223 -0
- package/dist/src/storage.js.map +1 -0
- package/dist/src/txPool.js +177 -0
- package/dist/src/txPool.js.map +1 -0
- package/dist/src/xcm.js +292 -0
- package/dist/src/xcm.js.map +1 -0
- package/package.json +61 -0
package/README.md
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
# forklift
|
|
2
|
+
|
|
3
|
+
A tool for forking live Substrate/Polkadot-SDK chains locally, built natively on [polkadot-api](https://github.com/polkadot-api/polkadot-api).
|
|
4
|
+
|
|
5
|
+
Forklift is inspired by [@acala-network/chopsticks](https://github.com/AcalaNetwork/chopsticks), but it was built primarily for testing workflows that need multiple live branches of the chain, making it possible to simulate forks, reorgs, and pruned branches.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Multiple live branches of the chain, including competing forks, reorgs, and pruned branches
|
|
10
|
+
- Immutable merkle-trie-backed storage with structural sharing between blocks
|
|
11
|
+
- Relay / parachain wiring helpers for local XCM testing
|
|
12
|
+
- Native `polkadot-api` implementation without `polkadot-js`
|
|
13
|
+
- Based on the new chainHead_v1 / archive_v1 JSON-RPC methods
|
|
14
|
+
- YAML-based CLI config for single-chain and multi-chain setups
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pnpm i @polkadot-api/forklift
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Then run:
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
pnpm forklift --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## CLI
|
|
29
|
+
|
|
30
|
+
Forklift can be started in two ways:
|
|
31
|
+
|
|
32
|
+
1. Directly from a remote endpoint
|
|
33
|
+
2. From a YAML config file
|
|
34
|
+
|
|
35
|
+
### Direct mode
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
forklift <url> [options]
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Arguments:
|
|
42
|
+
|
|
43
|
+
| Argument | Description |
|
|
44
|
+
| -------- | --------------------------------- |
|
|
45
|
+
| `url` | WebSocket URL of the node to fork |
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
|
|
49
|
+
| Option | Description | Default |
|
|
50
|
+
| ------------------------- | ------------------------------------------------------------- | ---------------- |
|
|
51
|
+
| `-b, --block <block>` | Block number or block hash to fork from | latest finalized |
|
|
52
|
+
| `-p, --port <port>` | Preferred local WebSocket port | `3000` |
|
|
53
|
+
| `-c, --config <file>` | Load a YAML config instead of using direct mode | |
|
|
54
|
+
| `-l, --log-level <level>` | Log level: `trace`, `debug`, `info`, `warn`, `error`, `fatal` | `info` |
|
|
55
|
+
|
|
56
|
+
Examples:
|
|
57
|
+
|
|
58
|
+
```sh
|
|
59
|
+
# Fork the latest finalized block
|
|
60
|
+
forklift wss://rpc.polkadot.io
|
|
61
|
+
|
|
62
|
+
# Fork a specific block number
|
|
63
|
+
forklift wss://rpc.polkadot.io --block 22000000
|
|
64
|
+
|
|
65
|
+
# Fork a specific block hash
|
|
66
|
+
forklift wss://rpc.polkadot.io --block 0xabc123...
|
|
67
|
+
|
|
68
|
+
# Prefer a specific local port
|
|
69
|
+
forklift wss://rpc.polkadot.io --port 9000
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The forklift CLI exposes a JSON-RPC WebSocket endpoint. In direct mode that is typically:
|
|
73
|
+
|
|
74
|
+
```txt
|
|
75
|
+
ws://localhost:3000
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
If the requested port is already in use, forklift will try the next free port.
|
|
79
|
+
|
|
80
|
+
## YAML Config
|
|
81
|
+
|
|
82
|
+
For anything beyond a single fork, the YAML config is the intended interface.
|
|
83
|
+
|
|
84
|
+
```sh
|
|
85
|
+
forklift --config forklift.yml
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The config supports either:
|
|
89
|
+
|
|
90
|
+
- a single chain at the root level
|
|
91
|
+
- multiple named chains under `chains:`
|
|
92
|
+
|
|
93
|
+
### Single-chain config
|
|
94
|
+
|
|
95
|
+
```yaml
|
|
96
|
+
endpoint: wss://rpc.polkadot.io
|
|
97
|
+
block: 22000000
|
|
98
|
+
port: 3000
|
|
99
|
+
options:
|
|
100
|
+
buildBlockMode:
|
|
101
|
+
timer: 100
|
|
102
|
+
finalizeMode:
|
|
103
|
+
timer: 2000
|
|
104
|
+
storage:
|
|
105
|
+
- key: 0x3a636f6465
|
|
106
|
+
value: null
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Multi-chain config
|
|
110
|
+
|
|
111
|
+
```yaml
|
|
112
|
+
chains:
|
|
113
|
+
relay:
|
|
114
|
+
endpoint: wss://rpc.polkadot.io
|
|
115
|
+
port: 3000
|
|
116
|
+
|
|
117
|
+
assetHub:
|
|
118
|
+
endpoint: wss://sys.ibp.network/asset-hub-polkadot
|
|
119
|
+
port: 3001
|
|
120
|
+
parachainOf: relay
|
|
121
|
+
|
|
122
|
+
bridgeHub:
|
|
123
|
+
endpoint: wss://sys.ibp.network/bridge-hub-polkadot
|
|
124
|
+
port: 3002
|
|
125
|
+
parachainOf: relay
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
In multi-chain mode:
|
|
129
|
+
|
|
130
|
+
- each entry under `chains:` starts its own local fork
|
|
131
|
+
- `parachainOf: <name>` declares that a chain should be attached to another local chain as its relay
|
|
132
|
+
- chains that share the same relay are also attached to each other as siblings
|
|
133
|
+
|
|
134
|
+
That makes the config suitable for relay/parachain and parachain/parachain XCM testing setups.
|
|
135
|
+
|
|
136
|
+
## Config Fields
|
|
137
|
+
|
|
138
|
+
Each chain config supports the following fields:
|
|
139
|
+
|
|
140
|
+
| Field | Type | Description |
|
|
141
|
+
| ------------- | -------- | ----------------------------------------------- | --------------------------------------------------- |
|
|
142
|
+
| `endpoint` | `string | string[]` | Remote WebSocket endpoint or endpoints to fork from |
|
|
143
|
+
| `block` | `number | string` | Optional block number or block hash to fork from |
|
|
144
|
+
| `port` | `number` | Preferred local WebSocket port |
|
|
145
|
+
| `parachainOf` | `string` | Name of the relay chain in a multi-chain config |
|
|
146
|
+
| `options` | `object` | Forklift runtime options |
|
|
147
|
+
| `storage` | `array` | Storage overrides applied after startup |
|
|
148
|
+
|
|
149
|
+
### `options`
|
|
150
|
+
|
|
151
|
+
`options` maps closely to the programmatic `ForkliftOptions`.
|
|
152
|
+
|
|
153
|
+
```yaml
|
|
154
|
+
options:
|
|
155
|
+
disableOnIdle: false
|
|
156
|
+
buildBlockMode:
|
|
157
|
+
timer: 100
|
|
158
|
+
finalizeMode:
|
|
159
|
+
timer: 2000
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Supported values:
|
|
163
|
+
|
|
164
|
+
- `disableOnIdle: boolean`
|
|
165
|
+
Disables `on_idle` hooks during block production. Some runtimes might perform actions that take a long time as they perform multiple serial storage queries. Setting this option to `true` disables that hook, which can increase the speed blocks can be produced.
|
|
166
|
+
|
|
167
|
+
- `buildBlockMode`
|
|
168
|
+
Controls when new blocks are built after transactions arrive.
|
|
169
|
+
|
|
170
|
+
Manual mode:
|
|
171
|
+
|
|
172
|
+
```yaml
|
|
173
|
+
buildBlockMode: manual
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
Timer mode:
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
buildBlockMode:
|
|
180
|
+
timer: 100
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- `finalizeMode`
|
|
184
|
+
Controls when built blocks are finalized.
|
|
185
|
+
|
|
186
|
+
Manual mode:
|
|
187
|
+
|
|
188
|
+
```yaml
|
|
189
|
+
finalizeMode: manual
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Timer mode:
|
|
193
|
+
|
|
194
|
+
```yaml
|
|
195
|
+
finalizeMode:
|
|
196
|
+
timer: 2000
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Notes:
|
|
200
|
+
|
|
201
|
+
- `manual` means forklift only changes state when you explicitly drive it
|
|
202
|
+
- `{ timer: 0 }` is allowed and means immediate scheduling
|
|
203
|
+
- if `port` is omitted, forklift will choose a free port automatically
|
|
204
|
+
|
|
205
|
+
## Storage Overrides
|
|
206
|
+
|
|
207
|
+
The `storage` section is applied after the local server has started and the initial block is available.
|
|
208
|
+
|
|
209
|
+
Forklift supports two storage override forms.
|
|
210
|
+
|
|
211
|
+
### Raw form
|
|
212
|
+
|
|
213
|
+
Use raw SCALE-encoded keys and values directly:
|
|
214
|
+
|
|
215
|
+
```yaml
|
|
216
|
+
storage:
|
|
217
|
+
- key: 0x1234...
|
|
218
|
+
value: 0xabcd...
|
|
219
|
+
- key: 0x5678...
|
|
220
|
+
value: null
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Use `null` to delete or clear a storage entry.
|
|
224
|
+
|
|
225
|
+
### Decoded form
|
|
226
|
+
|
|
227
|
+
Use pallet / storage names and let the CLI encode the key and value from metadata:
|
|
228
|
+
|
|
229
|
+
```yaml
|
|
230
|
+
storage:
|
|
231
|
+
- pallet: System
|
|
232
|
+
entry: Account
|
|
233
|
+
key:
|
|
234
|
+
- 14GjNs7Lw7nVbJrL8aL8m8m4vY2mQ2L9mQf8u2YpK9nQx7aD
|
|
235
|
+
value:
|
|
236
|
+
providers: 1
|
|
237
|
+
consumers: 0
|
|
238
|
+
sufficients: 0
|
|
239
|
+
data:
|
|
240
|
+
free: 100_0_000_000_000n
|
|
241
|
+
reserved: 0n
|
|
242
|
+
frozen: 0n
|
|
243
|
+
flags: 0
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Notes:
|
|
247
|
+
|
|
248
|
+
- `key` must be an array in decoded form, even if the storage entry takes a single key
|
|
249
|
+
- big integers can be written as strings ending in `n`, for example `1000000000000n`
|
|
250
|
+
- underscores are accepted in numeric strings for readability
|
|
251
|
+
- if a storage item, key, or value cannot be encoded against the chain metadata, forklift logs the error and skips that override
|
|
252
|
+
|
|
253
|
+
## Programmatic API
|
|
254
|
+
|
|
255
|
+
You can also create a chain from code:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import { forklift } from "@polkadot-api/forklift";
|
|
259
|
+
import { Enum } from "polkadot-api";
|
|
260
|
+
|
|
261
|
+
const polkadot = forklift(
|
|
262
|
+
{
|
|
263
|
+
type: "remote",
|
|
264
|
+
value: {
|
|
265
|
+
url: "wss://rpc.polkadot.io",
|
|
266
|
+
atBlock: 22000000,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
buildBlockMode: Enum("timer", 100),
|
|
271
|
+
finalizeMode: Enum("timer", 2000),
|
|
272
|
+
disableOnIdle: false,
|
|
273
|
+
}
|
|
274
|
+
);
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
The forklift instance then has a property `serve` which is a `JsonRpcProvider` - This is [an unopinionated](https://papi.how/providers/enhancers#enhancers) interface that serves JSON-RPC connections, and can be plugged directly into polkadot-api:
|
|
278
|
+
|
|
279
|
+
```ts
|
|
280
|
+
import { forklift } from "@polkadot-api/forklift";
|
|
281
|
+
import { createClient } from "polkadot-api";
|
|
282
|
+
|
|
283
|
+
const polkadot = forklift(/* … */);
|
|
284
|
+
const client = createClient(polkadot.serve);
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Or, given it's a simple interface, it's simple to expose that to a WS. For instance, using bun:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
import { forklift } from "@polkadot-api/forklift";
|
|
291
|
+
const polkadot = forklift(/* … */);
|
|
292
|
+
|
|
293
|
+
Bun.serve({
|
|
294
|
+
fetch(req, server) {
|
|
295
|
+
// Al WS connections start with a HTTP request, we tell bun to upgrade the connection to a WS
|
|
296
|
+
const success = server.upgrade(req, { data: {} });
|
|
297
|
+
if (success) {
|
|
298
|
+
return undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// handle HTTP request normally
|
|
302
|
+
return new Response("Nothing to see here, move along");
|
|
303
|
+
},
|
|
304
|
+
websocket: {
|
|
305
|
+
data: {} as any,
|
|
306
|
+
open(ws) {
|
|
307
|
+
// When the WS opens we call the JsonRpcProvider to open a connection, and wire up incoming messages from forklift to send them out to the WS
|
|
308
|
+
ws.data.connection = forklift.serve((msg) =>
|
|
309
|
+
ws.send(JSON.stringify(msg))
|
|
310
|
+
);
|
|
311
|
+
},
|
|
312
|
+
close(ws) {
|
|
313
|
+
// When it closes we just close the connection
|
|
314
|
+
ws.data.connection.disconnect();
|
|
315
|
+
},
|
|
316
|
+
async message(ws, message) {
|
|
317
|
+
// When we receive a message we just pass it down to forklift
|
|
318
|
+
ws.data.connection.send(JSON.parse(message as string));
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
### `Forklift` interface
|
|
325
|
+
|
|
326
|
+
```ts
|
|
327
|
+
interface Forklift {
|
|
328
|
+
serve: JsonRpcProvider;
|
|
329
|
+
|
|
330
|
+
newBlock(opts?: Partial<NewBlockOptions>): Promise<HexString>;
|
|
331
|
+
changeBest(hash: HexString): Promise<void>;
|
|
332
|
+
changeFinalized(hash: HexString): Promise<void>;
|
|
333
|
+
setStorage(
|
|
334
|
+
hash: HexString,
|
|
335
|
+
changes: Record<string, Uint8Array>
|
|
336
|
+
): Promise<void>;
|
|
337
|
+
getStorageDiff(
|
|
338
|
+
hash: HexString,
|
|
339
|
+
baseHash?: HexString
|
|
340
|
+
): Promise<
|
|
341
|
+
Record<string, { value: Uint8Array | null; prev?: Uint8Array | null }>
|
|
342
|
+
>;
|
|
343
|
+
changeOptions(opts: Partial<ForkliftOptions>): void;
|
|
344
|
+
destroy(): void;
|
|
345
|
+
}
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
### Block production modes
|
|
349
|
+
|
|
350
|
+
`buildBlockMode` controls when new blocks are produced:
|
|
351
|
+
|
|
352
|
+
- `Enum("manual")`: only explicit `newBlock()` calls produce blocks
|
|
353
|
+
- `Enum("timer", ms)`: automatically produce a block after a transaction arrives
|
|
354
|
+
|
|
355
|
+
`finalizeMode` controls when blocks are finalized:
|
|
356
|
+
|
|
357
|
+
- `Enum("manual")`: only explicit `changeFinalized()` calls finalize blocks
|
|
358
|
+
- `Enum("timer", ms)`: automatically finalize a block after it is built
|
|
359
|
+
|
|
360
|
+
### Producing forks
|
|
361
|
+
|
|
362
|
+
Pass a `parent` hash to branch from any existing block:
|
|
363
|
+
|
|
364
|
+
```ts
|
|
365
|
+
const base = await f.newBlock();
|
|
366
|
+
|
|
367
|
+
const forkA = await f.newBlock({ parent: base, type: "fork" });
|
|
368
|
+
const forkB = await f.newBlock({ parent: base, type: "fork" });
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Storage overrides from code
|
|
372
|
+
|
|
373
|
+
```ts
|
|
374
|
+
await f.setStorage(hash, {
|
|
375
|
+
"0x...key": new Uint8Array([...value]),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
const diff = await f.getStorageDiff(hash);
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
## JSON-RPC Surface
|
|
382
|
+
|
|
383
|
+
Forklift serves a WebSocket JSON-RPC endpoint and currently includes methods in these groups:
|
|
384
|
+
|
|
385
|
+
- `archive_v1_*`
|
|
386
|
+
- `chainHead_v1_*`
|
|
387
|
+
- `chainSpec_v1_*`
|
|
388
|
+
- `transaction_v1_*`
|
|
389
|
+
- `dev_*`
|
|
390
|
+
- `forklift_xcm_*`
|
|
391
|
+
|
|
392
|
+
## Acknowledgements
|
|
393
|
+
|
|
394
|
+
Forklift is heavily inspired by [@acala-network/chopsticks](https://github.com/AcalaNetwork/chopsticks) and reuses its WASM executor package, [`@acala-network/chopsticks-executor`](https://github.com/AcalaNetwork/chopsticks), for local runtime execution.
|