Skip to content

feat: AHM support for staking routes #1636

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

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
yarn-error.log
**/*.rs.bk

.env
.env.*
!.env.local
!.env.docker
Expand Down
4 changes: 4 additions & 0 deletions src/SidecarConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,10 @@ export class SidecarConfig {
},
SUBSTRATE: {
URL: config.Get(MODULES.SUBSTRATE, CONFIG.URL) as string,
MULTI_CHAIN_URL: config.Get(MODULES.SUBSTRATE, CONFIG.MULTI_CHAIN_URL) as {
url: string;
type: 'relay' | 'assethub' | 'parachain' | undefined;
}[],
TYPES_BUNDLE: config.Get(MODULES.SUBSTRATE, CONFIG.TYPES_BUNDLE) as string,
TYPES_CHAIN: config.Get(MODULES.SUBSTRATE, CONFIG.TYPES_CHAIN) as string,
TYPES_SPEC: config.Get(MODULES.SUBSTRATE, CONFIG.TYPES_SPEC) as string,
Expand Down
18 changes: 18 additions & 0 deletions src/Specs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,14 @@ export class Specs {
}),
);

this._specs.appendSpec(
MODULES.SUBSTRATE,
this._specs.getSpec(CONFIG.MULTI_CHAIN_URL, 'Websocket or HTTP URL', {
mandatory: false,
regexp: /^(ws|wss|http|https)?:\/\/.*/,
}),
);

// TYPES_BUNDLE
this._specs.appendSpec(
MODULES.SUBSTRATE,
Expand Down Expand Up @@ -183,6 +191,16 @@ export class Specs {
mandatory: false,
}),
);

// MULTI_CHAIN_URL
this._specs.appendSpec(
MODULES.SUBSTRATE,
this._specs.getSpec(CONFIG.MULTI_CHAIN_URL, 'Multichain URL Websocket or HTTP URL', {
mandatory: false,
default: [],
type: 'array',
}),
);
}

/**
Expand Down
134 changes: 134 additions & 0 deletions src/apiRegistry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { ApiPromise } from '@polkadot/api';
import { HttpProvider, WsProvider } from '@polkadot/rpc-provider';
import { OverrideBundleType, RegistryTypes } from '@polkadot/types/types';

import { Log } from '../logging/Log';
import tempTypesBundle from '../override-types/typesBundle';
import { SidecarConfig } from '../SidecarConfig';
import { MultiKeyBiMap } from '../util/MultiKeyBiMap';

export class ApiPromiseRegistry {
// SpecName to ApiPromise instances
private static _instancesBySpecName: Map<string, Array<ApiPromise>> = new Map();
// SpecName to Type map
private static specNameToTypeMap = new MultiKeyBiMap<string, string>();
// RPC URL to ApiPromise instance
private static _instancesByUrl: Map<string, ApiPromise> = new Map();

/**
* Get the ApiPromise instance for the given spec name.
* @param specName The spec name to get the ApiPromise instance for.
* @returns The ApiPromise instance for the given spec name.
*/

public static async initApi(url: string, type?: 'relay' | 'assethub' | 'parachain'): Promise<ApiPromise> {
const { logger } = Log;
logger.info(`Initializing API for ${url}`);

// TODO: for now use instance by URL as a staging check to make sure we don't need to recreate an API instance
if (!this._instancesByUrl.has(url)) {
const { config } = SidecarConfig;

const { TYPES_BUNDLE, TYPES_SPEC, TYPES_CHAIN, TYPES, CACHE_CAPACITY } = config.SUBSTRATE;
// Instantiate new API Promise instance
const api = await ApiPromise.create({
provider: url.startsWith('http')
? new HttpProvider(url, undefined, CACHE_CAPACITY || 0)
: new WsProvider(url, undefined, undefined, undefined, CACHE_CAPACITY || 0),
// only use extra types if the url is the same as the one in the config
...(config.SUBSTRATE.URL === url
? {
typesBundle: TYPES_BUNDLE
? (require(TYPES_BUNDLE) as OverrideBundleType)
: (tempTypesBundle as OverrideBundleType),
typesChain: TYPES_CHAIN ? (require(TYPES_CHAIN) as Record<string, RegistryTypes>) : undefined,
typesSpec: TYPES_SPEC ? (require(TYPES_SPEC) as Record<string, RegistryTypes>) : undefined,
types: TYPES ? (require(TYPES) as RegistryTypes) : undefined,
}
: {}),
});

const { specName } = await api.rpc.state.getRuntimeVersion();

if (!this.specNameToTypeMap.getByKey(specName.toString())) {
if (type) {
this.specNameToTypeMap.set(specName.toString(), type);
}
}

if (this._instancesBySpecName.has(specName.toString())) {
const existingInstances = this._instancesBySpecName.get(specName.toString())!;
this._instancesBySpecName.set(specName.toString(), [...existingInstances, api]);
} else {
this._instancesBySpecName.set(specName.toString(), [api]);
}
this._instancesByUrl.set(url, api);

logger.info(`API initialized for ${url} with specName ${specName.toString()}`);
} else {
const api = this._instancesByUrl.get(url);
// make sure we have stored the type for the SUBSTRATE_URL option
if (api && type) {
const { specName } = await api.rpc.state.getRuntimeVersion();

if (!this.specNameToTypeMap.getByKey(specName.toString())) {
if (type) {
this.specNameToTypeMap.set(specName.toString(), type);
}
}
}
}

return this._instancesByUrl.get(url)!;
}

