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: add info_get_peers rpc as poc #415

Merged
merged 21 commits into from
Jul 10, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
3 changes: 3 additions & 0 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ component_management:
target: 80%

individual_components:
- component_id: 'casper-js-api'
paths:
- /packages/api/src/
- component_id: '@casper-js-sdk/types'
paths:
- /packages/types/src/
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"devDependencies": {
"@jsdevtools/coverage-istanbul-loader": "^3.0.5",
"@rollup/plugin-buble": "^1.0.2",
"@rollup/plugin-commonjs": "^25.0.4",
"@rollup/plugin-commonjs": "^25.0.7",
"@rollup/plugin-json": "^6.0.0",
"@rollup/plugin-node-resolve": "^15.2.1",
"@rollup/plugin-typescript": "^11.1.3",
Expand Down Expand Up @@ -116,13 +116,16 @@
"@scure/bip32": "^1.1.5",
"@scure/bip39": "^1.2.0",
"@types/ws": "^8.2.2",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"eventsource": "^2.0.2",
"glob": "^7.1.6",
"humanize-duration": "^3.24.0",
"key-encoder": "^2.0.3",
"lodash": "^4.17.21",
"merge-options": "^3.0.4",
"node-fetch": "2.6.13",
"reflect-metadata": "^0.1.13",
"reflect-metadata": "^0.2.2",
"ts-results": "npm:@casperlabs/ts-results@^3.3.4",
"typedjson": "^1.6.0-rc2"
},
Expand Down
1 change: 1 addition & 0 deletions packages/api/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# casper-js-api
19 changes: 19 additions & 0 deletions packages/api/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "casper-js-api",
"version": "3.0.0-beta",
"description": "",
"main": "dist/lib.node.js",
"browser": "dist/lib.browser.js",
"types": "dist/index.d.ts",
"author": "Ryo Kanazawa <[email protected]>",
"license": "Apache-2.0",
"scripts": {
"build": "run -T rollup -c",
"test": "run -T cross-env NODE_ENV=test TS_NODE_FILES=true mocha -r ts-node/register \"src/**/*.test.ts\"",
"test:coverage": "run -T nyc --reporter=lcov yarn test",
"coverage": "run -T codecov"
},
"dependencies": {
"casper-js-types": "workspace:^"
}
}
1 change: 1 addition & 0 deletions packages/api/rollup.config.js
105 changes: 105 additions & 0 deletions packages/api/src/BaseJsonRpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import {
RequestManager,
HTTPTransport,
Client,
JSONRPCError
} from '@open-rpc/client-js';
import { ClassConstructor, Expose, plainToInstance } from 'class-transformer';
import { IsString, validateOrReject, ValidatorOptions } from 'class-validator';
import mergeOptions from 'merge-options';
import 'reflect-metadata';

import ProviderTransport, {
RequestArguments,
SafeEventEmitterProvider
} from './ProviderTransport';
import { DTO, ICamelToSnakeCase } from './utils';

export class JsonRpcError extends JSONRPCError {}

export interface JsonRpcOptions<T = ReturnType> {
returnType?: T;
timeout?: number;
validateParsedData?: boolean;
validatorOptions?: ValidatorOptions;
}

export class RpcResult {
@Expose({ name: 'api_version' })
@IsString()
apiVersion: string;
}
export type IRpcResult = ICamelToSnakeCase<DTO<RpcResult>>;

export enum ReturnType {
Raw = 'raw',
Parsed = 'parsed'
}

