-
Notifications
You must be signed in to change notification settings - Fork 284
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(json-rpc): initial package #2
Closed
Closed
Changes from 9 commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
e8a4304
feat(json-rpc): sketch of package
ludamad0 d9cbe79
feat(json-rpc): working minimal test
ludamad0 b148562
feat(json-rpc): cleaner test
ludamad0 b851fd9
feat(json-rpc): add, and appease, client tests
ludamad0 cd1ba73
feat(json-rpc): add Buffer support
ludamad0 ce711b9
chore: revert .vscode
ludamad0 69103ef
chore: update Dockerfile
ludamad0 4d63973
feat(json-rpc): test buffer support
ludamad0 4ecaa69
feat(json-rpc): move to snake-case
ludamad0 d893c72
feat(json-rpc): make client a proxy object
ludamad0 8993260
feat(json-rpc): make client a proxy object
ludamad0 7276421
Merge remote-tracking branch 'origin/adam/feat/json-rpc' into adam/fe…
ludamad0 0acff43
Revert .vscode
ludamad0 353cea6
chore: remove comment
ludamad0 03827fe
Merge branch 'master' into adam/feat/json-rpc
ludamad0 156b7f1
Merge remote-tracking branch 'origin/master' into adam/feat/json-rpc
ludamad0 6be3fef
docs: Documentation pass
ludamad0 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
require('@rushstack/eslint-patch/modern-module-resolution'); | ||
|
||
module.exports = { | ||
extends: ['@aztec/eslint-config'], | ||
parserOptions: { tsconfigRootDir: __dirname }, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
FROM 278380418400.dkr.ecr.eu-west-2.amazonaws.com/yarn-project-base AS builder | ||
COPY json-rpc json-rpc | ||
WORKDIR /usr/src/yarn-project/json-rpc | ||
RUN yarn build && yarn formatting && yarn test | ||
|
||
FROM alpine:latest | ||
COPY --from=builder /usr/src/yarn-project/json-rpc /usr/src/yarn-project/json-rpc |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
# json-rpc | ||
|
||
json-rpc | ||
|
||
``` | ||
-- src | ||
-- client | ||
Code to use by a client wishing to use a json-rpc server | ||
Includes syntax sugar for making requests with normal method syntax | ||
-- server | ||
Code for easily turning a class into an exposed RPC with one endpoint per method | ||
``` | ||
|
||
Each JsonRpcClient and JsonRpcServer class needs a map of classes that will be translated in input and output values. | ||
By default, Buffer is handled, but other usermade classes need to define toString() and static fromString() like so: | ||
|
||
``` | ||
class PublicKey { | ||
toString() { | ||
return '...'; | ||
} | ||
static fromString(str) { | ||
return new PublicKey(...); | ||
} | ||
} | ||
``` | ||
|
||
## Usage | ||
|
||
In Dapp: | ||
|
||
``` | ||
const wallet = new JsonRpcClient<WalletImplementation>('wallet-server.com', /*register classes*/ {PublicKey, TxRequest}); | ||
const response = await wallet.rpc.signTxRequest(accountPubKey, txRequest); | ||
``` | ||
|
||
The client will send `[{ name: 'PublicKey', value: accountPubKey.toString() }, { name: 'TxRequest', txRequest.toString() }]` to the server. | ||
|
||
In wallet: | ||
|
||
``` | ||
const publicClient = new JsonRpcClient<PublicClient>('public-client.com', /*register classes*/ ...); | ||
const keyStore = new JsonRpcClient<KeyStore>('key-store.com', /*register classes*/ ...); | ||
``` | ||
|
||
Different clients for different services. | ||
|
||
-- server | ||
Running a wallet server: | ||
|
||
``` | ||
const wallet = new WalletImplementation(); | ||
const server = new JsonRpcServer(wallet, /*register classes*/ ...); | ||
server.start(8080); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
{ | ||
"name": "@aztec/json-rpc", | ||
"version": "0.0.0", | ||
"type": "module", | ||
"exports": "./dest/index.js", | ||
"scripts": { | ||
"build": "yarn clean && yarn formatting && tsc -b tsconfig.dest.json", | ||
"build:dev": "tsc -b tsconfig.dest.json --watch", | ||
"clean": "rm -rf ./dest .tsbuildinfo", | ||
"formatting": "run -T prettier --check ./src && run -T eslint --max-warnings 0 ./src", | ||
"test": "NODE_NO_WARNINGS=1 node --experimental-vm-modules $(yarn bin jest) --no-cache", | ||
"test-debug": "NODE_NO_WARNINGS=1 node --inspect-brk --experimental-vm-modules $(yarn bin jest) --no-cache --runInBand --testTimeout 1000000" | ||
}, | ||
"jest": { | ||
"preset": "ts-jest/presets/default-esm", | ||
"globals": { | ||
"ts-jest": { | ||
"useESM": true | ||
} | ||
}, | ||
"moduleNameMapper": { | ||
"^(\\.{1,2}/.*)\\.js$": "$1" | ||
}, | ||
"testRegex": "./src/.*\\.test\\.ts$", | ||
"rootDir": "./src" | ||
}, | ||
"dependencies": { | ||
"@koa/cors": "^4.0.0", | ||
"koa": "^2.14.1", | ||
"koa-bodyparser": "^4.3.0", | ||
"koa-compress": "^5.1.0", | ||
"koa-router": "^12.0.0", | ||
"tslib": "^2.4.0" | ||
}, | ||
"devDependencies": { | ||
"@aztec/eslint-config": "workspace:^", | ||
"@jest/globals": "^29.4.3", | ||
"@rushstack/eslint-patch": "^1.1.4", | ||
"@types/jest": "^29.4.0", | ||
"@types/koa": "^2.13.5", | ||
"@types/koa-bodyparser": "^4.3.10", | ||
"@types/koa-compress": "^4.0.3", | ||
"@types/koa-router": "^7.0.42", | ||
"@types/koa__cors": "^3.3.0", | ||
"@types/node": "^18.7.23", | ||
"@types/supertest": "^2.0.12", | ||
"comlink": "^4.4.1", | ||
"jest": "^29.4.3", | ||
"supertest": "^6.3.3", | ||
"ts-jest": "^29.0.5", | ||
"ts-node": "^10.9.1", | ||
"typescript": "^4.9.5" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
import { assert, hasOwnProperty } from './js_utils.js'; | ||
|
||
/** | ||
* IOClass: | ||
* Represents a class compatible with our class conversion system | ||
* e.g. PublicKey here satisfies 'IOClass' | ||
* class PublicKey { | ||
* toString() { | ||
* return '...'; | ||
* } | ||
* static fromString(str) { | ||
* return new PublicKey(...); | ||
* } | ||
* } | ||
*/ | ||
interface IOClass { | ||
new (...args: any): any; | ||
fromString: (str: string) => any; | ||
} | ||
|
||
export interface ClassConverterInput { | ||
[className: string]: IOClass; | ||
} | ||
|
||
/** | ||
* Handles mapping of classes to names, and calling toString and fromString | ||
* to convert to and from JSON-friendly formats | ||
*/ | ||
export class ClassConverter { | ||
private toClass = new Map<string, IOClass>(); | ||
private toName = new Map<IOClass, string>(); | ||
|
||
constructor(input: ClassConverterInput) { | ||
for (const key of Object.keys(input)) { | ||
this.register(key, input[key]); | ||
} | ||
} | ||
register(type: string, class_: IOClass) { | ||
assert(type !== 'Buffer', "'Buffer' handling is hardcoded. Cannot use as name."); | ||
assert(hasOwnProperty(class_.prototype, 'toString'), `Class ${type} must define a toString() method.`); | ||
assert(class_['fromString'], `Class ${type} must define a fromString() static method.`); | ||
this.toName.set(class_, type); | ||
this.toClass.set(type, class_); | ||
} | ||
toClassObj(jsonObj: { type: string; data: string }) { | ||
const class_ = this.toClass.get(jsonObj.type); | ||
assert(class_, `Could not find type in lookup.`); | ||
return class_!.fromString(jsonObj.data); | ||
} | ||
toJsonObj(classObj: any) { | ||
const type = this.toName.get(classObj.constructor); | ||
assert(type, `Could not find class in lookup.`); | ||
return { type: type!, data: classObj.toString() }; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { JsonRpcClient } from './json_rpc_client.js'; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import request from 'supertest'; | ||
import { JsonRpcServer } from '../server/json_rpc_server.js'; | ||
import { TestState, TestNote } from '../test/test_state.js'; | ||
import { JsonRpcClient } from './json_rpc_client.js'; | ||
|
||
test('test an RPC function over client', async () => { | ||
const client = new JsonRpcClient<TestState>('', { TestNote }); | ||
// Mock the method | ||
(client as any).fetch = async (method: string, body: any) => { | ||
const server = new JsonRpcServer(new TestState([new TestNote('a'), new TestNote('b')]), { TestNote }); | ||
const result = await request(server.getApp().callback()).post(`/${method}`).send(body); | ||
return JSON.parse(result.text); | ||
}; | ||
const result = await client.rpc.addNotes([new TestNote('c')]); | ||
expect(result[0]).toBeInstanceOf(TestNote); | ||
expect(result[1]).toBeInstanceOf(TestNote); | ||
expect(result[2]).toBeInstanceOf(TestNote); | ||
expect(result[0].toString()).toBe('a'); | ||
expect(result[1].toString()).toBe('b'); | ||
expect(result[2].toString()).toBe('c'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import { retry } from '../js_utils.js'; | ||
import { logTrace } from '../log_utils.js'; | ||
// comlink: | ||
// Dev dependency just for the somewhat complex RemoteObject type | ||
// This takes a {foo(): T} and makes {foo(): Promise<T>} | ||
// while avoiding Promise of Promise. | ||
import { RemoteObject } from 'comlink'; | ||
import { ClassConverter, ClassConverterInput } from '../class_converter.js'; | ||
import { convertFromJsonObj, convertToJsonObj } from '../convert.js'; | ||
|
||
export class JsonRpcClient<T extends object> { | ||
private classConverter: ClassConverter; | ||
public readonly rpc: RemoteObject<T>; | ||
private id = 0; | ||
|
||
constructor(private host: string, classMap: ClassConverterInput, private netMustSucceed = false) { | ||
// the rpc object has syntax sugar over 'request' | ||
// for accessing the remote methods | ||
this.rpc = this.createRpcProxy(); | ||
this.classConverter = new ClassConverter(classMap); | ||
} | ||
/** | ||
* Creates a Proxy object that delegates over RPC | ||
* and satisfies RemoteObject<T> | ||
*/ | ||
private createRpcProxy(): RemoteObject<T> { | ||
return new Proxy( | ||
{}, | ||
{ | ||
get: | ||
(_, method: string) => | ||
(...params: any[]) => { | ||
logTrace(`JsonRpcClient.constructor`, 'proxy', method, '<-', params); | ||
return this.request(method, params); | ||
}, | ||
}, | ||
) as RemoteObject<T>; | ||
} | ||
public async request(method: string, params: any[]): Promise<any> { | ||
const body = { | ||
jsonrpc: '2.0', | ||
id: this.id++, | ||
method, | ||
params: params.map(param => convertToJsonObj(this.classConverter, param)), | ||
}; | ||
logTrace(`JsonRpcClient.request`, method, '<-', params); | ||
const res = await this.fetch(method, body); | ||
logTrace(`JsonRpcClient.request`, method, '->', res); | ||
if (res.error) { | ||
throw res.error; | ||
} | ||
return convertFromJsonObj(this.classConverter, res.result); | ||
} | ||
|
||
private async fetch(method: string, body: any) { | ||
const tryOnce = async () => { | ||
logTrace(`JsonRpcClient.fetch`, this.host, method, '<-', body); | ||
const resp = await fetch(`${this.host}/${method}`, { | ||
method: 'POST', | ||
body: JSON.stringify(body), | ||
headers: { 'content-type': 'application/json' }, | ||
}); | ||
|
||
if (!resp.ok) { | ||
throw new Error(resp.statusText); | ||
} | ||
|
||
const text = await resp.text(); | ||
try { | ||
return JSON.parse(text); | ||
} catch (err) { | ||
throw new Error(`Failed to parse body as JSON: ${text}`); | ||
} | ||
}; | ||
|
||
if (this.netMustSucceed) { | ||
return await retry(tryOnce, 'JsonRpcClient request'); | ||
} | ||
|
||
return await tryOnce(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { ClassConverter } from './class_converter.js'; | ||
import { convertFromJsonObj, convertToJsonObj } from './convert.js'; | ||
import { TestNote } from './test/test_state.js'; | ||
|
||
const TEST_BASE64 = 'YmFzZTY0IGRlY29kZXI='; | ||
test('test an RPC function over client', () => { | ||
const cc = new ClassConverter({ TestNote }); | ||
const buffer = Buffer.from(TEST_BASE64, 'base64'); | ||
expect(convertFromJsonObj(cc, convertToJsonObj(cc, buffer)).toString('base64')).toBe(TEST_BASE64); | ||
const note = new TestNote('1'); | ||
expect(convertFromJsonObj(cc, convertToJsonObj(cc, note))).toBeInstanceOf(TestNote); | ||
expect(convertFromJsonObj(cc, convertToJsonObj(cc, note)).toString()).toBe('1'); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { ClassConverter } from './class_converter.js'; | ||
|
||
export function convertFromJsonObj(cc: ClassConverter, obj: any): any { | ||
if (!obj) { | ||
return obj; // Primitive type | ||
} | ||
// Is this a serialized Node buffer? | ||
if (obj.type === 'Buffer' && typeof obj.data === 'string') { | ||
return Buffer.from(obj.data, 'base64'); | ||
} | ||
// Is this a convertible type? | ||
if (typeof obj.type === 'string' && typeof obj.data === 'string') { | ||
return cc.toClassObj(obj); | ||
} | ||
|
||
// Is this an array? | ||
if (Array.isArray(obj)) { | ||
return obj.map((x: any) => convertFromJsonObj(cc, x)); | ||
} | ||
// Is this a dictionary? | ||
if (obj.constructor === Object) { | ||
const newObj: any = {}; | ||
for (const key of Object.keys(obj)) { | ||
newObj[key] = convertFromJsonObj(cc, obj[key]); | ||
} | ||
return newObj; | ||
} | ||
|
||
// Leave alone, assume JSON primitive | ||
return obj; | ||
} | ||
|
||
export function convertToJsonObj(cc: ClassConverter, obj: any): any { | ||
if (!obj) { | ||
return obj; // Primitive type | ||
} | ||
// Is this a Node buffer? | ||
if (obj instanceof Buffer) { | ||
return { type: 'Buffer', data: obj.toString('base64') }; | ||
} | ||
// Is this a convertible type? | ||
if (obj.constructor.fromString) { | ||
return cc.toJsonObj(obj); | ||
} | ||
// Is this an array? | ||
if (Array.isArray(obj)) { | ||
return obj.map((x: any) => convertToJsonObj(cc, x)); | ||
} | ||
// Is this a dictionary? | ||
if (obj.constructor === Object) { | ||
const newObj: any = {}; | ||
for (const key of Object.keys(obj)) { | ||
newObj[key] = convertToJsonObj(cc, obj[key]); | ||
} | ||
return newObj; | ||
} | ||
|
||
// Leave alone, assume JSON primitive | ||
return obj; | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we make it call the methods directly from the client:
wallet.signTxRequest
?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorta. You need a proxy object to do this, I don't think you can do
new ...
and have something end up a proxy object. If we instead had a createRpcClient function we could do it. It could just return the .rpc object