diff --git a/README.md b/README.md index 22dda75c..f7fbf18e 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,32 @@ Make sure you have setup Rust environment (>= 1.64). - Use option `--output-path=` to print out JSON file - Run a test node + - `yarn start dev --endpoint=wss://acala-rpc-2.aca-api.network/ws` - You have a test node running at `ws://localhost:8000` - You can use [Polkadot.js Apps](https://polkadot.js.org/apps/) to connect to this node - Submit any transaction to produce a new block in the in parallel reality + - (Optional) Pre-define/override state using option `--state-path=state.json`. See example state below. + + ```json + { + "Sudo": { + "Key": "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" + }, + "TechnicalCommittee": { + "Members": ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"] + }, + "Tokens": { + "Accounts": [ + [ + ["5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", { "token": "KAR" }], + { + "free": 1000000000000000, + "reserved": 0, + "frozen": 0 + } + ] + ] + } + } + ``` diff --git a/e2e/__snapshots__/dev.test.ts.snap b/e2e/__snapshots__/dev.test.ts.snap index dc977874..d663013f 100644 --- a/e2e/__snapshots__/dev.test.ts.snap +++ b/e2e/__snapshots__/dev.test.ts.snap @@ -1,10 +1,10 @@ // Vitest Snapshot v1 -exports[`dev rpc > setStroages 1`] = `"5F98oWfz2r5rcRVnP9VCndg33DAAsky3iuoBSpaPUbgN9AJn"`; +exports[`dev rpc > setStorages 1`] = `"5F98oWfz2r5rcRVnP9VCndg33DAAsky3iuoBSpaPUbgN9AJn"`; -exports[`dev rpc > setStroages 2`] = `"5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu"`; +exports[`dev rpc > setStorages 2`] = `"5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu"`; -exports[`dev rpc > setStroages 3`] = ` +exports[`dev rpc > setStorages 3`] = ` { "consumers": 0, "data": { @@ -19,7 +19,7 @@ exports[`dev rpc > setStroages 3`] = ` } `; -exports[`dev rpc > setStroages 4`] = ` +exports[`dev rpc > setStorages 4`] = ` { "consumers": 0, "data": { diff --git a/e2e/dev.test.ts b/e2e/dev.test.ts index 920621ac..23ae3782 100644 --- a/e2e/dev.test.ts +++ b/e2e/dev.test.ts @@ -4,7 +4,7 @@ import { u8aToHex } from '@polkadot/util' import { api, dev, expectJson, testingPairs } from './helper' describe('dev rpc', () => { - it('setStroages', async () => { + it('setStorages', async () => { const { alice, test1 } = testingPairs() await expectJson(api.query.sudo.key()).toMatchSnapshot() diff --git a/src/blockchain/index.ts b/src/blockchain/index.ts index b5b53e39..f87f358c 100644 --- a/src/blockchain/index.ts +++ b/src/blockchain/index.ts @@ -15,7 +15,7 @@ import { defaultLogger } from '../logger' const logger = defaultLogger.child({ name: 'blockchain' }) export class Blockchain { - readonly #api: ApiPromise + readonly api: ApiPromise readonly tasks: TaskManager readonly #txpool: TxPool @@ -32,7 +32,7 @@ export class Blockchain { inherentProvider: InherentProvider, header: { number: number; hash: string } ) { - this.#api = api + this.api = api this.tasks = tasks this.#head = new Block(api, this, header.number, header.hash) this.#registerBlock(this.#head) @@ -59,8 +59,8 @@ export class Blockchain { return undefined } if (!this.#blocksByNumber[number]) { - const hash = await this.#api.rpc.chain.getBlockHash(number) - const block = new Block(this.#api, this, number, hash.toHex()) + const hash = await this.api.rpc.chain.getBlockHash(number) + const block = new Block(this.api, this, number, hash.toHex()) this.#registerBlock(block) } return this.#blocksByNumber[number] @@ -72,8 +72,8 @@ export class Blockchain { } if (!this.#blocksByHash[hash]) { try { - const header = await this.#api.rpc.chain.getHeader(hash) - const block = new Block(this.#api, this, header.number.toNumber(), hash) + const header = await this.api.rpc.chain.getHeader(hash) + const block = new Block(this.api, this, header.number.toNumber(), hash) this.#registerBlock(block) } catch (e) { logger.debug(`getBlock(${hash}) failed: ${e}`) @@ -90,7 +90,7 @@ export class Blockchain { Math.round(Math.random() * 100000000) .toString(16) .padEnd(64, '0') - const block = new Block(this.#api, this, number, hash, parent, { header, extrinsics: [], storage: parent.storage }) + const block = new Block(this.api, this, number, hash, parent, { header, extrinsics: [], storage: parent.storage }) this.#blocksByHash[hash] = block return block } @@ -122,7 +122,7 @@ export class Blockchain { const source = '0x02' // External const args = u8aToHex(u8aConcat(source, extrinsic, this.head.hash)) const res = await this.head.call('TaggedTransactionQueue_validate_transaction', args) - const validity: TransactionValidity = this.#api.createType('TransactionValidity', res.result) + const validity: TransactionValidity = this.api.createType('TransactionValidity', res.result) if (validity.isOk) { this.#txpool.submitExtrinsic(extrinsic) return blake2AsHex(extrinsic, 256) diff --git a/src/index.ts b/src/index.ts index 3599ab4e..d71427cf 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,8 +8,10 @@ import { SetTimestamp } from './blockchain/inherents' import { TaskManager } from './task' import { createServer } from './server' import { defaultLogger } from './logger' +import { existsSync, readFileSync, writeFileSync } from 'fs' import { handler } from './rpc' -import { writeFileSync } from 'fs' +import { setStorage } from './utils/set-storage' +import assert from 'assert' const setup = async (argv: any) => { const port = argv.port || process.env.PORT || 8000 @@ -42,6 +44,14 @@ const setup = async (argv: any) => { tasks.updateListeningPort(listeningPort) + const statePath = argv['state-path'] + if (statePath) { + assert(existsSync(statePath), 'Invalid state path') + const state = JSON.parse(String(readFileSync(statePath))) + defaultLogger.trace({ state }, 'SetStorage') + await setStorage(chain, state) + } + return context } @@ -141,6 +151,10 @@ yargs(hideBin(process.argv)) desc: 'Build block mode. Default to Batch', enum: [BuildBlockMode.Batch, BuildBlockMode.Manual, BuildBlockMode.Instant], }, + 'state-path': { + desc: 'Pre-defined JSON state file path', + string: true, + }, }), (argv) => { setup(argv).catch((err) => { diff --git a/src/rpc/substrate/author.ts b/src/rpc/substrate/author.ts index cb6c3387..79de3478 100644 --- a/src/rpc/substrate/author.ts +++ b/src/rpc/substrate/author.ts @@ -26,11 +26,18 @@ const handlers: Handlers = { unsubscribe(id) } - context.chain.submitExtrinsic(extrinsic).then(() => { - callback({ - Ready: null, + context.chain + .submitExtrinsic(extrinsic) + .then(() => { + callback({ + Ready: null, + }) + }) + .catch((error) => { + logger.error({ error }, 'ExtrinsicFailed') + callback({ Invalid: null }) + unsubscribe(id) }) - }) return id }, author_unwatchExtrinsic: async (_context, [subid], { unsubscribe }) => { diff --git a/src/utils.ts b/src/utils/index.ts similarity index 100% rename from src/utils.ts rename to src/utils/index.ts diff --git a/src/utils/set-storage.ts b/src/utils/set-storage.ts new file mode 100644 index 00000000..e7b7b849 --- /dev/null +++ b/src/utils/set-storage.ts @@ -0,0 +1,79 @@ +import { ApiPromise } from '@polkadot/api' +import { Metadata, StorageKey } from '@polkadot/types' +import { Registry } from '@polkadot/types/types' +import { StorageEntryMetadataLatest } from '@polkadot/types/interfaces' +import { createFunction } from '@polkadot/types/metadata/decorate/storage/createFunction' +import assert from 'assert' + +import { Blockchain } from '../blockchain' + +interface StorageKeyMaker { + meta: StorageEntryMetadataLatest + makeKey: (...keys: any[]) => StorageKey +} + +const storageKeyMaker = + (registry: Registry, metadata: Metadata) => + (section: string, method: string): StorageKeyMaker => { + const pallet = metadata.asLatest.pallets.filter((x) => x.name.toString() === section)[0] + assert(pallet) + const meta = pallet.storage + .unwrap() + .items.filter((x) => x.name.toString() === method)[0] as any as StorageEntryMetadataLatest + assert(meta) + + const storageFn = createFunction( + registry, + { + meta, + prefix: section, + section, + method, + }, + {} + ) + + return { + meta, + makeKey: (...keys: any[]): StorageKey => new StorageKey(registry, [storageFn, keys]), + } + } + +function objectToStorageItems( + api: ApiPromise, + storage: Record> +): [string, string | null][] { + const storageItems: [string, string | null][] = [] + for (const sectionName in storage) { + const section = storage[sectionName] + for (const storageName in section) { + const storage = section[storageName] + const { makeKey, meta } = storageKeyMaker(api.registry, api.runtimeMetadata)(sectionName, storageName) + if (meta.type.isPlain) { + const key = makeKey() + storageItems.push([key.toHex(), storage ? api.createType(key.outputType, storage).toHex(true) : null]) + } else { + for (const [keys, value] of storage) { + const key = makeKey(...keys) + storageItems.push([key.toHex(), value ? api.createType(key.outputType, value).toHex(true) : null]) + } + } + } + } + return storageItems +} + +export const setStorage = async ( + chain: Blockchain, + storage: [string, string][] | Record>> +): Promise => { + let storageItems: [string, string | null][] + if (Array.isArray(storage)) { + storageItems = storage + } else { + storageItems = objectToStorageItems(chain.api, storage) + } + const block = await chain.getBlock() + assert(block) + block.pushStorageLayer().setAll(storageItems) +}