diff --git a/.gitignore b/.gitignore index 722642d8..5febb8a1 100644 --- a/.gitignore +++ b/.gitignore @@ -115,7 +115,7 @@ dist !.yarn/patches !.yarn/plugins !.yarn/sdks -!.yarn/versions +.yarn/versions .idea diff --git a/chopsticks.js b/chopsticks.js index 7410d7b8..38ebf037 100755 --- a/chopsticks.js +++ b/chopsticks.js @@ -1,2 +1,2 @@ #!/usr/bin/env node -require('./dist/index.js') +require('./dist/cli.js') diff --git a/e2e/__snapshots__/dev.test.ts.snap b/e2e/__snapshots__/dev.test.ts.snap index 410afb1b..2f9327fb 100644 --- a/e2e/__snapshots__/dev.test.ts.snap +++ b/e2e/__snapshots__/dev.test.ts.snap @@ -504,11 +504,11 @@ exports[`dev rpc > dryRun 1`] = ` } `; -exports[`dev rpc > setStorages 1`] = `"5F98oWfz2r5rcRVnP9VCndg33DAAsky3iuoBSpaPUbgN9AJn"`; +exports[`dev rpc > setStorage 1`] = `"5F98oWfz2r5rcRVnP9VCndg33DAAsky3iuoBSpaPUbgN9AJn"`; -exports[`dev rpc > setStorages 2`] = `"5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu"`; +exports[`dev rpc > setStorage 2`] = `"5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu"`; -exports[`dev rpc > setStorages 3`] = ` +exports[`dev rpc > setStorage 3`] = ` { "consumers": 0, "data": { @@ -523,7 +523,7 @@ exports[`dev rpc > setStorages 3`] = ` } `; -exports[`dev rpc > setStorages 4`] = ` +exports[`dev rpc > setStorage 4`] = ` { "consumers": 0, "data": { @@ -538,7 +538,7 @@ exports[`dev rpc > setStorages 4`] = ` } `; -exports[`dev rpc > setStorages 5`] = ` +exports[`dev rpc > setStorage 5`] = ` { "consumers": 0, "data": { diff --git a/e2e/dev.test.ts b/e2e/dev.test.ts index ba24ab7e..8bc3eb41 100644 --- a/e2e/dev.test.ts +++ b/e2e/dev.test.ts @@ -6,12 +6,12 @@ import { api, dev, env, expectJson, setupApi, testingPairs, ws } from './helper' setupApi(env.mandala) describe('dev rpc', () => { - it('setStorages', async () => { + it('setStorage', async () => { const { alice, test1 } = testingPairs() await expectJson(api.query.sudo.key()).toMatchSnapshot() - await dev.setStorages([[api.query.sudo.key.key(), u8aToHex(alice.addressRaw)]]) + await dev.setStorage([[api.query.sudo.key.key(), u8aToHex(alice.addressRaw)]]) await expectJson(api.query.sudo.key()).toMatchSnapshot() @@ -20,11 +20,11 @@ describe('dev rpc', () => { await expectJson(api.query.system.account(test1.address)).toMatchSnapshot() - await dev.setStorages([[api.query.system.account.key(test1.address), null]], hash) + await dev.setStorage([[api.query.system.account.key(test1.address), null]], hash) await expectJson(api.query.system.account(test1.address)).toMatchSnapshot() - await dev.setStorages({ + await dev.setStorage({ System: { Account: [[[test1.address], { data: { free: 100000 }, nonce: 1 }]], }, @@ -33,15 +33,15 @@ describe('dev rpc', () => { await expectJson(api.query.system.account(test1.address)).toMatchSnapshot() }) - it('setStorages handle errors', async () => { + it('setStorage handle errors', async () => { await expect( - dev.setStorages({ + dev.setStorage({ SSystem: { Account: [] }, }) ).rejects.toThrowError('1: Error: Cannot find pallet SSystem') await expect( - dev.setStorages({ + dev.setStorage({ System: { AAccount: [] }, }) ).rejects.toThrowError('1: Error: Cannot find storage AAccount in pallet System') diff --git a/e2e/dry-run-extrinsic.test.ts b/e2e/dry-run-extrinsic.test.ts index 73796e48..64d24410 100644 --- a/e2e/dry-run-extrinsic.test.ts +++ b/e2e/dry-run-extrinsic.test.ts @@ -12,7 +12,7 @@ describe('dry-run-extrinsic', () => { const properties = await chain.api.chainProperties const { alice, bob } = testingPairs(properties.ss58Format) - await dev.setStorages({ + await dev.setStorage({ System: { Account: [[[alice.address], { data: { free: 1000 * 1e12 } }]], }, @@ -26,7 +26,7 @@ describe('dry-run-extrinsic', () => { it('dry run extrinsic with fake signature', async () => { const ALICE = '5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu' - await dev.setStorages({ Sudo: { Key: ALICE } }) + await dev.setStorage({ Sudo: { Key: ALICE } }) // sudo.sudo(system.fillBlock(10000000)) const call = '0xff00000080969800' diff --git a/e2e/genesis-provider.test.ts b/e2e/genesis-provider.test.ts index 19576a92..2b7ae728 100644 --- a/e2e/genesis-provider.test.ts +++ b/e2e/genesis-provider.test.ts @@ -20,7 +20,7 @@ describe('genesis provider works', () => { const properties = await chain.api.chainProperties const { test1, test2 } = testingPairs(properties.ss58Format) - await dev.setStorages({ + await dev.setStorage({ System: { Account: [[[test1.address], { data: { free: 1000 * 1e12 } }]], }, diff --git a/e2e/helper.ts b/e2e/helper.ts index dae11b20..c839a0e1 100644 --- a/e2e/helper.ts +++ b/e2e/helper.ts @@ -154,8 +154,8 @@ export const dev = { newBlock: (param?: { count?: number; to?: number }): Promise => { return ws.send('dev_newBlock', [param]) }, - setStorages: (values: StorageValues, blockHash?: string) => { - return ws.send('dev_setStorages', [values, blockHash]) + setStorage: (values: StorageValues, blockHash?: string) => { + return ws.send('dev_setStorage', [values, blockHash]) }, timeTravel: (date: string | number) => { return ws.send('dev_timeTravel', [date]) diff --git a/e2e/upgrade.test.ts b/e2e/upgrade.test.ts index 4cee6b59..92117ba8 100644 --- a/e2e/upgrade.test.ts +++ b/e2e/upgrade.test.ts @@ -13,7 +13,7 @@ setupApi({ describe('upgrade', () => { const { alice, bob } = testingPairs() it('setCode works', async () => { - await dev.setStorages({ + await dev.setStorage({ Sudo: { Key: alice.address, }, diff --git a/package.json b/package.json index 7219df4f..80777eed 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@acala-network/chopsticks", - "version": "0.3.5", + "version": "0.3.6", "main": "./dist/index.js", "types": "./dist/index.d.ts", "author": "Bryan Chen ", @@ -15,18 +15,18 @@ "lint": "tsc --noEmit --project tsconfig.json && eslint . --ext .js,.ts && prettier --check .", "fix": "eslint . --ext .js,.ts --fix && prettier -w .", "prepare": "husky install", - "start": "ts-node --transpile-only src/index.ts", + "start": "ts-node --transpile-only src/cli.ts", "build": "rm -rf dist && tsc -p tsconfig.prod.json", "build-wasm": "wasm-pack build executor --target nodejs --scope acala-network", "build-wasm-logging": "yarn build-wasm --features=logging", "check": "cd executor && cargo check --locked", "test": "vitest --silent", "test:dev": "LOG_LEVEL=trace vitest --inspect", - "dev": "LOG_LEVEL=trace ts-node-dev --transpile-only --inspect --notify=false src/index.ts -- dev --config=configs/dev.yml", - "dev:karura": "ts-node-dev --transpile-only --inspect --notify=false src/index.ts -- dev --config=configs/karura.yml", - "dev:acala": "ts-node-dev --transpile-only --inspect --notify=false src/index.ts -- dev --config=configs/acala.yml", - "dev:moonriver": "ts-node-dev --transpile-only --inspect --notify=false src/index.ts -- dev --config=configs/moonriver.yml", - "dev:moonbeam": "ts-node-dev --transpile-only --inspect --notify=false src/index.ts -- dev --config=configs/moonbeam.yml" + "dev": "LOG_LEVEL=trace ts-node-dev --transpile-only --inspect --notify=false src/cli.ts -- dev --config=configs/dev.yml", + "dev:karura": "ts-node-dev --transpile-only --inspect --notify=false src/cli.ts -- dev --config=configs/karura.yml", + "dev:acala": "ts-node-dev --transpile-only --inspect --notify=false src/cli.ts -- dev --config=configs/acala.yml", + "dev:moonriver": "ts-node-dev --transpile-only --inspect --notify=false src/cli.ts -- dev --config=configs/moonriver.yml", + "dev:moonbeam": "ts-node-dev --transpile-only --inspect --notify=false src/cli.ts -- dev --config=configs/moonbeam.yml" }, "dependencies": { "@acala-network/chopsticks-executor": "workspace:*", @@ -74,8 +74,7 @@ "files": [ "dist", "bin", - "template", - "chopsticks.mjs" + "template" ], "engines": { "node": ">=v14" diff --git a/src/api.ts b/src/api.ts index 7aac1cdb..89f1eeee 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,7 +1,6 @@ import { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types' import { HexString } from '@polkadot/util/types' import { ProviderInterface } from '@polkadot/rpc-provider/types' -import { WsProvider } from '@polkadot/rpc-provider' type ChainProperties = { ss58Format?: number @@ -45,12 +44,17 @@ export class Api { } get isReady() { - if (this.#provider instanceof WsProvider) { - return this.#provider.isReady - } - if (!this.#ready) { - this.#ready = this.#provider.connect() + if (this.#provider['isReady']) { + this.#ready = this.#provider['isReady'] + } else { + this.#ready = new Promise((resolve): void => { + this.#provider.on('connected', (): void => { + resolve() + }) + this.#provider.connect() + }) + } } return this.#ready diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 00000000..c1d73341 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,203 @@ +import { HexString } from '@polkadot/util/types' +import { hideBin } from 'yargs/helpers' +import { readFileSync } from 'node:fs' +import yaml from 'js-yaml' +import yargs from 'yargs' + +import { Blockchain, BuildBlockMode, connectParachains, connectVertical, setup, setupWithServer } from '.' +import { configSchema } from './schema' +import { decodeKey } from './utils/decoder' +import { dryRun } from './dry-run' +import { runBlock } from './run-block' + +const processConfig = (path: string) => { + const configFile = readFileSync(path, 'utf8') + const config = yaml.load(configFile) as any + return configSchema.parse(config) +} + +const processArgv = (argv: any) => { + if (argv.config) { + return { ...processConfig(argv.config), ...argv } + } + return argv +} + +const defaultOptions = { + endpoint: { + desc: 'Endpoint to connect to', + string: true, + }, + block: { + desc: 'Block hash or block number. Default to latest block', + }, + 'wasm-override': { + desc: 'Path to wasm override', + string: true, + }, + db: { + desc: 'Path to database', + string: true, + }, + config: { + desc: 'Path to config file', + string: true, + }, +} + +yargs(hideBin(process.argv)) + .scriptName('chopsticks') + .command( + 'run-block', + 'Replay a block', + (yargs) => + yargs.options({ + ...defaultOptions, + port: { + desc: 'Port to listen on', + number: true, + }, + 'output-path': { + desc: 'File path to print output', + string: true, + }, + html: { + desc: 'Generate html with storage diff', + }, + open: { + desc: 'Open generated html', + }, + }), + async (argv) => { + await runBlock(processArgv(argv)) + } + ) + .command( + 'dry-run', + 'Dry run an extrinsic', + (yargs) => + yargs.options({ + ...defaultOptions, + extrinsic: { + desc: 'Extrinsic or call to dry run. If you pass call here then address is required to fake signature', + string: true, + required: true, + }, + address: { + desc: 'Address to fake sign extrinsic', + string: true, + }, + at: { + desc: 'Block hash to dry run', + string: true, + }, + 'output-path': { + desc: 'File path to print output', + string: true, + }, + html: { + desc: 'Generate html with storage diff', + }, + open: { + desc: 'Open generated html', + }, + }), + async (argv) => { + await dryRun(processArgv(argv)) + } + ) + .command( + 'dev', + 'Dev mode', + (yargs) => + yargs.options({ + ...defaultOptions, + port: { + desc: 'Port to listen on', + number: true, + }, + 'build-block-mode': { + desc: 'Build block mode. Default to Batch', + enum: [BuildBlockMode.Batch, BuildBlockMode.Manual, BuildBlockMode.Instant], + }, + 'import-storage': { + desc: 'Pre-defined JSON/YAML storage file path', + string: true, + }, + 'mock-signature-host': { + desc: 'Mock signature host so any signature starts with 0xdeadbeef and filled by 0xcd is considered valid', + boolean: true, + }, + 'allow-unresolved-imports': { + desc: 'Allow wasm unresolved imports', + boolean: true, + }, + }), + async (argv) => { + await setupWithServer(processArgv(argv)) + } + ) + .command( + 'decode-key ', + 'Deocde a key', + (yargs) => + yargs + .positional('key', { + desc: 'Key to decode', + type: 'string', + }) + .options({ + ...defaultOptions, + }), + async (argv) => { + const context = await setup(processArgv(argv)) + const { storage, decodedKey } = await decodeKey(context.chain.head, argv.key as HexString) + if (storage && decodedKey) { + console.log( + `${storage.section}.${storage.method}`, + decodedKey.args.map((x) => JSON.stringify(x.toHuman())).join(', ') + ) + } else { + console.log('Unknown') + } + process.exit(0) + } + ) + .command( + 'xcm', + 'XCM setup with relaychain and parachains', + (yargs) => + yargs.options({ + relaychain: { + desc: 'Relaychain config file path', + string: true, + }, + parachain: { + desc: 'Parachain config file path', + type: 'array', + string: true, + required: true, + }, + }), + async (argv) => { + const parachains: Blockchain[] = [] + for (const config of argv.parachain) { + const { chain } = await setupWithServer(processConfig(config)) + parachains.push(chain) + } + + if (parachains.length > 1) { + await connectParachains(parachains) + } + + if (argv.relaychain) { + const { chain: relaychain } = await setupWithServer(processConfig(argv.relaychain)) + for (const parachain of parachains) { + await connectVertical(relaychain, parachain) + } + } + } + ) + .strict() + .help() + .alias('help', 'h').argv diff --git a/src/genesis-provider.ts b/src/genesis-provider.ts index a5cdd534..132b7bc5 100644 --- a/src/genesis-provider.ts +++ b/src/genesis-provider.ts @@ -20,7 +20,7 @@ export class GenesisProvider implements ProviderInterface { readonly stats?: ProviderStats #eventemitter: EventEmitter - #isReadyPromise: Promise + #isReadyPromise: Promise #genesis: Genesis #stateRoot: Promise @@ -38,7 +38,7 @@ export class GenesisProvider implements ProviderInterface { this.#isReadyPromise = new Promise((resolve, reject): void => { this.#eventemitter.once('connected', (): void => { - resolve(this) + resolve() }) this.#eventemitter.once('error', reject) }) @@ -80,7 +80,8 @@ export class GenesisProvider implements ProviderInterface { return this.#isConnected } - get isReady(): Promise { + get isReady(): Promise { + this.connect() return this.#isReadyPromise } diff --git a/src/index.ts b/src/index.ts index 88bb3dbc..463c3b12 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,207 +1,7 @@ -import { HexString } from '@polkadot/util/types' -import { hideBin } from 'yargs/helpers' -import { readFileSync } from 'node:fs' -import yaml from 'js-yaml' -import yargs from 'yargs' - -import { Blockchain } from './blockchain' -import { BuildBlockMode } from './blockchain/txpool' -import { configSchema } from './schema' -import { connectParachains, connectVertical } from './xcm' -import { decodeKey } from './utils/decoder' -import { dryRun } from './dry-run' -import { runBlock } from './run-block' -import { setup } from './setup' -import { setupWithServer } from './setup-with-server' - -const processConfig = (path: string) => { - const configFile = readFileSync(path, 'utf8') - const config = yaml.load(configFile) as any - return configSchema.parse(config) -} - -const processArgv = (argv: any) => { - if (argv.config) { - return { ...processConfig(argv.config), ...argv } - } - return argv -} - -const defaultOptions = { - endpoint: { - desc: 'Endpoint to connect to', - string: true, - }, - block: { - desc: 'Block hash or block number. Default to latest block', - }, - 'wasm-override': { - desc: 'Path to wasm override', - string: true, - }, - db: { - desc: 'Path to database', - string: true, - }, - config: { - desc: 'Path to config file', - string: true, - }, -} - -yargs(hideBin(process.argv)) - .scriptName('chopsticks') - .command( - 'run-block', - 'Replay a block', - (yargs) => - yargs.options({ - ...defaultOptions, - port: { - desc: 'Port to listen on', - number: true, - }, - 'output-path': { - desc: 'File path to print output', - string: true, - }, - html: { - desc: 'Generate html with storage diff', - }, - open: { - desc: 'Open generated html', - }, - }), - async (argv) => { - await runBlock(processArgv(argv)) - } - ) - .command( - 'dry-run', - 'Dry run an extrinsic', - (yargs) => - yargs.options({ - ...defaultOptions, - extrinsic: { - desc: 'Extrinsic or call to dry run. If you pass call here then address is required to fake signature', - string: true, - required: true, - }, - address: { - desc: 'Address to fake sign extrinsic', - string: true, - }, - at: { - desc: 'Block hash to dry run', - string: true, - }, - 'output-path': { - desc: 'File path to print output', - string: true, - }, - html: { - desc: 'Generate html with storage diff', - }, - open: { - desc: 'Open generated html', - }, - }), - async (argv) => { - await dryRun(processArgv(argv)) - } - ) - .command( - 'dev', - 'Dev mode', - (yargs) => - yargs.options({ - ...defaultOptions, - port: { - desc: 'Port to listen on', - number: true, - }, - 'build-block-mode': { - desc: 'Build block mode. Default to Batch', - enum: [BuildBlockMode.Batch, BuildBlockMode.Manual, BuildBlockMode.Instant], - }, - 'import-storage': { - desc: 'Pre-defined JSON/YAML storage file path', - string: true, - }, - 'mock-signature-host': { - desc: 'Mock signature host so any signature starts with 0xdeadbeef and filled by 0xcd is considered valid', - boolean: true, - }, - 'allow-unresolved-imports': { - desc: 'Allow wasm unresolved imports', - boolean: true, - }, - }), - async (argv) => { - await setupWithServer(processArgv(argv)) - } - ) - .command( - 'decode-key ', - 'Deocde a key', - (yargs) => - yargs - .positional('key', { - desc: 'Key to decode', - type: 'string', - }) - .options({ - ...defaultOptions, - }), - async (argv) => { - const context = await setup(processArgv(argv)) - const { storage, decodedKey } = await decodeKey(context.chain.head, argv.key as HexString) - if (storage && decodedKey) { - console.log( - `${storage.section}.${storage.method}`, - decodedKey.args.map((x) => JSON.stringify(x.toHuman())).join(', ') - ) - } else { - console.log('Unknown') - } - process.exit(0) - } - ) - .command( - 'xcm', - 'XCM setup with relaychain and parachains', - (yargs) => - yargs.options({ - relaychain: { - desc: 'Relaychain config file path', - string: true, - }, - parachain: { - desc: 'Parachain config file path', - type: 'array', - string: true, - required: true, - }, - }), - async (argv) => { - const parachains: Blockchain[] = [] - for (const config of argv.parachain) { - const { chain } = await setupWithServer(processConfig(config)) - parachains.push(chain) - } - - if (parachains.length > 1) { - await connectParachains(parachains) - } - - if (argv.relaychain) { - const { chain: relaychain } = await setupWithServer(processConfig(argv.relaychain)) - for (const parachain of parachains) { - await connectVertical(relaychain, parachain) - } - } - } - ) - .strict() - .help() - .alias('help', 'h').argv +export { Api } from './api' +export { Blockchain } from './blockchain' +export { BuildBlockMode } from './blockchain/txpool' +export { connectParachains, connectVertical } from './xcm' +export { setup } from './setup' +export { setupWithServer } from './setup-with-server' +export * from './blockchain/inherent' diff --git a/src/rpc/dev.ts b/src/rpc/dev.ts index 992abf88..ca2cca8f 100644 --- a/src/rpc/dev.ts +++ b/src/rpc/dev.ts @@ -29,7 +29,7 @@ const handlers: Handlers = { return finalHash }, - dev_setStorages: async (context, params) => { + dev_setStorage: async (context, params) => { const [values, blockHash] = params as [StorageValues, HexString?] const hash = await setStorage(context.chain, values, blockHash).catch((error) => { throw new ResponseError(1, error.toString()) @@ -39,7 +39,7 @@ const handlers: Handlers = { hash, values, }, - 'dev_setStorages' + 'dev_setStorage' ) return hash },