export class BaseJsonRpc<
T extends ReturnType = ReturnType.Parsed
> extends Client {
options: JsonRpcOptions;

constructor(
provider: string | SafeEventEmitterProvider,
options?: JsonRpcOptions<T>
) {
let transport: HTTPTransport | ProviderTransport;
if (typeof provider === 'string') {
let providerUrl = provider.endsWith('/')
? provider.substring(0, provider.length - 1)
: provider;

providerUrl = providerUrl.endsWith('/rpc')
? providerUrl
: providerUrl + '/rpc';

transport = new HTTPTransport(providerUrl);
} else {
transport = new ProviderTransport(provider);

Check warning on line 60 in packages/api/src/BaseJsonRpc.ts

View check run for this annotation

Codecov / codecov/patch

packages/api/src/BaseJsonRpc.ts#L60

Added line #L60 was not covered by tests
}
const requestManager = new RequestManager([transport]);

super(requestManager);

// TODO: Handle default option
const defaultJsonRpcOptions: JsonRpcOptions = {
returnType: ReturnType.Parsed,
validateParsedData: false,
validatorOptions: {
whitelist: true,
forbidNonWhitelisted: true
}
};

this.options = mergeOptions(defaultJsonRpcOptions, options);
}

private async _request<R extends readonly unknown[] | object, U = unknown>(
requestObject: RequestArguments<R>,
options: JsonRpcOptions
): Promise<U> {
// TODO: Handle options properly

return this.request(requestObject, options.timeout);
}

async requests<R extends readonly unknown[] | object, U = any>(
cls: undefined | ClassConstructor<U>,
requestObject: RequestArguments<R>,
options?: JsonRpcOptions
): Promise<U> {
const mergedOptions: JsonRpcOptions = mergeOptions(this.options, options);
const response = await this._request<R, U>(requestObject, mergedOptions);

if (mergedOptions.returnType === ReturnType.Raw || cls === undefined)
return response;

const parsed = plainToInstance(cls, response);
if (mergedOptions.validateParsedData) {
await validateOrReject(parsed as object, mergedOptions.validatorOptions);

Check warning on line 101 in packages/api/src/BaseJsonRpc.ts

View check run for this annotation

Codecov / codecov/patch

packages/api/src/BaseJsonRpc.ts#L101

Added line #L101 was not covered by tests
}
return parsed;
}
}
1 change: 1 addition & 0 deletions packages/api/src/CasperJsonRpc/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './informational';
1 change: 1 addition & 0 deletions packages/api/src/CasperJsonRpc/informational/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './info_get_peers';
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { HTTPTransport } from '@open-rpc/client-js';
import { expect, should } from 'chai';
import sinon from 'sinon';

should();

import { BaseJsonRpc, ReturnType } from '../../BaseJsonRpc';
import { IGetPeersResult, infoGetPeers, Peer } from './info_get_peers';

describe('info_get_peers', () => {
const baseJsonRPC = new BaseJsonRpc('http://localhost:7777/rpc');

const mockedResponse: IGetPeersResult = {
api_version: '2.0.0',
peers: [
{
node_id: 'tls:0101..0101',
address: '127.0.0.1:54321'
}
]
};

before(() => {
sinon
.stub(HTTPTransport.prototype, 'sendData')
.callsFake(async () => mockedResponse);
});

it('should return a list of peers connected to the node - raw', async () => {
// baseJsonRPC ReturnType is Parsed by default, so type case - <ReturnType.Raw> is required with modified options.
const result = await infoGetPeers<ReturnType.Raw>(baseJsonRPC, [], {
returnType: ReturnType.Raw
});

expect(result).to.deep.equal(mockedResponse);
});

it('should return a list of peers connected to the node - parsed', async () => {
const result = await infoGetPeers(baseJsonRPC, []);
expect(result.apiVersion).to.eq(mockedResponse.api_version);
expect(result.peers.length).to.eq(1);
result.peers[0].should.be.instanceof(Peer);
});
});
60 changes: 60 additions & 0 deletions packages/api/src/CasperJsonRpc/informational/info_get_peers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { Expose, Type } from 'class-transformer';
import { IsString, ValidateNested } from 'class-validator';

import {
BaseJsonRpc,
IRpcResult,
JsonRpcOptions,
RpcResult,
ReturnType
} from '../../BaseJsonRpc';
import { DTO, ICamelToSnakeCase } from '../../utils';

export class Peer {
@Expose({ name: 'node_id' })
@IsString()
nodeId: string;

@IsString()
address: string;
}

export type IPeer = ICamelToSnakeCase<DTO<Peer>>;

export class GetPeersResult extends RpcResult {
@Type(() => Peer)
@ValidateNested({ each: true })
peers: Peer[];
}

export interface IGetPeersResult extends IRpcResult {
peers: IPeer[];
}

export type GetPeersParams = string[];

