Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: load rpc additional methods by cli #756

Merged
merged 5 commits into from
May 26, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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)()
console.log(`${Object.keys(rpcScriptMethods).length} extension rpc methods loaded from ${path}`)
Wsteth marked this conversation as resolved.
Show resolved Hide resolved
} 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
71 changes: 71 additions & 0 deletions packages/e2e/src/rpc-extention-methods.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it } from 'vitest'
import { request } from 'http'

Check failure on line 2 in packages/e2e/src/rpc-extention-methods.test.ts

View workflow job for this annotation

GitHub Actions / tests

'request' is declared but its value is never read.

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,
],
},
}
`,
)
}
})
})
10 changes: 10 additions & 0 deletions packages/e2e/src/rpc-methods-test-scripts.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
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 }
},
}
Loading