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

add metadata fetching #700

Draft
wants to merge 4 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions packages/api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@sora-substrate/api",
"version": "1.27.2",
"version": "1.27.2-beta.1",
"license": "Apache-2.0",
"main": "./build/index.js",
"typings": "./build/index.d.ts",
Expand All @@ -10,6 +10,6 @@
"dependencies": {
"@open-web3/orml-api-derive": "1.1.4",
"@polkadot/api": "9.14.2",
"@sora-substrate/types": "1.27.2"
"@sora-substrate/types": "1.27.2-beta.1"
}
}
5 changes: 3 additions & 2 deletions packages/connection/package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
{
"name": "@sora-substrate/connection",
"version": "1.27.2",
"version": "1.27.2-beta.1",
"license": "Apache-2.0",
"main": "./build/index.js",
"typings": "./build/index.d.ts",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@sora-substrate/api": "1.27.2"
"@sora-substrate/api": "1.27.2-beta.1",
"axios": "^0.21.1"
}
}
186 changes: 186 additions & 0 deletions packages/connection/src/connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
import { ApiPromise } from '@polkadot/api';
import { WsProvider } from '@polkadot/rpc-provider';
import { options } from '@sora-substrate/api';

import { fetchRpc, getRpcEndpoint } from './http';

import type { ApiInterfaceEvents, ApiOptions } from '@polkadot/api/types';
import type { ProviderInterfaceEmitCb } from '@polkadot/rpc-provider/types';
import type { HexString } from '@polkadot/util/types';

type ConnectionEventListener = [ApiInterfaceEvents, ProviderInterfaceEmitCb];

export interface ConnectionRunOptions {
once?: boolean;
timeout?: number;
autoConnectMs?: number;
eventListeners?: ConnectionEventListener[];
getMetadata?: (genesisHash: string, specVersion: number) => Promise<string | null>;
setMetadata?: (genesisHash: string, specVersion: number, metadata: string) => Promise<void>;
}

const disconnectApi = async (api: ApiPromise, eventListeners: ConnectionEventListener[]): Promise<void> => {
if (!api) return;

eventListeners.forEach(([eventName, eventHandler]) => api.off(eventName, eventHandler));

// close the connection manually
if (api.isConnected) {
try {
await api.disconnect();
} catch (error) {
console.error(error);
}
}
};

const createConnectionTimeout = (timeout: number): Promise<void> => {
return new Promise((_resolve, reject) => {
setTimeout(() => reject(new Error('Connection Timeout')), timeout);
});
};

