Skip to content

Commit

Permalink
feat: load rpc additional methods by cli (#756)
Browse files Browse the repository at this point in the history
* feat: load rpc additional methods by cli

* test: add e2e test for rpc extension methods & add a guide to README.MD

* fix: remove unused imports & console.log -> logger.info

* fix: no need to lint the loading test scripts

* fix: do yarn fix to format
  • Loading branch information
Wsteth authored May 26, 2024
1 parent 69ae97c commit ed41f5d
Show file tree
Hide file tree
Showing 6 changed files with 146 additions and 9 deletions.
30 changes: 29 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Make sure you have setup Rust environment (>= 1.64).

## Dry-run

- Dry run hep:
- Dry run help:
```
npx @acala-network/chopsticks@latest dry-run --help
```
Expand Down Expand Up @@ -153,3 +153,31 @@ Chopsticks is designed to be extensible. You can write your own plugin to extend
There are 2 types of plugins: `cli` and `rpc`. `cli` plugins are used to extend Chopsticks' CLI, while `rpc` plugins are used to extend Chopsticks' RPC.
To create a new plugin, you could check out the [run-block plugin](packages/chopsticks/src/plugins/run-block/) as an example.
## RPC Methods
Chopsticks allows you to load your extended rpc methods by adding the cli argument `--unsafe-rpc-methods=<file path>`or `-ur=<file path>`.
### **WARNING:**
It loads an **unverified** scripts, making it **unsafe**. Ensure you load a **trusted** script.
**example**:
`npx @acala-network/chopsticks@latest --unsafe-rpc-methods=rpc-methods-scripts.js`
**scripts example of rpc-methods-scripts:**
```
return {
async testdev_testRpcMethod1(context, params) {
console.log('testdev_testRpcMethod 1', params)
return { methods: 1, params }
},
async testdev_testRpcMethod2(context, params) {
console.log('testdev_testRpcMethod 2', params)
return { methods: 2, params }
},
}
```
8 changes: 6 additions & 2 deletions packages/chopsticks/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,15 @@ import type { MiddlewareFunction } from 'yargs'

import { Blockchain, connectParachains, connectVertical, environment } from '@acala-network/chopsticks-core'
import { configSchema, fetchConfig, getYargsOptions } from './schema/index.js'
import { pluginExtendCli } from './plugins/index.js'
import { loadRpcMethodsByScripts, pluginExtendCli } from './plugins/index.js'
import { setupWithServer } from './index.js'

dotenvConfig()

const processArgv: MiddlewareFunction<{ config?: string; port?: number }> = async (argv) => {
const processArgv: MiddlewareFunction<{ config?: string; port?: number; unsafeRpcMethods?: string }> = async (argv) => {
if (argv.unsafeRpcMethods) {
await loadRpcMethodsByScripts(argv.unsafeRpcMethods)
}
if (argv.config) {
Object.assign(argv, _.defaults(argv, await fetchConfig(argv.config)))
}
Expand Down Expand Up @@ -84,6 +87,7 @@ const commands = yargs(hideBin(process.argv))
.alias('endpoint', 'e')
.alias('port', 'p')
.alias('block', 'b')
.alias('unsafe-rpc-methods', 'ur')
.alias('import-storage', 's')
.alias('wasm-override', 'w')
.usage('Usage: $0 <command> [options]')
Expand Down
28 changes: 26 additions & 2 deletions packages/chopsticks/src/plugins/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { Handlers, environment } from '@acala-network/chopsticks-core'
import { lstatSync, readdirSync } from 'fs'
import { lstatSync, readFileSync, readdirSync } from 'fs'
import _ from 'lodash'
import type { Argv } from 'yargs'

import { defaultLogger } from '../logger.js'
import { resolve } from 'path'

const logger = defaultLogger.child({ name: 'plugin' })

Expand All @@ -19,7 +20,7 @@ export const rpcPluginMethods = plugins
.filter((name) => readdirSync(new URL(name, import.meta.url)).some((file) => file.startsWith('rpc')))
.map((name) => `dev_${_.camelCase(name)}`)

export const loadRpcPlugin = async (method: string) => {
const loadRpcPlugin = async (method: string) => {
if (environment.DISABLE_PLUGINS) {
return undefined
}
Expand All @@ -39,6 +40,29 @@ export const loadRpcPlugin = async (method: string) => {
return rpc
}

// store the loaded methods by cli
let rpcScriptMethods: Handlers = {}

// use cli to load rpc methods of external scripts
export const loadRpcMethodsByScripts = async (path: string) => {
try {
const scriptContent = readFileSync(resolve(path), 'utf8')
rpcScriptMethods = new Function(scriptContent)()
logger.info(`${Object.keys(rpcScriptMethods).length} extension rpc methods loaded from ${path}`)
} catch (error) {
console.log('Failed to load rpc extension methods')
}
}

export const getRpcExtensionMethods = () => {
return [...Object.keys(rpcScriptMethods), ...rpcPluginMethods]
}

export const loadRpcExtensionMethod = async (method: string) => {
if (rpcScriptMethods[method]) return rpcScriptMethods[method]
return loadRpcPlugin(method)
}

export const pluginExtendCli = async (y: Argv) => {
for (const plugin of plugins) {
const location = new URL(`${plugin}/index.js`, import.meta.url)
Expand Down
8 changes: 4 additions & 4 deletions packages/chopsticks/src/rpc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
defaultLogger,
} from '@acala-network/chopsticks-core'

import { loadRpcPlugin, rpcPluginMethods } from '../plugins/index.js'
import { getRpcExtensionMethods, loadRpcExtensionMethod } from '../plugins/index.js'

const rpcLogger = defaultLogger.child({ name: 'rpc' })

Expand All @@ -16,15 +16,15 @@ const allHandlers: Handlers = {
rpc_methods: async () =>
Promise.resolve({
version: 1,
methods: [...Object.keys(allHandlers), ...rpcPluginMethods].sort(),
methods: [...Object.keys(allHandlers), ...getRpcExtensionMethods()].sort(),
}),
}

const getHandler = async (method: string) => {
const handler = allHandlers[method]
if (!handler) {
// no handler for this method, check if it's a plugin
return loadRpcPlugin(method)
// no handler for this method, check if it's a plugin or a script loaded
return loadRpcExtensionMethod(method)
}
return handler
}
Expand Down
69 changes: 69 additions & 0 deletions packages/e2e/src/rpc-extention-methods.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { describe, expect, it } from 'vitest'
import { env, setupApi, ws } from './helper.js'
import { getRpcExtensionMethods, loadRpcMethodsByScripts } from '@acala-network/chopsticks/plugins/index.js'
import { join, resolve } from 'path'

setupApi(env.acala)

describe('rpc methods load by scripts', () => {
it('before load', async () => {
const methods = getRpcExtensionMethods()
console.log(methods)
expect(methods.includes('dev_runBlock')).eq(true)
expect(methods.includes('testdev_testRpcMethod1')).eq(false)
expect(methods.includes('testdev_testRpcMethod2')).eq(false)
})
it('loaded', async () => {
loadRpcMethodsByScripts(resolve(join(__dirname, 'rpc-methods-test-scripts.js')))

const methods = getRpcExtensionMethods()
expect(methods.includes('dev_runBlock')).eq(true)
expect(methods.includes('testdev_testRpcMethod1')).eq(true)
expect(methods.includes('testdev_testRpcMethod2')).eq(true)
})
it('server rpc test', async () => {
const port = /:(\d+$)/.exec(ws.endpoint)?.[1]
if (!port) {
throw new Error('cannot found port')
}

{
const res = await fetch(`http://localhost:${port}`, {
method: 'POST',
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'testdev_testRpcMethod1', params: [] }),
})
expect(await res.json()).toMatchInlineSnapshot(
`
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"methods": 1,
"params": [],
},
}
`,
)
}
{
const res = await fetch(`http://localhost:${port}`, {
method: 'POST',
body: JSON.stringify({ id: 1, jsonrpc: '2.0', method: 'testdev_testRpcMethod2', params: [2] }),
})
expect(await res.json()).toMatchInlineSnapshot(
`
{
"id": 1,
"jsonrpc": "2.0",
"result": {
"methods": 2,
"params": [
2,
],
},
}
`,
)
}
})
})
12 changes: 12 additions & 0 deletions packages/e2e/src/rpc-methods-test-scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* eslint-disable */
// used to test rpc methods, no need to lint
return {
async testdev_testRpcMethod1(context, params) {
console.log('testdev_testRpcMethod 1', params)
return { methods: 1, params }
},
async testdev_testRpcMethod2(context, params) {
console.log('testdev_testRpcMethod 2', params)
return { methods: 2, params }
},
}

0 comments on commit ed41f5d

Please sign in to comment.