public static getApi(specName: string): ApiPromise | undefined {
const api = this._instancesBySpecName.get(specName);
if (!api) {
throw new Error(`API not found for specName: ${specName}`);
}
// TODO: create logic to return the correct API instance based on workload/necessity
return api[0];
}

public static getApiByUrl(url: string): ApiPromise | undefined {
return this._instancesByUrl.get(url);
}

public static getTypeBySpecName(specName: string): string | undefined {
return this.specNameToTypeMap.getByKey(specName);
}

public static getSpecNameByType(type: string): Set<string> | undefined {
return this.specNameToTypeMap.getByValue(type);
}

public static getApiByType(type: string): {
specName: string;
api: ApiPromise;
}[] {
const specNames = this.specNameToTypeMap.getByValue(type);
if (!specNames) {
return [];
}

const specNameApis = [];
for (const specName of specNames) {
const api = this.getApi(specName);
if (api) {
specNameApis.push({ specName, api });
}
}

return specNameApis;
}

public static clear(): void {
this._instancesBySpecName.clear();
this._instancesByUrl.clear();
this.specNameToTypeMap = new MultiKeyBiMap<string, string>();
const { logger } = Log;
logger.info('Cleared API registry');
}
}
34 changes: 17 additions & 17 deletions src/chains-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.

import { ApiPromise } from '@polkadot/api';
import { ISidecarConfig } from 'src/types/sidecar-config';

import { controllers } from '../controllers';
Expand Down Expand Up @@ -86,20 +85,23 @@ export const specToControllerMap: { [x: string]: ControllerConfig } = {
'coretime-kusama': coretimeControllers,
};

export const assetHubSpecNames = new Set(['statemine', 'statemint', 'westmint']);