export class Connection {
public api: ApiPromise | null = null;
public endpoint = '';
public loading = false;

private eventListeners: Array<[ApiInterfaceEvents, ProviderInterfaceEmitCb]> = [];

constructor(private readonly apiOptions: ApiOptions) {}

private async withLoading<T>(func: () => Promise<T>): Promise<T> {
this.loading = true;
try {
return await func();
} finally {
this.loading = false;
}
}

private async run(endpoint: string, runOptions?: ConnectionRunOptions): Promise<void> {
const {
once = false,
timeout = 0,
autoConnectMs = 5000,
eventListeners = [],
getMetadata,
setMetadata,
} = runOptions || {};

const providerAutoConnectMs = once ? false : autoConnectMs;
const apiConnectionPromise = once ? 'isReadyOrError' : 'isReady';

let metadata!: Record<string, HexString>;

if (getMetadata || setMetadata) {
const metadataKeys = await this.fetchMetadataKeys(endpoint);

if (metadataKeys) {
const { genesisHash, specVersion } = metadataKeys;
const metadataHex = (await getMetadata?.(genesisHash, specVersion)) ?? (await this.fetchMetadataHex(endpoint));

if (metadataHex) {
const key = this.generateMetadataKey(genesisHash, specVersion);
metadata = { [key]: metadataHex as HexString };
setMetadata?.(genesisHash, specVersion, metadataHex);
}
}
}

const provider = new WsProvider(endpoint, providerAutoConnectMs);

this.api = new ApiPromise({ ...this.apiOptions, provider, noInitWarn: true, metadata });
this.endpoint = endpoint;

const connectionRequests: Array<Promise<any>> = [this.api[apiConnectionPromise]];

if (timeout) connectionRequests.push(createConnectionTimeout(timeout));

try {
eventListeners.forEach(([eventName, eventHandler]) => {
this.addEventListener(eventName, eventHandler);
});

// we should manually call connect fn without autoConnectMs
if (!providerAutoConnectMs) {
this.api.connect();
}

await Promise.race(connectionRequests);
} catch (error) {
this.stop();
throw error;
}
}

private async stop(): Promise<void> {
await disconnectApi(this.api!, this.eventListeners);

Check warning on line 118 in packages/connection/src/connection.ts

View check run for this annotation

Soramitsu-Sonar-PR-decoration / sora2-substrate-js-library Sonarqube Results

packages/connection/src/connection.ts#L118

This assertion is unnecessary since it does not change the type of the expression.
this.api = null;
this.endpoint = '';
this.eventListeners = [];
}

public addEventListener(eventName: ApiInterfaceEvents, eventHandler: ProviderInterfaceEmitCb) {
this.api!.on(eventName, eventHandler);

Check warning on line 125 in packages/connection/src/connection.ts

View check run for this annotation

Soramitsu-Sonar-PR-decoration / sora2-substrate-js-library Sonarqube Results

packages/connection/src/connection.ts#L125

This assertion is unnecessary since it does not change the type of the expression.
this.eventListeners.push([eventName, eventHandler]);
}

public get opened(): boolean {
return !!this.api;
}

/**
* Open connection
* @param endpoint address of node
* @param options
*/
public async open(endpoint?: string, options?: ConnectionRunOptions): Promise<void> {
if (!(endpoint || this.endpoint)) throw new Error('You should set endpoint for connection');
await this.withLoading(async () => await this.run(endpoint || this.endpoint, options));
}

/**
* Close connection
*/
public async close(): Promise<void> {
await this.withLoading(async () => await this.stop());
}

// https://github.com/polkadot-js/api/blob/master/packages/api/src/base/Init.ts#L344
public generateMetadataKey(genesisHash: string, specVersion: string | number) {
return `${genesisHash}-${String(specVersion)}`;
}

public async fetchMetadataKeys(wsEndpoint: string): Promise<{ genesisHash: string; specVersion: number } | null> {
try {
const rpcEndpoint = getRpcEndpoint(wsEndpoint);

const [genesisHash, runtimeVersion] = await Promise.all([
fetchRpc(rpcEndpoint, 'chain_getBlockHash', [0]),
fetchRpc(rpcEndpoint, 'state_getRuntimeVersion', []),
]);
const { specVersion } = runtimeVersion;

return { genesisHash, specVersion: Number(specVersion) };
} catch {
return null;
}
}

public async fetchMetadataHex(wsEndpoint: string): Promise<HexString | null> {
try {
const rpcEndpoint = getRpcEndpoint(wsEndpoint);
const metadataHex = await fetchRpc(rpcEndpoint, 'state_getMetadata', []);

return metadataHex;
} catch {
return null;
}
}
}

/**
* Base SORA connection object
*/
export const connection = new Connection(options());
29 changes: 29 additions & 0 deletions packages/connection/src/http.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import axios from 'axios';

const throwError = (fnName: string, arg: string) => {
throw new Error(`${fnName}: argument ${arg} is required`);
};

export const axiosInstance = axios.create();

export function getRpcEndpoint(wsEndpoint: string): string {
// for soramitsu nodes
if (/^wss:\/\/ws\.(?:.+\.)*sora2\.soramitsu\.co\.jp\/?$/.test(wsEndpoint)) {

Check failure on line 11 in packages/connection/src/http.ts

View check run for this annotation

Soramitsu-Sonar-PR-decoration / sora2-substrate-js-library Sonarqube Results

packages/connection/src/http.ts#L11

Make sure the regex used here, which is vulnerable to super-linear runtime due to backtracking, cannot lead to denial of service.
return wsEndpoint.replace(/^wss:\/\/ws/, 'https://rpc');
}
return wsEndpoint.replace(/^ws(s)?:/, 'http$1:');
}

