@acala-network/chopsticks 0.8.5-4 → 0.8.5-6
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/chopsticks.js +1 -1
- package/{lib → dist/cjs}/plugins/dry-run/dry-run-preimage.js +1 -3
- package/{lib → dist/cjs}/plugins/run-block/index.js +1 -4
- package/{lib → dist/cjs}/utils/generate-html-diff.js +1 -1
- package/dist/esm/cli-options.js +40 -0
- package/dist/esm/cli.js +90 -0
- package/dist/esm/context.js +86 -0
- package/dist/esm/plugins/decode-key/index.js +23 -0
- package/dist/esm/plugins/dry-run/cli.js +42 -0
- package/dist/esm/plugins/dry-run/dry-run-extrinsic.js +29 -0
- package/dist/esm/plugins/dry-run/dry-run-preimage.js +92 -0
- package/dist/esm/plugins/dry-run/rpc.js +101 -0
- package/dist/esm/plugins/index.js +25 -0
- package/dist/esm/plugins/new-block/index.js +67 -0
- package/dist/esm/plugins/run-block/index.js +176 -0
- package/dist/esm/plugins/set-block-build-mode/index.js +25 -0
- package/dist/esm/plugins/set-head/index.js +31 -0
- package/dist/esm/plugins/set-runtime-log-level/index.js +24 -0
- package/dist/esm/plugins/set-storage/index.js +38 -0
- package/dist/esm/plugins/time-travel/index.js +23 -0
- package/dist/esm/plugins/try-runtime/index.js +54 -0
- package/{lib/plugins/types.d.ts → dist/esm/plugins/types.js} +3 -3
- package/dist/esm/rpc/index.js +26 -0
- package/dist/esm/schema/index.js +53 -0
- package/dist/esm/server.js +164 -0
- package/dist/esm/setup-with-server.js +22 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/utils/decoder.js +15 -0
- package/dist/esm/utils/generate-html-diff.js +16 -0
- package/dist/esm/utils/open-html.js +5 -0
- package/dist/esm/utils/override.js +43 -0
- package/dist/esm/utils/tunnel.js +11 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/logger.d.ts +1 -0
- package/dist/types/plugins/dry-run/index.d.ts +2 -0
- package/dist/types/plugins/types.d.ts +11 -0
- package/dist/types/utils/index.d.ts +4 -0
- package/package.json +23 -14
- /package/{lib → dist/cjs}/cli-options.js +0 -0
- /package/{lib → dist/cjs}/cli.js +0 -0
- /package/{lib → dist/cjs}/context.js +0 -0
- /package/{lib → dist/cjs}/index.js +0 -0
- /package/{lib → dist/cjs}/logger.js +0 -0
- /package/{lib → dist/cjs}/plugins/decode-key/index.js +0 -0
- /package/{lib → dist/cjs}/plugins/dry-run/cli.js +0 -0
- /package/{lib → dist/cjs}/plugins/dry-run/dry-run-extrinsic.js +0 -0
- /package/{lib → dist/cjs}/plugins/dry-run/index.js +0 -0
- /package/{lib → dist/cjs}/plugins/dry-run/rpc.js +0 -0
- /package/{lib → dist/cjs}/plugins/index.js +0 -0
- /package/{lib → dist/cjs}/plugins/new-block/index.js +0 -0
- /package/{lib → dist/cjs}/plugins/set-block-build-mode/index.js +0 -0
- /package/{lib → dist/cjs}/plugins/set-head/index.js +0 -0
- /package/{lib → dist/cjs}/plugins/set-runtime-log-level/index.js +0 -0
- /package/{lib → dist/cjs}/plugins/set-storage/index.js +0 -0
- /package/{lib → dist/cjs}/plugins/time-travel/index.js +0 -0
- /package/{lib → dist/cjs}/plugins/try-runtime/index.js +0 -0
- /package/{lib → dist/cjs}/plugins/types.js +0 -0
- /package/{lib → dist/cjs}/rpc/index.js +0 -0
- /package/{lib → dist/cjs}/schema/index.js +0 -0
- /package/{lib → dist/cjs}/server.js +0 -0
- /package/{lib → dist/cjs}/setup-with-server.js +0 -0
- /package/{lib → dist/cjs}/types.js +0 -0
- /package/{lib → dist/cjs}/utils/decoder.js +0 -0
- /package/{lib → dist/cjs}/utils/index.js +0 -0
- /package/{lib → dist/cjs}/utils/open-html.js +0 -0
- /package/{lib → dist/cjs}/utils/override.js +0 -0
- /package/{lib → dist/cjs}/utils/tunnel.js +0 -0
- /package/{lib/index.d.ts → dist/esm/index.js} +0 -0
- /package/{lib/logger.d.ts → dist/esm/logger.js} +0 -0
- /package/{lib/plugins/dry-run/index.d.ts → dist/esm/plugins/dry-run/index.js} +0 -0
- /package/{lib/utils/index.d.ts → dist/esm/utils/index.js} +0 -0
- /package/{lib → dist/types}/cli-options.d.ts +0 -0
- /package/{lib → dist/types}/cli.d.ts +0 -0
- /package/{lib → dist/types}/context.d.ts +0 -0
- /package/{lib → dist/types}/plugins/decode-key/index.d.ts +0 -0
- /package/{lib → dist/types}/plugins/dry-run/cli.d.ts +0 -0
- /package/{lib → dist/types}/plugins/dry-run/dry-run-extrinsic.d.ts +0 -0
- /package/{lib → dist/types}/plugins/dry-run/dry-run-preimage.d.ts +0 -0
- /package/{lib → dist/types}/plugins/dry-run/rpc.d.ts +0 -0
- /package/{lib → dist/types}/plugins/index.d.ts +0 -0
- /package/{lib → dist/types}/plugins/new-block/index.d.ts +0 -0
- /package/{lib → dist/types}/plugins/run-block/index.d.ts +0 -0
- /package/{lib → dist/types}/plugins/set-block-build-mode/index.d.ts +0 -0
- /package/{lib → dist/types}/plugins/set-head/index.d.ts +0 -0
- /package/{lib → dist/types}/plugins/set-runtime-log-level/index.d.ts +0 -0
- /package/{lib → dist/types}/plugins/set-storage/index.d.ts +0 -0
- /package/{lib → dist/types}/plugins/time-travel/index.d.ts +0 -0
- /package/{lib → dist/types}/plugins/try-runtime/index.d.ts +0 -0
- /package/{lib → dist/types}/rpc/index.d.ts +0 -0
- /package/{lib → dist/types}/schema/index.d.ts +0 -0
- /package/{lib → dist/types}/server.d.ts +0 -0
- /package/{lib → dist/types}/setup-with-server.d.ts +0 -0
- /package/{lib → dist/types}/types.d.ts +0 -0
- /package/{lib → dist/types}/utils/decoder.d.ts +0 -0
- /package/{lib → dist/types}/utils/generate-html-diff.d.ts +0 -0
- /package/{lib → dist/types}/utils/open-html.d.ts +0 -0
- /package/{lib → dist/types}/utils/override.d.ts +0 -0
- /package/{lib → dist/types}/utils/tunnel.d.ts +0 -0
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
import { Block, decodeKeyValue, printRuntimeLogs, runTask, taskHandler } from '@acala-network/chopsticks-core';
|
|
5
|
+
import { defaultOptions, mockOptions } from '../../cli-options';
|
|
6
|
+
import { generateHtmlDiffPreviewFile } from '../../utils/generate-html-diff';
|
|
7
|
+
import { openHtml } from '../../utils/open-html';
|
|
8
|
+
import { setupContext } from '../../context';
|
|
9
|
+
export const cli = (y) => {
|
|
10
|
+
y.command('run-block', 'Replay a block', (yargs) => yargs.options({
|
|
11
|
+
...defaultOptions,
|
|
12
|
+
...mockOptions,
|
|
13
|
+
'output-path': {
|
|
14
|
+
desc: 'File path to print output',
|
|
15
|
+
string: true,
|
|
16
|
+
},
|
|
17
|
+
html: {
|
|
18
|
+
desc: 'Generate html with storage diff',
|
|
19
|
+
},
|
|
20
|
+
open: {
|
|
21
|
+
desc: 'Open generated html',
|
|
22
|
+
},
|
|
23
|
+
}), async (argv) => {
|
|
24
|
+
const context = await setupContext(argv, true);
|
|
25
|
+
const header = await context.chain.head.header;
|
|
26
|
+
const block = context.chain.head;
|
|
27
|
+
const parent = await block.parentBlock;
|
|
28
|
+
if (!parent)
|
|
29
|
+
throw Error('cant find parent block');
|
|
30
|
+
const wasm = await parent.wasm;
|
|
31
|
+
const calls = [['Core_initialize_block', [header.toHex()]]];
|
|
32
|
+
for (const extrinsic of await block.extrinsics) {
|
|
33
|
+
calls.push(['BlockBuilder_apply_extrinsic', [extrinsic]]);
|
|
34
|
+
}
|
|
35
|
+
calls.push(['BlockBuilder_finalize_block', []]);
|
|
36
|
+
const result = await runTask({
|
|
37
|
+
wasm,
|
|
38
|
+
calls,
|
|
39
|
+
mockSignatureHost: false,
|
|
40
|
+
allowUnresolvedImports: false,
|
|
41
|
+
runtimeLogLevel: argv.runtimeLogLevel || 0,
|
|
42
|
+
}, taskHandler(parent));
|
|
43
|
+
if ('Error' in result) {
|
|
44
|
+
throw new Error(result.Error);
|
|
45
|
+
}
|
|
46
|
+
printRuntimeLogs(result.Call.runtimeLogs);
|
|
47
|
+
if (argv.html) {
|
|
48
|
+
const filePath = await generateHtmlDiffPreviewFile(parent, result.Call.storageDiff, block.hash);
|
|
49
|
+
console.log(`Generated preview ${filePath}`);
|
|
50
|
+
if (argv.open) {
|
|
51
|
+
openHtml(filePath);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (argv.outputPath) {
|
|
55
|
+
writeFileSync(argv.outputPath, JSON.stringify(result, null, 2));
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
console.dir(result, { depth: null, colors: false });
|
|
59
|
+
}
|
|
60
|
+
process.exit(0);
|
|
61
|
+
});
|
|
62
|
+
};
|
|
63
|
+
const zHex = z.custom((val) => /^0x\w+$/.test(val));
|
|
64
|
+
const zHash = z.string().length(66).and(zHex);
|
|
65
|
+
const schema = z.object({
|
|
66
|
+
includeRaw: z.boolean().optional(),
|
|
67
|
+
includeParsed: z.boolean().optional(),
|
|
68
|
+
includeBlockDetails: z.boolean().optional(),
|
|
69
|
+
parent: zHash.optional(),
|
|
70
|
+
block: z.object({
|
|
71
|
+
header: z.any(),
|
|
72
|
+
extrinsics: z.array(zHex),
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
export const name = 'runBlock';
|
|
76
|
+
/**
|
|
77
|
+
* Run a set of extrinsics on top of a block and get the storage diff
|
|
78
|
+
* and optionally the parsed storage diff and block details.
|
|
79
|
+
* NOTE: The extrinsics should include inherents or tranasctions may have unexpected results.
|
|
80
|
+
*
|
|
81
|
+
* This function is a dev rpc handler. Use `dev_runBlock` as the method name when calling it.
|
|
82
|
+
*/
|
|
83
|
+
export const rpc = async ({ chain }, [params]) => {
|
|
84
|
+
const { includeRaw, includeParsed, includeBlockDetails, parent, block } = schema.parse(params);
|
|
85
|
+
const includeRawStorage = includeRaw ?? true;
|
|
86
|
+
const parentBlock = await chain.getBlock(parent);
|
|
87
|
+
if (!parentBlock) {
|
|
88
|
+
throw Error(`Invalid block hash ${parent}`);
|
|
89
|
+
}
|
|
90
|
+
const registry = await parentBlock.registry;
|
|
91
|
+
const header = registry.createType('Header', block.header);
|
|
92
|
+
const wasm = await parentBlock.wasm;
|
|
93
|
+
const blockNumber = parentBlock.number + 1;
|
|
94
|
+
const hash = `0x${Math.round(Math.random() * 100000000)
|
|
95
|
+
.toString(16)
|
|
96
|
+
.padEnd(64, '0')}`;
|
|
97
|
+
const newBlock = new Block(chain, blockNumber, hash, parentBlock, {
|
|
98
|
+
header,
|
|
99
|
+
extrinsics: [],
|
|
100
|
+
storage: parentBlock.storage,
|
|
101
|
+
});
|
|
102
|
+
const resp = {
|
|
103
|
+
phases: [],
|
|
104
|
+
};
|
|
105
|
+
const run = async (fn, args) => {
|
|
106
|
+
const result = await runTask({
|
|
107
|
+
wasm,
|
|
108
|
+
calls: [[fn, args]],
|
|
109
|
+
mockSignatureHost: false,
|
|
110
|
+
allowUnresolvedImports: false,
|
|
111
|
+
runtimeLogLevel: 5,
|
|
112
|
+
}, taskHandler(newBlock));
|
|
113
|
+
if ('Error' in result) {
|
|
114
|
+
throw new Error(result.Error);
|
|
115
|
+
}
|
|
116
|
+
const resp = {};
|
|
117
|
+
const raw = result.Call.storageDiff;
|
|
118
|
+
newBlock.pushStorageLayer().setAll(raw);
|
|
119
|
+
if (includeRawStorage) {
|
|
120
|
+
resp.raw = raw;
|
|
121
|
+
}
|
|
122
|
+
if (includeParsed) {
|
|
123
|
+
const meta = await newBlock.meta;
|
|
124
|
+
const parsed = {};
|
|
125
|
+
for (const [key, value] of raw) {
|
|
126
|
+
_.merge(parsed, decodeKeyValue(meta, newBlock, key, value, false));
|
|
127
|
+
}
|
|
128
|
+
// clear events because it can be stupidly large and redudant
|
|
129
|
+
if (parsed['system']?.['events']) {
|
|
130
|
+
delete parsed['system']['events'];
|
|
131
|
+
}
|
|
132
|
+
resp.parsed = parsed;
|
|
133
|
+
}
|
|
134
|
+
resp.logs = result.Call.runtimeLogs;
|
|
135
|
+
return resp;
|
|
136
|
+
};
|
|
137
|
+
const resInit = await run('Core_initialize_block', [header.toHex()]);
|
|
138
|
+
resp.phases.push({ phase: 'Initialization', ...resInit });
|
|
139
|
+
for (const extrinsic of block.extrinsics) {
|
|
140
|
+
const res = await run('BlockBuilder_apply_extrinsic', [extrinsic]);
|
|
141
|
+
resp.phases.push({ phase: resp.phases.length - 1, ...res });
|
|
142
|
+
}
|
|
143
|
+
const resFinalize = await run('BlockBuilder_finalize_block', []);
|
|
144
|
+
resp.phases.push({ phase: 'Finalization', ...resFinalize });
|
|
145
|
+
if (includeBlockDetails) {
|
|
146
|
+
const meta = await newBlock.meta;
|
|
147
|
+
const registry = await newBlock.registry;
|
|
148
|
+
const timestamp = await newBlock.read('u64', meta.query.timestamp.now);
|
|
149
|
+
const events = await newBlock.read('Vec<EventRecord>', meta.query.system.events);
|
|
150
|
+
const parsedEvents = events?.map((event) => ({
|
|
151
|
+
phase: event.phase.isApplyExtrinsic ? event.phase.asApplyExtrinsic.toNumber() : event.phase.toString(),
|
|
152
|
+
section: event.event.section,
|
|
153
|
+
method: event.event.method,
|
|
154
|
+
args: event.event.data.map((arg) => arg.toJSON()),
|
|
155
|
+
}));
|
|
156
|
+
const extrinsics = block.extrinsics.map((extrinsic, idx) => {
|
|
157
|
+
const parsed = registry.createType('GenericExtrinsic', extrinsic);
|
|
158
|
+
const resultEvent = events?.find(({ event, phase }) => event.section === 'system' &&
|
|
159
|
+
(event.method === 'ExtrinsicSuccess' || event.method === 'ExtrinsicFailed') &&
|
|
160
|
+
phase.isApplyExtrinsic &&
|
|
161
|
+
phase.asApplyExtrinsic.eq(idx));
|
|
162
|
+
return {
|
|
163
|
+
section: parsed.method.section,
|
|
164
|
+
method: parsed.method.method,
|
|
165
|
+
args: parsed.method.args.map((arg) => arg.toJSON()),
|
|
166
|
+
success: resultEvent?.event.method === 'ExtrinsicSuccess',
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
resp.blockDetails = {
|
|
170
|
+
timestamp: timestamp?.toString(),
|
|
171
|
+
events: parsedEvents,
|
|
172
|
+
extrinsics,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return resp;
|
|
176
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { BuildBlockMode, ResponseError } from '@acala-network/chopsticks-core';
|
|
2
|
+
import { defaultLogger } from '../../logger';
|
|
3
|
+
/**
|
|
4
|
+
* Set a build block mode. See [BuildBlockMode](../core/enums/BuildBlockMode).
|
|
5
|
+
*
|
|
6
|
+
* This function is a dev rpc handler. Use `dev_setBlockBuildMode` as the method name when calling it.
|
|
7
|
+
*
|
|
8
|
+
* @param context - The context object of the rpc handler
|
|
9
|
+
* @param params - The parameters of the rpc handler
|
|
10
|
+
*
|
|
11
|
+
* @example Set build block mode to instant
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { WsProvider } from '@polkadot/rpc-provider'
|
|
14
|
+
* import { BuildBlockMode } from '@acala-network/chopsticks-core'
|
|
15
|
+
* const ws = new WsProvider(`ws://localhost:8000`)
|
|
16
|
+
* await ws.send('dev_setBlockBuildMode', [BuildBlockMode.Instant])
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export const rpc = async (context, [mode]) => {
|
|
20
|
+
defaultLogger.debug({ mode }, 'dev_setBlockBuildMode');
|
|
21
|
+
if (BuildBlockMode[mode] === undefined) {
|
|
22
|
+
throw new ResponseError(1, `Invalid mode ${mode}`);
|
|
23
|
+
}
|
|
24
|
+
context.chain.txPool.mode = mode;
|
|
25
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ResponseError } from '@acala-network/chopsticks-core';
|
|
2
|
+
/**
|
|
3
|
+
* Set head.
|
|
4
|
+
*
|
|
5
|
+
* This function is a dev rpc handler. Use `dev_setHead` as the method name when calling it.
|
|
6
|
+
*
|
|
7
|
+
* @param context - The context object of the rpc handler
|
|
8
|
+
* @param hashOrNumber - The block hash or number to set as head
|
|
9
|
+
*
|
|
10
|
+
* @example Set head to block 1000000
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { WsProvider } from '@polkadot/rpc-provider'
|
|
13
|
+
* const ws = new WsProvider(`ws://localhost:8000`)
|
|
14
|
+
* await ws.send('dev_setHead', [1000000])
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export const rpc = async (context, [hashOrNumber]) => {
|
|
18
|
+
let block;
|
|
19
|
+
if (typeof hashOrNumber === 'number') {
|
|
20
|
+
const blockNumber = hashOrNumber > 0 ? hashOrNumber : context.chain.head.number + hashOrNumber;
|
|
21
|
+
block = await context.chain.getBlockAt(blockNumber);
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
block = await context.chain.getBlock(hashOrNumber);
|
|
25
|
+
}
|
|
26
|
+
if (!block) {
|
|
27
|
+
throw new ResponseError(1, `Block not found ${hashOrNumber}`);
|
|
28
|
+
}
|
|
29
|
+
await context.chain.setHead(block);
|
|
30
|
+
return block.hash;
|
|
31
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { ResponseError } from '@acala-network/chopsticks-core';
|
|
2
|
+
import { defaultLogger } from '../../logger';
|
|
3
|
+
/**
|
|
4
|
+
* Set runtime log level.
|
|
5
|
+
*
|
|
6
|
+
* This function is a dev rpc handler. Use `dev_setRuntimeLogLevel` as the method name when calling it.
|
|
7
|
+
*
|
|
8
|
+
* @param context - The context object of the rpc handler
|
|
9
|
+
* @param runtimeLogLevel - The runtime log level to set
|
|
10
|
+
*
|
|
11
|
+
* @example Set runtime log level to 1
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { WsProvider } from '@polkadot/rpc-provider'
|
|
14
|
+
* const ws = new WsProvider(`ws://localhost:8000`)
|
|
15
|
+
* await ws.send('dev_setRuntimeLogLevel', [1])
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export const rpc = async (context, [runtimeLogLevel]) => {
|
|
19
|
+
defaultLogger.debug({ runtimeLogLevel }, 'dev_setRuntimeLogLevel');
|
|
20
|
+
if (typeof runtimeLogLevel !== 'number') {
|
|
21
|
+
throw new ResponseError(1, `Invalid runtimeLogLevel ${runtimeLogLevel}`);
|
|
22
|
+
}
|
|
23
|
+
context.chain.runtimeLogLevel = runtimeLogLevel;
|
|
24
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { ResponseError, setStorage } from '@acala-network/chopsticks-core';
|
|
2
|
+
import { defaultLogger } from '../../logger';
|
|
3
|
+
/**
|
|
4
|
+
* Set storage values.
|
|
5
|
+
*
|
|
6
|
+
* This function is a dev rpc handler. Use `dev_setStorage` as the method name when calling it.
|
|
7
|
+
*
|
|
8
|
+
* @param context - The context object of the rpc handler
|
|
9
|
+
* @param params - The parameters of the rpc handler
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { WsProvider } from '@polkadot/rpc-provider'
|
|
14
|
+
* import { Keyring } from '@polkadot/keyring'
|
|
15
|
+
*
|
|
16
|
+
* const ws = new WsProvider(`ws://localhost:8000`)
|
|
17
|
+
* const keyring = new Keyring({ type: 'ed25519' })
|
|
18
|
+
* const bob = keyring.addFromUri('//Bob')
|
|
19
|
+
*
|
|
20
|
+
* const storage = {
|
|
21
|
+
* System: {
|
|
22
|
+
* Account: [[[bob.address], { data: { free: 100000 }, nonce: 1 }]],
|
|
23
|
+
* },
|
|
24
|
+
* }
|
|
25
|
+
* await ws.send('dev_setStorage', [storage])
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
export const rpc = async (context, params) => {
|
|
29
|
+
const [values, blockHash] = params;
|
|
30
|
+
const hash = await setStorage(context.chain, values, blockHash).catch((error) => {
|
|
31
|
+
throw new ResponseError(1, error.toString());
|
|
32
|
+
});
|
|
33
|
+
defaultLogger.debug({
|
|
34
|
+
hash,
|
|
35
|
+
values,
|
|
36
|
+
}, 'dev_setStorage');
|
|
37
|
+
return hash;
|
|
38
|
+
};
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ResponseError, timeTravel } from '@acala-network/chopsticks-core';
|
|
2
|
+
/**
|
|
3
|
+
* Travel to a specific time.
|
|
4
|
+
*
|
|
5
|
+
* This function is a dev rpc handler. Use `dev_timeTravel` as the method name when calling it.
|
|
6
|
+
*
|
|
7
|
+
* @param context - The context object of the rpc handler
|
|
8
|
+
* @param date - Timestamp or date string to set
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { WsProvider } from '@polkadot/rpc-provider'
|
|
13
|
+
* const ws = new WsProvider(`ws://localhost:8000`)
|
|
14
|
+
* await ws.send('dev_timeTravel', ['Jan 1, 2023'])
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export const rpc = async (context, [date]) => {
|
|
18
|
+
const timestamp = typeof date === 'string' ? Date.parse(date) : date;
|
|
19
|
+
if (Number.isNaN(timestamp))
|
|
20
|
+
throw new ResponseError(1, 'Invalid date');
|
|
21
|
+
await timeTravel(context.chain, timestamp);
|
|
22
|
+
return timestamp;
|
|
23
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { writeFileSync } from 'node:fs';
|
|
2
|
+
import { defaultOptions } from '../../cli-options';
|
|
3
|
+
import { generateHtmlDiffPreviewFile } from '../../utils/generate-html-diff';
|
|
4
|
+
import { openHtml } from '../../utils/open-html';
|
|
5
|
+
import { setupContext } from '../../context';
|
|
6
|
+
export const cli = (y) => {
|
|
7
|
+
y.command('try-runtime', 'Runs runtime upgrade', (yargs) => yargs.options({
|
|
8
|
+
...defaultOptions,
|
|
9
|
+
'wasm-override': {
|
|
10
|
+
desc: 'Path to WASM built with feature `try-runtime` enabled',
|
|
11
|
+
string: true,
|
|
12
|
+
required: true,
|
|
13
|
+
},
|
|
14
|
+
'output-path': {
|
|
15
|
+
desc: 'File path to print output',
|
|
16
|
+
string: true,
|
|
17
|
+
},
|
|
18
|
+
html: {
|
|
19
|
+
desc: 'Generate html with storage diff',
|
|
20
|
+
boolean: true,
|
|
21
|
+
},
|
|
22
|
+
open: {
|
|
23
|
+
desc: 'Open generated html',
|
|
24
|
+
boolean: true,
|
|
25
|
+
},
|
|
26
|
+
}), async (argv) => {
|
|
27
|
+
const context = await setupContext(argv);
|
|
28
|
+
const block = context.chain.head;
|
|
29
|
+
const registry = await block.registry;
|
|
30
|
+
registry.register({
|
|
31
|
+
UpgradeCheckSelect: {
|
|
32
|
+
_enum: {
|
|
33
|
+
None: null,
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
const select_none = registry.createType('UpgradeCheckSelect', 'None');
|
|
38
|
+
const result = await block.call('TryRuntime_on_runtime_upgrade', [select_none.toHex()]);
|
|
39
|
+
if (argv.html) {
|
|
40
|
+
const filePath = await generateHtmlDiffPreviewFile(block, result.storageDiff, block.hash);
|
|
41
|
+
console.log(`Generated preview ${filePath}`);
|
|
42
|
+
if (argv.open) {
|
|
43
|
+
openHtml(filePath);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else if (argv.outputPath) {
|
|
47
|
+
writeFileSync(argv.outputPath, JSON.stringify(result, null, 2));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
console.dir(result, { depth: null, colors: false });
|
|
51
|
+
}
|
|
52
|
+
process.exit(0);
|
|
53
|
+
});
|
|
54
|
+
};
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export { rpc as newBlock
|
|
2
|
-
export { rpc as dryRun
|
|
1
|
+
export { rpc as newBlock } from './new-block';
|
|
2
|
+
export { rpc as dryRun } from './dry-run';
|
|
3
3
|
export { rpc as setBlockBuildMode } from './set-block-build-mode';
|
|
4
4
|
export { rpc as setHead } from './set-head';
|
|
5
5
|
export { rpc as setRuntimeLogLevel } from './set-runtime-log-level';
|
|
6
6
|
export { rpc as setStorage } from './set-storage';
|
|
7
7
|
export { rpc as timeTravel } from './time-travel';
|
|
8
|
-
export { rpc as runBlock
|
|
8
|
+
export { rpc as runBlock } from './run-block';
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { ResponseError, logger, substrate, } from '@acala-network/chopsticks-core';
|
|
2
|
+
import { pluginHandlers } from '../plugins';
|
|
3
|
+
const allHandlers = {
|
|
4
|
+
...substrate,
|
|
5
|
+
rpc_methods: async () => Promise.resolve({
|
|
6
|
+
version: 1,
|
|
7
|
+
methods: [...Object.keys(allHandlers), ...Object.keys(pluginHandlers)],
|
|
8
|
+
}),
|
|
9
|
+
};
|
|
10
|
+
const getHandler = (method) => {
|
|
11
|
+
const handler = allHandlers[method];
|
|
12
|
+
if (!handler) {
|
|
13
|
+
// no handler for this method, check if it's a plugin
|
|
14
|
+
return pluginHandlers[method];
|
|
15
|
+
}
|
|
16
|
+
return handler;
|
|
17
|
+
};
|
|
18
|
+
export const handler = (context) => ({ method, params }, subscriptionManager) => {
|
|
19
|
+
logger.trace('Handling %s', method);
|
|
20
|
+
const handler = getHandler(method);
|
|
21
|
+
if (!handler) {
|
|
22
|
+
logger.warn('Method not found %s', method);
|
|
23
|
+
throw new ResponseError(-32601, `Method not found: ${method}`);
|
|
24
|
+
}
|
|
25
|
+
return handler(context, params, subscriptionManager);
|
|
26
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { BuildBlockMode, defaultLogger, genesisSchema, isUrl } from '@acala-network/chopsticks-core';
|
|
2
|
+
import { basename, extname } from 'node:path';
|
|
3
|
+
import { readFileSync } from 'node:fs';
|
|
4
|
+
import { z } from 'zod';
|
|
5
|
+
import _ from 'lodash';
|
|
6
|
+
import axios from 'axios';
|
|
7
|
+
import yaml from 'js-yaml';
|
|
8
|
+
export const configSchema = z
|
|
9
|
+
.object({
|
|
10
|
+
port: z.number().optional(),
|
|
11
|
+
endpoint: z.string().optional(),
|
|
12
|
+
block: z.union([z.string().length(66).startsWith('0x'), z.number(), z.null()]).optional(),
|
|
13
|
+
'build-block-mode': z.nativeEnum(BuildBlockMode).optional(),
|
|
14
|
+
'import-storage': z.any().optional(),
|
|
15
|
+
'mock-signature-host': z.boolean().optional(),
|
|
16
|
+
'max-memory-block-count': z.number().optional(),
|
|
17
|
+
db: z.string().optional(),
|
|
18
|
+
'wasm-override': z.string().optional(),
|
|
19
|
+
genesis: z.union([z.string(), genesisSchema]).optional(),
|
|
20
|
+
timestamp: z.number().optional(),
|
|
21
|
+
'registered-types': z.any().optional(),
|
|
22
|
+
'runtime-log-level': z.number().min(0).max(5).optional(),
|
|
23
|
+
'offchain-worker': z.boolean().optional(),
|
|
24
|
+
resume: z.union([z.string().length(66).startsWith('0x'), z.number(), z.boolean()]).optional(),
|
|
25
|
+
})
|
|
26
|
+
.strict();
|
|
27
|
+
const CONFIGS_BASE_URL = 'https://raw.githubusercontent.com/AcalaNetwork/chopsticks/master/configs/';
|
|
28
|
+
export const fetchConfig = async (path) => {
|
|
29
|
+
let file;
|
|
30
|
+
if (isUrl(path)) {
|
|
31
|
+
file = await axios.get(path).then((x) => x.data);
|
|
32
|
+
}
|
|
33
|
+
else {
|
|
34
|
+
try {
|
|
35
|
+
file = readFileSync(path, 'utf8');
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
if (basename(path) === path && ['', '.yml', '.yaml', '.json'].includes(extname(path))) {
|
|
39
|
+
if (extname(path) === '') {
|
|
40
|
+
path += '.yml';
|
|
41
|
+
}
|
|
42
|
+
const url = CONFIGS_BASE_URL + path;
|
|
43
|
+
defaultLogger.info(`Loading config file ${url}`);
|
|
44
|
+
file = await axios.get(url).then((x) => x.data);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
throw err;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const config = yaml.load(_.template(file, { variable: 'env' })(process.env));
|
|
52
|
+
return configSchema.parse(config);
|
|
53
|
+
};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { WebSocket, WebSocketServer } from 'ws';
|
|
2
|
+
import { ResponseError } from '@acala-network/chopsticks-core';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { defaultLogger, truncate } from './logger';
|
|
5
|
+
const logger = defaultLogger.child({ name: 'ws' });
|
|
6
|
+
const singleRequest = z.object({
|
|
7
|
+
id: z.number(),
|
|
8
|
+
jsonrpc: z.literal('2.0'),
|
|
9
|
+
method: z.string(),
|
|
10
|
+
params: z.array(z.any()).default([]),
|
|
11
|
+
});
|
|
12
|
+
const batchRequest = z.array(singleRequest);
|
|
13
|
+
const requestSchema = z.union([singleRequest, batchRequest]);
|
|
14
|
+
const parseRequest = (request) => {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(request);
|
|
17
|
+
}
|
|
18
|
+
catch (e) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const createWS = async (port) => {
|
|
23
|
+
const wss = new WebSocketServer({ port, maxPayload: 1024 * 1024 * 100 });
|
|
24
|
+
const promise = new Promise((resolve) => {
|
|
25
|
+
wss.on('listening', () => {
|
|
26
|
+
resolve([wss, wss.address().port]);
|
|
27
|
+
});
|
|
28
|
+
wss.on('error', (_) => {
|
|
29
|
+
resolve([]);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
return promise;
|
|
33
|
+
};
|
|
34
|
+
export const createServer = async (handler, port) => {
|
|
35
|
+
let wss;
|
|
36
|
+
let listenPort;
|
|
37
|
+
for (let i = 0; i < 10; i++) {
|
|
38
|
+
const preferPort = (port ?? 0) > 0 ? (port ?? 0) + i : 0;
|
|
39
|
+
logger.debug('Try starting on port %d', preferPort);
|
|
40
|
+
const [maybeWss, maybeListenPort] = await createWS(preferPort);
|
|
41
|
+
if (maybeWss && maybeListenPort) {
|
|
42
|
+
wss = maybeWss;
|
|
43
|
+
listenPort = maybeListenPort;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (!wss || !listenPort) {
|
|
48
|
+
throw new Error(`Failed to create WebsocketServer at port ${port}`);
|
|
49
|
+
}
|
|
50
|
+
wss.on('connection', (ws) => {
|
|
51
|
+
logger.debug('New connection');
|
|
52
|
+
const send = (data) => {
|
|
53
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
54
|
+
ws.send(JSON.stringify(data));
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
const subscriptions = {};
|
|
58
|
+
const subscriptionManager = {
|
|
59
|
+
subscribe: (method, subid, onCancel = () => { }) => {
|
|
60
|
+
subscriptions[subid] = onCancel;
|
|
61
|
+
return (data) => {
|
|
62
|
+
if (subscriptions[subid]) {
|
|
63
|
+
logger.trace({ method, subid, data: truncate(data) }, 'Subscription notification');
|
|
64
|
+
send({
|
|
65
|
+
jsonrpc: '2.0',
|
|
66
|
+
method,
|
|
67
|
+
params: {
|
|
68
|
+
result: data,
|
|
69
|
+
subscription: subid,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
unsubscribe: (subid) => {
|
|
76
|
+
if (subscriptions[subid]) {
|
|
77
|
+
subscriptions[subid](subid);
|
|
78
|
+
delete subscriptions[subid];
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const processRequest = async (req) => {
|
|
83
|
+
logger.trace({
|
|
84
|
+
id: req.id,
|
|
85
|
+
method: req.method,
|
|
86
|
+
}, 'Received message');
|
|
87
|
+
try {
|
|
88
|
+
const resp = await handler(req, subscriptionManager);
|
|
89
|
+
logger.trace({
|
|
90
|
+
id: req.id,
|
|
91
|
+
method: req.method,
|
|
92
|
+
result: truncate(resp),
|
|
93
|
+
}, 'Response for request');
|
|
94
|
+
return {
|
|
95
|
+
id: req.id,
|
|
96
|
+
jsonrpc: '2.0',
|
|
97
|
+
result: resp ?? null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
catch (e) {
|
|
101
|
+
logger.info('Error handling request: %s %o', e, e.stack);
|
|
102
|
+
return {
|
|
103
|
+
id: req.id,
|
|
104
|
+
jsonrpc: '2.0',
|
|
105
|
+
error: e instanceof ResponseError ? e : { code: -32603, message: `Internal ${e}` },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
ws.on('close', () => {
|
|
110
|
+
logger.debug('Connection closed');
|
|
111
|
+
for (const [subid, onCancel] of Object.entries(subscriptions)) {
|
|
112
|
+
onCancel(subid);
|
|
113
|
+
}
|
|
114
|
+
ws.removeAllListeners();
|
|
115
|
+
});
|
|
116
|
+
ws.on('error', () => {
|
|
117
|
+
logger.debug('Connection error');
|
|
118
|
+
for (const [subid, onCancel] of Object.entries(subscriptions)) {
|
|
119
|
+
onCancel(subid);
|
|
120
|
+
}
|
|
121
|
+
ws.removeAllListeners();
|
|
122
|
+
});
|
|
123
|
+
ws.on('message', async (message) => {
|
|
124
|
+
const parsed = await requestSchema.safeParseAsync(parseRequest(message.toString()));
|
|
125
|
+
if (!parsed.success) {
|
|
126
|
+
logger.info('Invalid request: %s', message);
|
|
127
|
+
send({
|
|
128
|
+
id: null,
|
|
129
|
+
jsonrpc: '2.0',
|
|
130
|
+
error: {
|
|
131
|
+
code: -32600,
|
|
132
|
+
message: 'Invalid JSON Request',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const { data: req } = parsed;
|
|
138
|
+
if (Array.isArray(req)) {
|
|
139
|
+
logger.trace({ req }, 'Received batch request');
|
|
140
|
+
const resp = await Promise.all(req.map(processRequest));
|
|
141
|
+
send(resp);
|
|
142
|
+
}
|
|
143
|
+
else {
|
|
144
|
+
logger.trace({ req }, 'Received single request');
|
|
145
|
+
const resp = await processRequest(req);
|
|
146
|
+
send(resp);
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
return {
|
|
151
|
+
port: listenPort,
|
|
152
|
+
close: () => new Promise((resolve, reject) => {
|
|
153
|
+
wss?.clients.forEach((socket) => socket.close());
|
|
154
|
+
wss?.close((err) => {
|
|
155
|
+
if (err) {
|
|
156
|
+
reject(err);
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
resolve();
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}),
|
|
163
|
+
};
|
|
164
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createServer } from './server';
|
|
2
|
+
import { handler } from './rpc';
|
|
3
|
+
import { logger } from '@acala-network/chopsticks-core';
|
|
4
|
+
import { setupContext } from './context';
|
|
5
|
+
export const setupWithServer = async (argv) => {
|
|
6
|
+
const context = await setupContext(argv);
|
|
7
|
+
const port = argv.port ?? 8000;
|
|
8
|
+
if (argv.genesis) {
|
|
9
|
+
// mine 1st block when starting from genesis to set some mock validation data
|
|
10
|
+
await context.chain.newBlock();
|
|
11
|
+
}
|
|
12
|
+
const { close, port: listenPort } = await createServer(handler(context), port);
|
|
13
|
+
logger.info(`${await context.chain.api.getSystemChain()} RPC listening on port ${listenPort}`);
|
|
14
|
+
return {
|
|
15
|
+
...context,
|
|
16
|
+
listenPort,
|
|
17
|
+
async close() {
|
|
18
|
+
await context.chain.close();
|
|
19
|
+
await close();
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { decodeBlockStorageDiff } from '@acala-network/chopsticks-core';
|
|
2
|
+
import { create } from 'jsondiffpatch';
|
|
3
|
+
import _ from 'lodash';
|
|
4
|
+
const diffPatcher = create({
|
|
5
|
+
array: { detectMove: false },
|
|
6
|
+
textDiff: { minLength: Number.MAX_VALUE }, // skip text diff
|
|
7
|
+
});
|
|
8
|
+
export const decodeStorageDiff = async (block, diff) => {
|
|
9
|
+
const [oldState, newState] = await decodeBlockStorageDiff(block, diff);
|
|
10
|
+
const oldStateWithoutEvents = _.cloneDeep(oldState);
|
|
11
|
+
if (oldStateWithoutEvents['system']?.['events']) {
|
|
12
|
+
oldStateWithoutEvents['system']['events'] = [];
|
|
13
|
+
}
|
|
14
|
+
return { oldState, newState, delta: diffPatcher.diff(oldStateWithoutEvents, newState) };
|
|
15
|
+
};
|