export type InfoGetPeersReturnTypeMap = {
[ReturnType.Raw]: IGetPeersResult;
[ReturnType.Parsed]: GetPeersResult;
};

/**
* Returns a list of peers connected to the node
* @param baseJsonRPC
* @param options
* @returns
*/
export async function infoGetPeers<T extends keyof InfoGetPeersReturnTypeMap>(
ryo-casper marked this conversation as resolved.
Show resolved Hide resolved
baseJsonRPC: BaseJsonRpc<T>,
params: GetPeersParams = [],
options?: JsonRpcOptions<T>
): Promise<InfoGetPeersReturnTypeMap[T]> {
return baseJsonRPC.requests<GetPeersParams>(
GetPeersResult,
{
method: 'info_get_peers',
params
},
options
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,17 @@ export type JRPCVersion = '2.0';
export type JRPCId = number | string | void;

export interface JRPCBase {
jsonrpc?: JRPCVersion;
id?: JRPCId;
jsonrpc: JRPCVersion;
id: JRPCId;
}

export interface JRPCRequest<T> extends JRPCBase {
export interface JsonRpcRequest<T> extends JRPCBase {
method: string;
params?: T;
}

export interface JRPCResponse<T> extends JRPCBase {
result?: T;
export interface JsonRpcResponse<T> extends JRPCBase {
result: T;
error?: any;
}

Expand All @@ -35,10 +35,10 @@ export interface RequestArguments<T> {
}

export interface SafeEventEmitterProvider {
sendAsync: <T, U>(req: JRPCRequest<T>) => Promise<U>;
sendAsync: <T, U>(req: JsonRpcRequest<T>) => Promise<U>;
send: <T, U>(
req: JRPCRequest<T>,
callback: SendCallBack<JRPCResponse<U>>
req: JsonRpcRequest<T>,
callback: SendCallBack<JsonRpcResponse<U>>
) => void;
request: <T, U>(req: RequestArguments<T>) => Promise<Maybe<U>>;
}
Expand All @@ -61,7 +61,7 @@ class ProviderTransport extends Transport {
const batch = getBatchRequests(data);
try {
const result = await this.provider.request(
(data.request as IJSONRPCRequest) as RequestArguments<any>
data.request as IJSONRPCRequest as RequestArguments<any>
);
const jsonrpcResponse = {
id: data.request.id,
Expand Down
4 changes: 4 additions & 0 deletions packages/api/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export * from './BaseJsonRpc';
export * from './CasperJsonRpc';
export * from './ProviderTransport';
export { default as ProviderTransport } from './ProviderTransport';
45 changes: 45 additions & 0 deletions packages/api/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
? `${T}${Capitalize<SnakeToCamelCase<U>>}`
: S;

export type ISnakeToCamelCase<T> = {
[K in keyof T as SnakeToCamelCase<string & K>]: T[K];
};

type CamelToSnakeCase<S extends string> = S extends `${infer T}${infer U}`
? `${T extends Capitalize<T> ? '_' : ''}${Lowercase<T>}${CamelToSnakeCase<U>}`
: S;

export type ICamelToSnakeCase<T> = {
[K in keyof T as CamelToSnakeCase<string & K>]: T[K];
};

type DataPropertyNames<T> = {
// eslint-disable-next-line @typescript-eslint/ban-types
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];

type DataPropertiesOnly<T> = {
[P in DataPropertyNames<T>]: T[P] extends object ? DTO<T[P]> : T[P];
};

export type DTO<T> = DataPropertiesOnly<T>;

// Uage:
//
// declare class Person1 {
// firstName: string;
// myAge: number;
// location: string;
// available(now: number): boolean;
// }

// declare class Person2 {
// first_name: string;
// my_age: number;
// location: string;
// available(now: number): boolean;
// }

// type SnakePerson = ICamelToSnakeCase<DTO<Person1>>;
// type CamelPerson = ISnakeToCamelCase<DTO<Person2>>;
8 changes: 8 additions & 0 deletions packages/api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist/",
},
"include": ["src"],
"exclude": ["*/**/*.test.ts"]
}
4 changes: 4 additions & 0 deletions packages/api/typedoc.config.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('typedoc').TypeDocOptions} */
module.exports = {
entryPoints: ['./src/index.ts']
};
Loading
Loading