export async function fetchRpc(url: string, method: string, params?: any): Promise<any> {
if (!url) return throwError(fetchRpc.name, 'url');
if (!method) return throwError(fetchRpc.name, 'method');

const { data } = await axiosInstance.post(url, {
id: 1,
jsonrpc: '2.0',
method,
params,
});

return data.result;
}
130 changes: 2 additions & 128 deletions packages/connection/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,2 @@
import { ApiPromise } from '@polkadot/api';
import { WsProvider } from '@polkadot/rpc-provider';
import { options } from '@sora-substrate/api';
import type { ApiInterfaceEvents, ApiOptions } from '@polkadot/api/types';
import type { ProviderInterfaceEmitCb } from '@polkadot/rpc-provider/types';

type ConnectionEventListener = [ApiInterfaceEvents, ProviderInterfaceEmitCb];

export interface ConnectionRunOptions {
once?: boolean;
timeout?: number;
autoConnectMs?: number;
eventListeners?: ConnectionEventListener[];
}

const disconnectApi = async (api: ApiPromise, eventListeners: ConnectionEventListener[]): Promise<void> => {
if (!api) return;

eventListeners.forEach(([eventName, eventHandler]) => api.off(eventName, eventHandler));

// close the connection manually
if (api.isConnected) {
try {
await api.disconnect();
} catch (error) {
console.error(error);
}
}
};

const createConnectionTimeout = (timeout: number): Promise<void> => {
return new Promise((_, reject) => {
setTimeout(() => reject('Connection Timeout'), timeout);
});
};

class Connection {
public api: ApiPromise | null = null;
public endpoint = '';
public loading = false;

private eventListeners: Array<[ApiInterfaceEvents, ProviderInterfaceEmitCb]> = [];

constructor(private readonly apiOptions: ApiOptions) {}

private async withLoading(func: Function): Promise<any> {
this.loading = true;
try {
return await func();
} catch (e) {
throw e;
} finally {
this.loading = false;
}
}

private async run(endpoint: string, runOptions?: ConnectionRunOptions): Promise<void> {
const { once = false, timeout = 0, autoConnectMs = 5000, eventListeners = [] } = runOptions || {};

const providerAutoConnectMs = once ? false : autoConnectMs;
const apiConnectionPromise = once ? 'isReadyOrError' : 'isReady';

const provider = new WsProvider(endpoint, providerAutoConnectMs);

this.api = new ApiPromise({ ...this.apiOptions, provider, noInitWarn: true });
this.endpoint = endpoint;

const connectionRequests: Array<Promise<any>> = [this.api[apiConnectionPromise]];

if (timeout) connectionRequests.push(createConnectionTimeout(timeout));

try {
eventListeners.forEach(([eventName, eventHandler]) => {
this.addEventListener(eventName, eventHandler);
});

// we should manually call connect fn without autoConnectMs
if (!providerAutoConnectMs) {
this.api.connect();
}

await Promise.race(connectionRequests);
} catch (error) {
this.stop();
throw error;
}
}

private async stop(): Promise<void> {
await disconnectApi(this.api, this.eventListeners);
this.api = null;
this.endpoint = '';
this.eventListeners = [];
}

public addEventListener(eventName: ApiInterfaceEvents, eventHandler: ProviderInterfaceEmitCb) {
this.api.on(eventName, eventHandler);
this.eventListeners.push([eventName, eventHandler]);
}

public get opened(): boolean {
return !!this.api;
}

/**
* Open connection
* @param endpoint address of node
* @param options
*/
public async open(endpoint?: string, options?: ConnectionRunOptions): Promise<void> {
if (!(endpoint || this.endpoint)) throw new Error('You should set endpoint for connection');
await this.withLoading(async () => await this.run(endpoint || this.endpoint, options));
}

/**
* Close connection
*/
public async close(): Promise<void> {
await this.withLoading(async () => await this.stop());
}
}

/**
* Base SORA connection object
*/
const connection = new Connection(options());

export { connection, Connection };
export { connection, Connection } from './connection';
export { axiosInstance, getRpcEndpoint, fetchRpc } from './http';
4 changes: 2 additions & 2 deletions packages/liquidity-proxy/package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "@sora-substrate/liquidity-proxy",
"version": "1.27.2",
"version": "1.27.2-beta.1",
"license": "Apache-2.0",
"main": "./build/index.js",
"typings": "./build/index.d.ts",
"publishConfig": {
"access": "public"
},
"dependencies": {
"@sora-substrate/math": "1.27.2"
"@sora-substrate/math": "1.27.2-beta.1"
}
}
Loading