diff --git a/.gitignore b/.gitignore index 351d449b..bbce9914 100644 --- a/.gitignore +++ b/.gitignore @@ -122,6 +122,7 @@ dist *.sqlite *.sqlite-journal *.wasm +*.db .DS_store diff --git a/.prettierignore b/.prettierignore index 42db64fb..a3f3eef7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,4 +7,3 @@ vendor/ .pnp.loader.mjs lib/ preview/ -tsconfig.json diff --git a/packages/chopsticks/src/plugins/dry-run/rpc.ts b/packages/chopsticks/src/plugins/dry-run/rpc.ts index 9821a9f0..cfd6a460 100644 --- a/packages/chopsticks/src/plugins/dry-run/rpc.ts +++ b/packages/chopsticks/src/plugins/dry-run/rpc.ts @@ -1,7 +1,7 @@ import { HexString } from '@polkadot/util/types' import { z } from 'zod' -import { Context, ResponseError } from '../../rpc/shared' +import { Context, ResponseError } from '@acala-network/chopsticks-core' import { decodeStorageDiff } from '../../utils/decoder' import { generateHtmlDiff } from '../../utils/generate-html-diff' diff --git a/packages/chopsticks/src/plugins/index.ts b/packages/chopsticks/src/plugins/index.ts index bdc0f88f..19743c8f 100644 --- a/packages/chopsticks/src/plugins/index.ts +++ b/packages/chopsticks/src/plugins/index.ts @@ -1,8 +1,8 @@ +import { Handlers } from '@acala-network/chopsticks-core' import { camelCase } from 'lodash' import { lstatSync, readdirSync } from 'fs' import type yargs from 'yargs' -import { Handlers } from '../rpc/shared' import { defaultLogger } from '../logger' const logger = defaultLogger.child({ name: 'plugin' }) diff --git a/packages/chopsticks/src/plugins/new-block/index.ts b/packages/chopsticks/src/plugins/new-block/index.ts index e74eea19..673c416d 100644 --- a/packages/chopsticks/src/plugins/new-block/index.ts +++ b/packages/chopsticks/src/plugins/new-block/index.ts @@ -1,5 +1,4 @@ -import { Context, ResponseError } from '../../rpc/shared' -import { DownwardMessage, HorizontalMessage } from '@acala-network/chopsticks-core' +import { Context, DownwardMessage, HorizontalMessage, ResponseError } from '@acala-network/chopsticks-core' import { HexString } from '@polkadot/util/types' import { defaultLogger } from '../../logger' diff --git a/packages/chopsticks/src/plugins/set-block-build-mode/index.ts b/packages/chopsticks/src/plugins/set-block-build-mode/index.ts index 588920e0..7ef11c39 100644 --- a/packages/chopsticks/src/plugins/set-block-build-mode/index.ts +++ b/packages/chopsticks/src/plugins/set-block-build-mode/index.ts @@ -1,5 +1,4 @@ -import { BuildBlockMode } from '@acala-network/chopsticks-core' -import { Context, ResponseError } from '../../rpc/shared' +import { BuildBlockMode, Context, ResponseError } from '@acala-network/chopsticks-core' import { defaultLogger } from '../../logger' /** diff --git a/packages/chopsticks/src/plugins/set-head/index.ts b/packages/chopsticks/src/plugins/set-head/index.ts index 43de5ed2..ccfe5a00 100644 --- a/packages/chopsticks/src/plugins/set-head/index.ts +++ b/packages/chopsticks/src/plugins/set-head/index.ts @@ -1,5 +1,4 @@ -import { Block } from '@acala-network/chopsticks-core' -import { Context, ResponseError } from '../../rpc/shared' +import { Block, Context, ResponseError } from '@acala-network/chopsticks-core' import { HexString } from '@polkadot/util/types' /** diff --git a/packages/chopsticks/src/plugins/set-runtime-log-level/index.ts b/packages/chopsticks/src/plugins/set-runtime-log-level/index.ts index cab7bccd..f5944a62 100644 --- a/packages/chopsticks/src/plugins/set-runtime-log-level/index.ts +++ b/packages/chopsticks/src/plugins/set-runtime-log-level/index.ts @@ -1,4 +1,4 @@ -import { Context, ResponseError } from '../../rpc/shared' +import { Context, ResponseError } from '@acala-network/chopsticks-core' import { defaultLogger } from '../../logger' /** diff --git a/packages/chopsticks/src/plugins/set-storage/index.ts b/packages/chopsticks/src/plugins/set-storage/index.ts index 60d52edf..6ee9787f 100644 --- a/packages/chopsticks/src/plugins/set-storage/index.ts +++ b/packages/chopsticks/src/plugins/set-storage/index.ts @@ -1,7 +1,6 @@ +import { Context, ResponseError, StorageValues, setStorage } from '@acala-network/chopsticks-core' import { HexString } from '@polkadot/util/types' -import { Context, ResponseError } from '../../rpc/shared' -import { StorageValues, setStorage } from '@acala-network/chopsticks-core' import { defaultLogger } from '../../logger' /** diff --git a/packages/chopsticks/src/plugins/time-travel/index.ts b/packages/chopsticks/src/plugins/time-travel/index.ts index aeeb0467..81238524 100644 --- a/packages/chopsticks/src/plugins/time-travel/index.ts +++ b/packages/chopsticks/src/plugins/time-travel/index.ts @@ -1,5 +1,4 @@ -import { Context, ResponseError } from '../../rpc/shared' -import { timeTravel } from '@acala-network/chopsticks-core' +import { Context, ResponseError, timeTravel } from '@acala-network/chopsticks-core' /** * Travel to a specific time. diff --git a/packages/chopsticks/src/rpc/index.ts b/packages/chopsticks/src/rpc/index.ts index 6dabaee1..abfd4374 100644 --- a/packages/chopsticks/src/rpc/index.ts +++ b/packages/chopsticks/src/rpc/index.ts @@ -1,6 +1,13 @@ -import { Context, Handlers, ResponseError, SubscriptionManager, logger } from './shared' +import { + Context, + Handlers, + ResponseError, + SubscriptionManager, + logger, + substrate, +} from '@acala-network/chopsticks-core' + import { pluginHandlers } from '../plugins' -import substrate from './substrate' const allHandlers: Handlers = { ...substrate, diff --git a/packages/chopsticks/src/server.ts b/packages/chopsticks/src/server.ts index 7498099a..0e6864e2 100644 --- a/packages/chopsticks/src/server.ts +++ b/packages/chopsticks/src/server.ts @@ -1,7 +1,7 @@ +import { ResponseError, SubscriptionManager } from '@acala-network/chopsticks-core' import { z } from 'zod' import WebSocket, { AddressInfo, WebSocketServer } from 'ws' -import { ResponseError, SubscriptionManager } from './rpc/shared' import { defaultLogger, truncate } from './logger' const logger = defaultLogger.child({ name: 'ws' }) diff --git a/packages/chopsticks/src/setup-with-server.ts b/packages/chopsticks/src/setup-with-server.ts index 776539d2..075ee655 100644 --- a/packages/chopsticks/src/setup-with-server.ts +++ b/packages/chopsticks/src/setup-with-server.ts @@ -1,7 +1,7 @@ import { Config } from './schema' import { createServer } from './server' import { handler } from './rpc' -import { logger } from './rpc/shared' +import { logger } from '@acala-network/chopsticks-core' import { setupContext } from './context' import _ from 'lodash' diff --git a/packages/chopsticks/src/types.ts b/packages/chopsticks/src/types.ts index c6063abc..756a1844 100644 --- a/packages/chopsticks/src/types.ts +++ b/packages/chopsticks/src/types.ts @@ -10,7 +10,12 @@ * * @packageDocumentation */ -export { ChainProperties, RuntimeVersion } from '@acala-network/chopsticks-core' +export type { + ChainProperties, + RuntimeVersion, + Context, + SubscriptionManager, + Handler, +} from '@acala-network/chopsticks-core' +export * from '@acala-network/chopsticks-core/src/rpc/substrate' export * from './plugins/types' -export * from './rpc/substrate' -export { Context, SubscriptionManager, Handler } from './rpc/shared' diff --git a/packages/chopsticks/tsconfig.json b/packages/chopsticks/tsconfig.json index fcec2fa2..c3be3a2d 100644 --- a/packages/chopsticks/tsconfig.json +++ b/packages/chopsticks/tsconfig.json @@ -6,13 +6,11 @@ }, "include": ["src/**/*"], "exclude": ["src/**/*.test.ts"], - "references": [ - { "path": "../core" }, - ], - "typedocOptions": { - "entryPoints": ["src/types.ts"], - "out": "../../docs-src/chopsticks", - "plugin": "typedoc-plugin-markdown", - "readme": "none", - } + "references": [{ "path": "../core" }], + "typedocOptions": { + "entryPoints": ["src/types.ts"], + "out": "../../docs-src/chopsticks", + "plugin": "typedoc-plugin-markdown", + "readme": "none" + } } diff --git a/packages/core/src/chopsticks-provider.ts b/packages/core/src/chopsticks-provider.ts new file mode 100644 index 00000000..3a1c37fa --- /dev/null +++ b/packages/core/src/chopsticks-provider.ts @@ -0,0 +1,286 @@ +import { EventEmitter } from 'eventemitter3' +import { + ProviderInterface, + ProviderInterfaceCallback, + ProviderInterfaceEmitCb, + ProviderInterfaceEmitted, + ProviderStats, +} from '@polkadot/rpc-provider/types' + +import { StorageValues } from './utils' +import { defaultLogger } from './logger' + +const logger = defaultLogger.child({ name: '[Chopsticks provider]' }) + +interface SubscriptionHandler { + callback: ProviderInterfaceCallback + type: string +} + +interface Subscription extends SubscriptionHandler { + method: string + params: unknown[] + onCancel?: () => void + result?: unknown +} + +interface Handler { + callback: ProviderInterfaceCallback + method: string + params: unknown[] + start: number + subscription?: SubscriptionHandler | undefined +} + +export interface ChopsticksProviderProps { + /** upstream endpoint */ + endpoint: string + /** default to latest block */ + blockHash?: string + dbPath?: string + storageValues?: StorageValues +} + +/** + * A provider for ApiPromise. + * + * Currectly only support browser environment. + */ +export class ChopsticksProvider implements ProviderInterface { + #isConnected = false + #eventemitter: EventEmitter + #isReadyPromise: Promise + #endpoint: string + readonly stats?: ProviderStats + #subscriptions: Record = {} + #worker: Worker + #blockHash: string | undefined + #dbPath: string | undefined + #storageValues: StorageValues | undefined + #handlers: Record = {} + #idCounter = 0 + + constructor({ endpoint, blockHash, dbPath, storageValues }: ChopsticksProviderProps) { + if (!endpoint) { + throw new Error('ChopsticksProvider requires the upstream endpoint') + } + this.#endpoint = endpoint + this.#blockHash = blockHash + this.#dbPath = dbPath + this.#storageValues = storageValues + + this.#eventemitter = new EventEmitter() + + this.#isReadyPromise = new Promise((resolve, reject): void => { + this.#eventemitter.once('connected', (): void => { + logger.debug('isReadyPromise: connected.') + resolve() + }) + this.#eventemitter.once('error', reject) + }) + + const chopsticksWorker = new Worker(new URL('./chopsticks-worker', import.meta.url), { type: 'module' }) + this.#worker = chopsticksWorker + + this.connect() + } + + get hasSubscriptions(): boolean { + return true + } + + get isClonable(): boolean { + return true + } + + get isConnected(): boolean { + return this.#isConnected + } + + get isReady(): Promise { + return this.#isReadyPromise + } + + clone = (): ProviderInterface => { + return new ChopsticksProvider({ endpoint: this.#endpoint }) + } + + connect = async (): Promise => { + if (this.#isConnected) { + return + } + + this.#worker!.onmessage = this.#onWorkerMessage + + this.#worker?.postMessage({ + type: 'connect', + endpoint: this.#endpoint, + blockHash: this.#blockHash, + dbPath: this.#dbPath, + storageValues: this.#storageValues, + }) + } + + disconnect = async (): Promise => { + this.#worker?.postMessage({ type: 'disconnect' }) + this.#isConnected = false + this.#eventemitter.emit('disconnected') + } + + on = (type: ProviderInterfaceEmitted, sub: ProviderInterfaceEmitCb): (() => void) => { + this.#eventemitter.on(type, sub) + + return (): void => { + this.#eventemitter.removeListener(type, sub) + } + } + + send = async ( + method: string, + params: unknown[], + _isCacheable?: boolean, + subscription?: SubscriptionHandler, + ): Promise => { + return new Promise((resolve, reject): void => { + try { + if (!this.isConnected || this.#worker === undefined) { + throw new Error('Api is not connected') + } + + logger.debug('send', { method, params }) + + const id = `${method}::${this.#idCounter++}` + + const callback = (error?: Error | null, result?: T): void => { + if (subscription) { + // if it's a subscription, we usually returns the subid + const subid = result as string + if (subid) { + if (!this.#subscriptions[subid]) { + this.#subscriptions[subid] = { + callback: subscription.callback, + method, + params, + type: subscription.type, + } + } + } + } + + error ? reject(error) : resolve(result as T) + } + + this.#handlers[id] = { + callback, + method, + params, + start: Date.now(), + subscription, + } + + this.#worker?.postMessage({ + type: 'send', + id, + method, + params, + }) + } catch (error) { + reject(error) + } + }) + } + + subscribe( + type: string, + method: string, + params: unknown[], + callback: ProviderInterfaceCallback, + ): Promise { + return this.send(method, params, false, { callback, type }) + } + + async unsubscribe(_type: string, method: string, id: number | string): Promise { + if (!this.#subscriptions[id]) { + logger.error(`Unable to find active subscription=${id}`) + return false + } + + try { + return this.isConnected ? this.send(method, [id]) : true + } catch { + return false + } + } + + #onWorkerMessage = (e: any) => { + switch (e.data.type) { + case 'connection': + logger.debug('connection.', e.data) + if (e.data.connected) { + this.#isConnected = true + this.#eventemitter.emit('connected') + } else { + this.#isConnected = false + this.#eventemitter.emit('error', new Error('Unable to connect to the chain')) + logger.error(`Unable to connect to the chain: ${e.data.message}`) + } + break + + case 'subscribe-callback': + { + logger.debug('subscribe-callback', e.data) + const sub = this.#subscriptions[e.data.subid] + if (!sub) { + // record it first, sometimes callback comes first + this.#subscriptions[e.data.subid] = { + callback: () => {}, + method: e.data.method, + params: e.data.params, + type: e.data.type, + result: JSON.parse(e.data.result), + } + return + } + sub.callback(null, JSON.parse(e.data.result)) + } + break + + case 'unsubscribe-callback': + { + logger.debug('unsubscribe-callback', e.data) + const sub = this.#subscriptions[e.data.subid] + if (!sub) { + logger.error(`Unable to find active subscription=${e.data.subid}`) + return + } + sub?.onCancel?.() + delete this.#subscriptions[e.data.subid] + } + break + + case 'send-result': + { + const handler = this.#handlers[e.data.id] + if (!handler) { + logger.error(`Unable to find handler=${e.data.id}`) + return + } + logger.debug('send-result', { + method: e.data.method, + result: JSON.parse(e.data.result || '{}'), + data: e.data, + }) + try { + handler.callback(null, e.data.result ? JSON.parse(e.data.result) : undefined) + } catch (error) { + handler.callback(error as Error, undefined) + } + delete this.#handlers[e.data.id] + } + break + + default: + break + } + } +} diff --git a/packages/core/src/chopsticks-worker.ts b/packages/core/src/chopsticks-worker.ts new file mode 100644 index 00000000..8dbe2ab8 --- /dev/null +++ b/packages/core/src/chopsticks-worker.ts @@ -0,0 +1,101 @@ +import { Blockchain } from './blockchain' +import { allHandlers } from './rpc' +import { defaultLogger } from './logger' +import { setStorage } from './utils' +import { setup } from './setup' + +let chain: Blockchain | undefined + +const logger = defaultLogger.child({ name: '[Chopsticks worker]' }) + +const subscriptions = {} + +const providerHandlers = { + ...allHandlers, + new_block: async (context: any, _params: any, _subscriptionManager: any) => { + const { chain } = context + const block = await chain.newBlock() + return block + }, +} + +const subscriptionManager = { + subscribe: (method: string, subid: string, onCancel: () => void = () => {}) => { + subscriptions[subid] = onCancel + return (data: any) => { + postMessage({ + type: 'subscribe-callback', + method, + subid, + result: JSON.stringify(data), + }) + } + }, + unsubscribe: (subid: string) => { + if (subscriptions[subid]) { + subscriptions[subid](subid) // call onCancel + postMessage({ + type: 'unsubscribe-callback', + subid, + }) + } + }, +} + +onmessage = async (e) => { + switch (e.data.type) { + case 'connect': + try { + logger.debug('onMessage: connect. Initializing...') + chain = await setup({ + endpoint: e.data.endpoint, + mockSignatureHost: true, + db: e.data.dbPath, + block: e.data.blockHash, + }) + logger.debug('onMessage: connect. Chain setup done.') + await setStorage(chain, e.data.storageValues) + logger.debug('onMessage: connect. Set storage done.') + postMessage({ + type: 'connection', + connected: true, + }) + } catch (e) { + logger.error('onMessage: connect error.', e) + postMessage({ + type: 'connection', + connected: false, + message: e, + }) + } + break + + case 'disconnect': + if (chain) { + await chain?.api?.disconnect() + await chain?.close() + } + break + + case 'send': + { + const { method, params } = e.data + const handler = providerHandlers[method] + if (!handler) { + logger.error(`Unable to find rpc handler=${method}`) + return Promise.reject(new Error(`Unable to find handler=${method}`)) + } + const result = await handler({ chain: chain! }, params, subscriptionManager) + postMessage({ + type: 'send-result', + id: e.data.id, + method: method, + result: JSON.stringify(result), + }) + } + break + + default: + break + } +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index cba8bab0..27eb57d7 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -22,3 +22,5 @@ export * from './setup' export * from './blockchain/inherent' export * from './logger' export * from './offchain' +export * from './chopsticks-provider' +export * from './rpc' diff --git a/packages/core/src/rpc/index.ts b/packages/core/src/rpc/index.ts new file mode 100644 index 00000000..6735914c --- /dev/null +++ b/packages/core/src/rpc/index.ts @@ -0,0 +1,15 @@ +import { Handlers } from './shared' +import substrate from './substrate' + +export const allHandlers: Handlers = { + ...substrate, + rpc_methods: async () => + Promise.resolve({ + version: 1, + methods: [...Object.keys(allHandlers)], + }), +} + +export { default as substrate } from './substrate' +export { ResponseError } from './shared' +export type { Context, SubscriptionManager, Handler, Handlers } from './shared' diff --git a/packages/chopsticks/src/rpc/shared.ts b/packages/core/src/rpc/shared.ts similarity index 92% rename from packages/chopsticks/src/rpc/shared.ts rename to packages/core/src/rpc/shared.ts index cb9a05ee..fb037b6e 100644 --- a/packages/chopsticks/src/rpc/shared.ts +++ b/packages/core/src/rpc/shared.ts @@ -22,7 +22,7 @@ export class ResponseError extends Error { export interface Context { /** - * The blockchain instance, see `Blockchain` type in the `core` package + * The blockchain instance */ chain: Blockchain } diff --git a/packages/chopsticks/src/rpc/substrate/author.ts b/packages/core/src/rpc/substrate/author.ts similarity index 96% rename from packages/chopsticks/src/rpc/substrate/author.ts rename to packages/core/src/rpc/substrate/author.ts index a87cf219..cd3db11f 100644 --- a/packages/chopsticks/src/rpc/substrate/author.ts +++ b/packages/core/src/rpc/substrate/author.ts @@ -1,7 +1,8 @@ -import { APPLY_EXTRINSIC_ERROR, Block } from '@acala-network/chopsticks-core' import { HexString } from '@polkadot/util/types' import { TransactionValidityError } from '@polkadot/types/interfaces' +import { APPLY_EXTRINSIC_ERROR } from '../../blockchain/txpool' +import { Block } from '../../blockchain/block' import { Handler, ResponseError, SubscriptionManager } from '../shared' import { defaultLogger } from '../../logger' diff --git a/packages/chopsticks/src/rpc/substrate/chain.ts b/packages/core/src/rpc/substrate/chain.ts similarity index 100% rename from packages/chopsticks/src/rpc/substrate/chain.ts rename to packages/core/src/rpc/substrate/chain.ts diff --git a/packages/chopsticks/src/rpc/substrate/index.ts b/packages/core/src/rpc/substrate/index.ts similarity index 100% rename from packages/chopsticks/src/rpc/substrate/index.ts rename to packages/core/src/rpc/substrate/index.ts diff --git a/packages/chopsticks/src/rpc/substrate/payment.ts b/packages/core/src/rpc/substrate/payment.ts similarity index 100% rename from packages/chopsticks/src/rpc/substrate/payment.ts rename to packages/core/src/rpc/substrate/payment.ts diff --git a/packages/chopsticks/src/rpc/substrate/state.ts b/packages/core/src/rpc/substrate/state.ts similarity index 96% rename from packages/chopsticks/src/rpc/substrate/state.ts rename to packages/core/src/rpc/substrate/state.ts index 0e841f5a..074bdae8 100644 --- a/packages/chopsticks/src/rpc/substrate/state.ts +++ b/packages/core/src/rpc/substrate/state.ts @@ -1,14 +1,10 @@ -import { - Block, - RuntimeVersion, - isPrefixedChildKey, - prefixedChildKey, - stripChildPrefix, -} from '@acala-network/chopsticks-core' +import { Block } from '../../blockchain/block' import { HexString } from '@polkadot/util/types' import { Handler, ResponseError } from '../shared' +import { RuntimeVersion } from '../../executor' import { defaultLogger } from '../../logger' +import { isPrefixedChildKey, prefixedChildKey, stripChildPrefix } from '../../utils' const logger = defaultLogger.child({ name: 'rpc-state' }) diff --git a/packages/chopsticks/src/rpc/substrate/system.ts b/packages/core/src/rpc/substrate/system.ts similarity index 87% rename from packages/chopsticks/src/rpc/substrate/system.ts rename to packages/core/src/rpc/substrate/system.ts index 7972f658..40dcc65a 100644 --- a/packages/chopsticks/src/rpc/substrate/system.ts +++ b/packages/core/src/rpc/substrate/system.ts @@ -1,10 +1,8 @@ -import { ChainProperties } from '@acala-network/chopsticks-core' import { HexString } from '@polkadot/util/types' import { Index } from '@polkadot/types/interfaces' import { hexToU8a } from '@polkadot/util' -import { readFileSync } from 'node:fs' -import path from 'node:path' +import { ChainProperties } from '../../api' import { Handler } from '../shared' export const system_localPeerId = async () => '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY' @@ -20,8 +18,7 @@ export const system_name: Handler = async (context) => { return context.chain.api.getSystemName() } export const system_version: Handler = async (_context) => { - const { version } = JSON.parse(readFileSync(path.join(__dirname, '../../../package.json'), 'utf-8')) - return `chopsticks-v${version}` + return 'chopsticks-v1' } export const system_chainType: Handler = async (_context) => { return 'Development' diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 03ab9c12..8f1f3584 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -4,16 +4,17 @@ "outDir": "lib", "rootDir": "src", "target": "es2016", + "module": "esnext", "lib": ["es6", "dom", "dom.iterable"], "isolatedModules": true }, "include": ["src/**/*"], "exclude": ["src/**/*.test.ts"], - "typedocOptions": { - "entryPoints": ["src/index.ts"], - "out": "../../docs-src/core", - "plugin": "typedoc-plugin-markdown", - "readme": "none", - "excludePrivate": true, - } + "typedocOptions": { + "entryPoints": ["src/index.ts"], + "out": "../../docs-src/core", + "plugin": "typedoc-plugin-markdown", + "readme": "none", + "excludePrivate": true + } } diff --git a/packages/e2e/src/chopsticks-provider.test.ts b/packages/e2e/src/chopsticks-provider.test.ts new file mode 100644 index 00000000..8328f9b8 --- /dev/null +++ b/packages/e2e/src/chopsticks-provider.test.ts @@ -0,0 +1,99 @@ +import { ApiPromise } from '@polkadot/api' +import { ChopsticksProvider } from '@acala-network/chopsticks-core' +import { afterAll, beforeAll, describe, expect, it } from 'vitest' + +import { env, expectHex, expectJson, mockCallback, testingPairs } from './helper' + +// TODO: to be enabled after impl worker thread for nodejs compatibility +describe.skip('chopsticks provider works', () => { + let api: ApiPromise + + beforeAll(async () => { + const chopsticksProvider = new ChopsticksProvider({ endpoint: env.acala.endpoint, blockHash: env.acala.blockHash }) + api = await ApiPromise.create({ + provider: chopsticksProvider, + signedExtensions: { + SetEvmOrigin: { + extrinsic: {}, + payload: {}, + }, + }, + }) + await api.isReady + }) + + afterAll(async () => { + await api.disconnect() + }) + + it('chain rpc', async () => { + const hashHead = '0x0df086f32a9c3399f7fa158d3d77a1790830bd309134c5853718141c969299c7' + const hash0 = '0xfc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c' + const hash1000 = '0x1d2927c6b4aca4c42cb1f88ed7fa46dc53118bb00370475aaf514ac88933e3cc' + + expectHex(await api.rpc.chain.getBlockHash()).toMatch(hashHead) + expectHex(await api.rpc.chain.getBlockHash(0)).toMatch(hash0) + expectHex(await api.rpc.chain.getBlockHash(1000)).toMatch(hash1000) + + expectJson(await api.rpc.chain.getHeader()).toMatchSnapshot() + expectJson(await api.rpc.chain.getHeader(hashHead)).toMatchSnapshot() + expectJson(await api.rpc.chain.getHeader(hash0)).toMatchSnapshot() + expectJson(await api.rpc.chain.getHeader(hash1000)).toMatchSnapshot() + + expectJson(await api.rpc.chain.getBlock()).toMatchSnapshot() + expectJson(await api.rpc.chain.getBlock(hashHead)).toMatchSnapshot() + expectJson(await api.rpc.chain.getBlock(hash0)).toMatchSnapshot() + expectJson(await api.rpc.chain.getBlock(hash1000)).toMatchSnapshot() + + expectHex(await api.rpc.chain.getFinalizedHead()).toMatch(hashHead) + }) + + it('state rpc', async () => { + expectJson(await api.rpc.state.getRuntimeVersion()).toMatchSnapshot() + expectHex(await api.rpc.state.getMetadata(env.acala.blockHash)).toMatchSnapshot() + const genesisHash = await api.rpc.chain.getBlockHash(0) + expect(await api.rpc.state.getMetadata(genesisHash)).to.not.be.eq(await api.rpc.state.getMetadata()) + }) + + it('system rpc', async () => { + expect(await api.rpc.system.chain()).toMatch('Acala') + expect(await api.rpc.system.name()).toMatch('Subway') + expect(await api.rpc.system.version()).toBeInstanceOf(String) + expect(await api.rpc.system.properties()).not.toBeNull() + expectJson(await api.rpc.system.health()).toMatchObject({ + peers: 0, + isSyncing: false, + shouldHavePeers: false, + }) + }) + + it('handles tx', async () => { + const { alice, bob } = testingPairs() + + // setStorage(chain, { + // System: { + // Account: [ + // [[alice.address], { data: { free: 10 * 1e12 } }], + // [[bob.address], { data: { free: 10 * 1e12 } }], + // ], + // }, + // Sudo: { + // Key: alice.address, + // }, + // }) + + const { callback, next } = mockCallback() + + await api.tx.balances.transfer(bob.address, 100).signAndSend(alice, callback) + // await chain?.newBlock() + + await next() + + expect(callback.mock.calls).toMatchSnapshot() + callback.mockClear() + + expectJson(await api.rpc.chain.getBlock()).toMatchSnapshot() + expectJson(await api.query.system.account(alice.address)).toMatchSnapshot() + expectJson(await api.query.system.account(bob.address)).toMatchSnapshot() + }) +}) diff --git a/packages/e2e/tsconfig.json b/packages/e2e/tsconfig.json index 1deb8fcd..2e4ba37e 100644 --- a/packages/e2e/tsconfig.json +++ b/packages/e2e/tsconfig.json @@ -2,12 +2,8 @@ "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "lib", - "rootDir": "src", + "rootDir": "src" }, "include": ["src/**/*"], - "references": [ - { "path": "../core" }, - { "path": "../chopsticks" }, - { "path": "../testing" }, - ], + "references": [{ "path": "../core" }, { "path": "../chopsticks" }, { "path": "../testing" }] } diff --git a/packages/testing/tsconfig.json b/packages/testing/tsconfig.json index 95a60671..5aef92ec 100644 --- a/packages/testing/tsconfig.json +++ b/packages/testing/tsconfig.json @@ -3,13 +3,13 @@ "compilerOptions": { "outDir": "lib", "rootDir": "src", - "module": "ES2022", // this is required for vitest to work + "module": "ES2022" // this is required for vitest to work }, "include": ["src/**/*"], "exclude": ["src/**/*.test.ts"], "references": [ { - "path": "../chopsticks" + "path": "../chopsticks" } - ], + ] } diff --git a/packages/web-test/src/index.tsx b/packages/web-test/src/index.tsx index 1cbca5c4..93952bba 100644 --- a/packages/web-test/src/index.tsx +++ b/packages/web-test/src/index.tsx @@ -1,8 +1,43 @@ -import './index.css' +import { ApiPromise } from '@polkadot/api' +import { ChopsticksProvider } from '@acala-network/chopsticks-core' +import { HexString } from '@polkadot/util/types' +import { Keyring } from '@polkadot/keyring' import { createRoot } from 'react-dom/client' -import App from './App' import React from 'react' +import './index.css' +import App from './App' + +// for playing with chopsticks apiPromise in dev console +try { + const keyring = new Keyring({ type: 'ed25519' }) + const alice = keyring.addFromUri('//Alice') // 5FA9nQDVg267DEd8m1ZypXLBnvN7SFxYwV7ndqSYGiN9TTpu + const bob = keyring.addFromUri('//Bob') // 5GoNkf6WdbxCFnPdAnYYQyCjAKPJgLNxXwPjwTh6DGg6gN3E + + const api = new ApiPromise({ + provider: new ChopsticksProvider({ + endpoint: 'wss://acala-rpc.aca-api.network', + // 3,800,000 + blockHash: '0x0df086f32a9c3399f7fa158d3d77a1790830bd309134c5853718141c969299c7' as HexString, + storageValues: { + System: { + Account: [ + [[alice.address], { providers: 1, data: { free: 1 * 1e12 } }], + [[bob.address], { providers: 1, data: { free: 1 * 1e12 } }], + ], + }, + }, + }), + }) + globalThis.api = api + api.isReady.then(() => { + api.rpc('new_block') + api.tx.balances.transfer(bob.address, 1000).signAndSend(alice, () => console.log('sent')) + }) +} catch (e) { + console.log(e) +} + createRoot(document.getElementById('root')!).render( diff --git a/packages/web-test/src/vite-env.d.ts b/packages/web-test/src/vite-env.d.ts index 83dfec54..51ef182b 100644 --- a/packages/web-test/src/vite-env.d.ts +++ b/packages/web-test/src/vite-env.d.ts @@ -1,8 +1,11 @@ /// +import { ApiPromise } from '@polkadot/api' import { Blockchain } from '@acala-network/chopsticks-core' declare global { // eslint-disable-next-line no-var var chain: Blockchain + // eslint-disable-next-line no-var + var api: ApiPromise } diff --git a/packages/web-test/tests/chopsticks-provider.spec.ts b/packages/web-test/tests/chopsticks-provider.spec.ts new file mode 100644 index 00000000..c501da99 --- /dev/null +++ b/packages/web-test/tests/chopsticks-provider.spec.ts @@ -0,0 +1,73 @@ +import '@polkadot/api-augment' +import { ApiPromise } from '@polkadot/api' +import { Keyring } from '@polkadot/keyring' +import { Page, expect, test } from '@playwright/test' + +// Not working yet: +// 1. globalThis.api cannot be correctly evaluate by playwright, all api.rpc method gives undefined. +// 2. if init api promise inside this test, chopsticks worker cannot be created inside a playwright test worker +test.describe.skip('chopsticks provider', async () => { + let page: Page + let api: ApiPromise + + test.beforeAll(async ({ browser }) => { + test.setTimeout(60000) + page = await browser.newPage() + await page.goto('/') + await page.waitForLoadState() + await expect(page.getByText('Save')).toBeDisabled() + // sleep + await new Promise((resolve) => setTimeout(resolve, 10000)) + api = await page.evaluate(() => globalThis.api) + await api.isReady + }) + + test.afterAll(async () => { + await api.disconnect() + }) + + test('chain rpc', async () => { + const hashHead = '0x0df086f32a9c3399f7fa158d3d77a1790830bd309134c5853718141c969299c7' + const hash0 = '0xfc41b9bd8ef8fe53d58c7ea67c794c7ec9a73daf05e6d54b14ff6342c99ba64c' + const hash1000 = '0x1d2927c6b4aca4c42cb1f88ed7fa46dc53118bb00370475aaf514ac88933e3cc' + + expect(await api.rpc.chain.getBlockHash()).toMatch(hashHead) + expect(await api.rpc.chain.getBlockHash(0)).toMatch(hash0) + expect(await api.rpc.chain.getBlockHash(1000)).toMatch(hash1000) + + expect(await api.rpc.chain.getFinalizedHead()).toMatch(hashHead) + }) + + test('state rpc', async () => { + expect(await api.rpc.state.getRuntimeVersion()).toMatchSnapshot() + expect( + await api.rpc.state.getMetadata('0x0df086f32a9c3399f7fa158d3d77a1790830bd309134c5853718141c969299c7'), + ).toMatchSnapshot() + const genesisHash = await api.rpc.chain.getBlockHash(0) + expect(await api.rpc.state.getMetadata(genesisHash)).not.toEqual(await api.rpc.state.getMetadata()) + }) + + test('system rpc', async () => { + expect(await api.rpc.system.chain()).toMatch('Acala') + expect(await api.rpc.system.name()).toMatch('Subway') + expect(await api.rpc.system.version()).toBeInstanceOf(String) + expect(await api.rpc.system.properties()).not.toBeNull() + expect(await api.rpc.system.health()).toMatchObject({ + peers: 0, + isSyncing: false, + shouldHavePeers: false, + }) + }) + + test('handles tx', async () => { + const keyring = new Keyring({ type: 'ed25519' }) + const alice = keyring.addFromUri('//Alice') + const bob = keyring.addFromUri('//Bob') + + await api.tx.balances.transfer(bob.address, 1000).signAndSend(alice) + await api.rpc('new_block') + + const bobAccount = await api.query.system.account(bob.address) + expect(bobAccount.data.free.toHuman()).toBe(`${1 * 1e12 + 1000}`) + }) +}) diff --git a/packages/web-test/tsconfig.json b/packages/web-test/tsconfig.json index caf265af..bb3b81c1 100644 --- a/packages/web-test/tsconfig.json +++ b/packages/web-test/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "jsx": "react-jsx", + "jsx": "react-jsx", "outDir": "lib", "rootDir": "src", "target": "es2016", @@ -10,7 +10,5 @@ "sourceMap": true }, "include": ["src/**/*"], - "references": [ - { "path": "../core" } - ] + "references": [{ "path": "../core" }] } diff --git a/packages/web-test/vite.config.js b/packages/web-test/vite.config.js index 7acaa5eb..91f69007 100644 --- a/packages/web-test/vite.config.js +++ b/packages/web-test/vite.config.js @@ -8,4 +8,7 @@ export default defineConfig({ build: { outDir: '../../dist', }, + worker: { + format: 'es', + }, })