/**
* Return an array of instantiated controller instances based off of a `specName`.
*
* @param api ApiPromise to inject into controllers
* @param implName
* @param specName spacName of the chain to get controllers and options for
* @param multiChainApi ApiPromise to inject into controllers that support multi-chain
*/
export function getControllersForSpec(api: ApiPromise, specName: string): AbstractController<AbstractService>[] {
export function getControllersForSpec(specName: string): AbstractController<AbstractService>[] {
if (specToControllerMap[specName]) {
return getControllersFromConfig(api, specToControllerMap[specName]);
return getControllersFromConfig(specName, specToControllerMap[specName]);
}

// If we don't have the specName in the specToControllerMap we use the default
// contoller config
return getControllersFromConfig(api, defaultControllers);
return getControllersFromConfig(specName, defaultControllers);
}

/**
Expand All @@ -109,11 +111,11 @@ export function getControllersForSpec(api: ApiPromise, specName: string): Abstra
* @param api ApiPromise to inject into controllers
* @param config controller mount configuration object
*/
function getControllersFromConfig(api: ApiPromise, config: ControllerConfig) {
function getControllersFromConfig(specName: string, config: ControllerConfig) {
const controllersToInclude = config.controllers;

return controllersToInclude.reduce((acc, controller) => {
acc.push(new controllers[controller](api, config.options));
acc.push(new controllers[controller](specName, config.options));

return acc;
}, [] as AbstractController<AbstractService>[]);
Expand All @@ -125,32 +127,30 @@ function getControllersFromConfig(api: ApiPromise, config: ControllerConfig) {
* @param api ApiPromise to inject into controllers
* @param specName specName of chain to get options
*/

export const getControllersByPallets = (pallets: string[], api: ApiPromise, specName: string) => {
export const getControllersByPallets = (specName: string, pallets: string[]) => {
const controllersSet: AbstractController<AbstractService>[] = [];
const config = specToControllerMap?.[specName]?.options || defaultControllers?.options;

Object.values(controllers).forEach((controller) => {
if (controller.canInjectByPallets(pallets)) {
controllersSet.push(new controller(api, config));
controllersSet.push(new controller(specName, config));
}
});

return controllersSet;
};

export const getControllers = (
api: ApiPromise,
config: ISidecarConfig,
specName: string,
pallets: string[],
): AbstractController<AbstractService>[] => {
if (!specName || !specName.length) {
throw new Error('specName is required');
}
if (config.EXPRESS.INJECTED_CONTROLLERS) {
return getControllersByPallets(
(api.registry.metadata.toJSON().pallets as unknown as Record<string, unknown>[]).map((p) => p.name as string),
api,
specName,
);
return getControllersByPallets(specName, pallets);
} else {
return getControllersForSpec(api, specName);
return getControllersForSpec(specName);
}
};
15 changes: 13 additions & 2 deletions src/controllers/AbstractController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {
IRangeQueryParam,
} from 'src/types/requests';

import { ApiPromiseRegistry } from '../../src/apiRegistry';
import { sanitizeNumbers } from '../sanitize';
import { isBasicLegacyError } from '../types/errors';
import { ISanitizeOptions } from '../types/sanitize';
Expand Down Expand Up @@ -62,11 +63,10 @@ export default abstract class AbstractController<T extends AbstractService> {
static requiredPallets: RequiredPallets;

constructor(
protected api: ApiPromise,
private _specName: string,
private _path: string,
protected service: T,
) {}

get path(): string {
return this._path;
}
Expand All @@ -75,6 +75,17 @@ export default abstract class AbstractController<T extends AbstractService> {
return this._router;
}

get api(): ApiPromise {
const api = ApiPromiseRegistry.getApi(this._specName);
if (!api) {
throw new InternalServerError('API not found during controller initilization');
}
return api;
}

get specName(): string {
return this._specName;
}
/**
* Mount all controller handler methods on the class's private router.
*
Expand Down
22 changes: 17 additions & 5 deletions src/controllers/AbstractControllers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { BlockHash, BlockNumber } from '@polkadot/types/interfaces';
import { Request, Response } from 'express';
import { BadRequest, InternalServerError } from 'http-errors';

import { ApiPromiseRegistry } from '../apiRegistry';
import { AbstractService } from '../services/AbstractService';
import { kusamaRegistry } from '../test-helpers/registries';
import AbstractController from './AbstractController';
Expand Down Expand Up @@ -49,6 +50,12 @@ const api = {
chain: {
getBlockHash: promiseBlockHash,
getHeader: promiseHeader,
getRuntimeVersion: () =>
Promise.resolve().then(() => {
return {
specVersion: kusamaRegistry.createType('Text', 'mock'),
};
}),
},
},
};
Expand All @@ -60,15 +67,20 @@ const MockController = class MockController extends AbstractController<AbstractS
}
};
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const MockService = new (class MockService extends AbstractService {})(api as unknown as ApiPromise);
const MockService = new (class MockService extends AbstractService {})('mock');

const controller = new MockController(api as unknown as ApiPromise, '/mock', MockService);
const controller = new MockController('mock', '/mock', MockService);

// Mock arguments for Express RequestHandler
const req = 'req' as unknown as Request;
const res = 'res' as unknown as Response;

describe('AbstractController', () => {
beforeAll(() => {
jest.spyOn(ApiPromiseRegistry, 'getApi').mockImplementation(() => {
return api as unknown as ApiPromise;
});
});
describe('catchWrap', () => {
it('catches throw from an async function and calls next with error', async () => {
const next = jest.fn();
Expand Down Expand Up @@ -253,7 +265,7 @@ describe('AbstractController', () => {
throw 'dummy getHeader error';
});

const mock = new MockController(api as ApiPromise, '/mock', MockService);
const mock = new MockController('mock', '/mock', MockService);
// We only try api.rpc.chain.getHeader when the block number is too high
await expect(mock['getHashForBlock']('101')).rejects.toEqual(
new InternalServerError('Failed while trying to get the latest header.'),
Expand All @@ -268,7 +280,7 @@ describe('AbstractController', () => {
throw 'dummy getBlockHash error';
});

const mock = new MockController(api as ApiPromise, '/mock', MockService);
const mock = new MockController('mock', '/mock', MockService);
await expect(mock['getHashForBlock']('99')).rejects.toEqual(
new InternalServerError(`Cannot get block hash for ${'99'}.`),
);
Expand All @@ -284,7 +296,7 @@ describe('AbstractController', () => {
expect(valid).toMatch(/^0x[a-fA-F0-9]+$/);
expect(valid.length).toBe(66);
expect(api.createType).toThrow('dummy createType error');
const mock = new MockController(api as ApiPromise, '/mock', MockService);
const mock = new MockController('mock', '/mock', MockService);

await expect(mock['getHashForBlock'](valid)).rejects.toEqual(
new InternalServerError(`Cannot get block hash for ${valid}.`),
Expand Down
Loading