Pass ScConnect as argument to ScProvider (#5405)

* Pass ScConnect as argument to ScProvider

* CHANGELOG

* Adjust type exports

* Adjust

* Adjust

* Renames

* throw error on non-TS

* Attempt to fix tests

* Comment around cowboy class

* Remove all .skip (no dynamic import)

* Move @substrate/connect to dev/optional dep

* typo
This commit is contained in:
Jaco
2023-01-07 15:30:35 +02:00
committed by GitHub
parent 81b5d2280d
commit 5c1e701ab6
8 changed files with 125 additions and 92 deletions
+5 -3
View File
@@ -45,8 +45,9 @@ console.log('latest block Hash', hash);
Instantiating a Provider for the Polkadot Relay Chain:
```javascript
import { ScProvider } from '@polkadot/rpc-provider';
import * as Sc from '@substrate/connect';
const provider = new ScProvider(ScProvider.WellKnownChain.polkadot);
const provider = new ScProvider(Sc, Sc.WellKnownChain.polkadot);
await provider.connect();
@@ -56,9 +57,10 @@ const version = await provider.send('chain_getBlockHash', []);
Instantiating a Provider for a Polkadot parachain:
```javascript
import { ScProvider } from '@polkadot/rpc-provider';
import * as Sc from '@substrate/connect';
const polkadotProvider = new ScProvider(ScProvider.WellKnownChain.polkadot);
const parachainProvider = new ScProvider(parachainSpec, polkadotProvider);
const polkadotProvider = new ScProvider(Sc, Sc.WellKnownChain.polkadot);
const parachainProvider = new ScProvider(Sc, parachainSpec, polkadotProvider);
await parachainProvider.connect();
+6 -1
View File
@@ -32,9 +32,14 @@
"@polkadot/x-fetch": "^10.2.2",
"@polkadot/x-global": "^10.2.2",
"@polkadot/x-ws": "^10.2.2",
"@substrate/connect": "0.7.18",
"eventemitter3": "^4.0.7",
"mock-socket": "^9.1.5",
"nock": "^13.2.9"
},
"devDependencies": {
"@substrate/connect": "0.7.18"
},
"optionalDependencies": {
"@substrate/connect": "0.7.18"
}
}
@@ -1,22 +1,10 @@
// Copyright 2017-2023 @polkadot/rpc-provider authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { HealthChecker, SmoldotHealth } from './types';
import { stringify } from '@polkadot/util';
export interface SmoldotHealth {
isSyncing: boolean
peers: number
shouldHavePeers: boolean
}
export interface HealthChecker {
setSendJsonRpc(sendRequest: (request: string) => void): void
start(healthCallback: (health: SmoldotHealth) => void): void
stop(): void
sendJsonRpc(request: string): void
responsePassThrough(response: string): string | null
}
interface JSONRequest {
id: string;
jsonrpc: '2.0',
@@ -18,12 +18,13 @@
// FIXME A number of tests here, that were passing, is not skipped since
// Jest has "some" issues with `await import` - we don't transform these
import type { Chain, JsonRpcCallback } from '@substrate/connect';
import type { ScProviderClass } from '.';
import type { HealthChecker, SmoldotHealth } from './Health';
import type Sc from '@substrate/connect';
import type { HealthChecker, SmoldotHealth } from './types';
import { jest } from '@jest/globals';
import { ScProvider } from '.';
const wait = (ms: number) => new Promise((res) => setTimeout(res, ms));
type MockedHealthChecker = HealthChecker & {
@@ -76,7 +77,7 @@ const healthCheckerFactory = () => {
jest.mock('./Health', () => healthCheckerFactory());
type MockChain = Chain & {
type MockChain = Sc.Chain & {
_spec: () => string
_recevedRequests: () => string[]
_isTerminated: () => boolean
@@ -86,7 +87,7 @@ type MockChain = Chain & {
_getLatestRequest: () => string
}
const getFakeChain = (spec: string, callback: JsonRpcCallback): MockChain => {
const getFakeChain = (spec: string, callback: Sc.JsonRpcCallback): MockChain => {
const _receivedRequests: string[] = [];
let _isTerminated = false;
@@ -133,7 +134,7 @@ const getFakeClient = () => {
_setAddWellKnownChainInterceptor: (interceptor: Promise<void>) => {
addWellKnownChainInterceptor = interceptor;
},
addChain: (chainSpec: string, cb: JsonRpcCallback): Promise<MockChain> =>
addChain: (chainSpec: string, cb: Sc.JsonRpcCallback): Promise<MockChain> =>
addChainInterceptor.then(() => {
const result = getFakeChain(chainSpec, cb);
@@ -143,7 +144,7 @@ const getFakeClient = () => {
}),
addWellKnownChain: (
wellKnownChain: string,
cb: JsonRpcCallback
cb: Sc.JsonRpcCallback
): Promise<MockChain> =>
addWellKnownChainInterceptor.then(() => {
const result = getFakeChain(wellKnownChain, cb);
@@ -182,10 +183,7 @@ const connectorFactory = () => {
};
};
jest.mock('@substrate/connect', () => connectorFactory());
let ScProvider: ScProviderClass;
let mockedConnector: ReturnType<typeof connectorFactory>;
let mockSc: typeof Sc & { latestChain: () => MockChain };
let mockedHealthChecker: ReturnType<typeof healthCheckerFactory>;
const getCurrentHealthChecker = () => mockedHealthChecker._latestHealthChecker();
@@ -197,18 +195,15 @@ const setChainSyncyingStatus = (isSyncing: boolean) => {
});
};
beforeAll(async () => {
({ ScProvider } = await import('.'));
mockedConnector = (await import(
'@substrate/connect'
)) as unknown as ReturnType<typeof connectorFactory>;
mockedHealthChecker = healthCheckerFactory(); // await import('./Health')
beforeAll(() => {
mockSc = connectorFactory() as unknown as typeof Sc & { latestChain: () => MockChain };
mockedHealthChecker = healthCheckerFactory();
});
describe('ScProvider', () => {
describe('on', () => {
it('emits `connected` as soon as the chain is not syncing', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
@@ -222,7 +217,7 @@ describe('ScProvider', () => {
});
it('stops receiving notifications after unsubscribing', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
@@ -236,7 +231,7 @@ describe('ScProvider', () => {
});
it('synchronously emits connected if the Provider is already `connected`', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
@@ -249,7 +244,7 @@ describe('ScProvider', () => {
});
it('emits `disconnected` once the chain goes back to syncing', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
@@ -275,7 +270,7 @@ describe('ScProvider', () => {
describe('hasSubscriptions', () => {
it('supports subscriptions', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
@@ -285,7 +280,7 @@ describe('ScProvider', () => {
describe('clone', () => {
it('can not be clonned', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
@@ -294,18 +289,18 @@ describe('ScProvider', () => {
});
describe('connect', () => {
it.skip('does not create a new chain when trying to re-connect while the current chain is syncing', async () => {
const provider = new ScProvider('');
it('does not create a new chain when trying to re-connect while the current chain is syncing', async () => {
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
const chain = mockedConnector.latestChain();
const chain = mockSc.latestChain();
await provider.connect(undefined, mockedHealthChecker.healthChecker);
expect(chain).toBe(mockedConnector.latestChain());
expect(chain).toBe(mockSc.latestChain());
});
it('throws when trying to connect on an already connected Provider', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
@@ -318,11 +313,11 @@ describe('ScProvider', () => {
});
describe('disconnect', () => {
it.skip('removes the chain and cleans up', async () => {
const provider = new ScProvider('');
it('removes the chain and cleans up', async () => {
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
const chain = mockedConnector.latestChain();
const chain = mockSc.latestChain();
await provider.disconnect();
@@ -330,7 +325,7 @@ describe('ScProvider', () => {
});
it('does not throw when disconnecting on an already disconnected Provider', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
@@ -341,18 +336,18 @@ describe('ScProvider', () => {
describe('send', () => {
it('throws when trying to send a request while the Provider is not connected', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
await expect(provider.send('', [])).rejects.toThrow();
});
it.skip('receives responses to its requests', async () => {
const provider = new ScProvider('');
it('receives responses to its requests', async () => {
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
const chain = mockedConnector.latestChain();
const chain = mockSc.latestChain();
setChainSyncyingStatus(false);
@@ -376,11 +371,11 @@ describe('ScProvider', () => {
expect(response).toEqual(result);
});
it.skip("rejects when the response can't be deserialized", async () => {
const provider = new ScProvider('');
it("rejects when the response can't be deserialized", async () => {
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
const chain = mockedConnector.latestChain();
const chain = mockSc.latestChain();
setChainSyncyingStatus(false);
@@ -394,11 +389,11 @@ describe('ScProvider', () => {
await expect(provider.send('getData', ['foo'])).rejects.toThrow();
});
it.skip('rejects when the smoldot chain has crashed', async () => {
const provider = new ScProvider('');
it('rejects when the smoldot chain has crashed', async () => {
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
const chain = mockedConnector.latestChain();
const chain = mockSc.latestChain();
setChainSyncyingStatus(false);
await wait(0);
@@ -415,11 +410,11 @@ describe('ScProvider', () => {
});
describe('subscribe', () => {
it.skip('subscribes and recives messages until it unsubscribes', async () => {
const provider = new ScProvider('');
it('subscribes and recives messages until it unsubscribes', async () => {
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
const chain = mockedConnector.latestChain();
const chain = mockSc.latestChain();
setChainSyncyingStatus(false);
@@ -480,11 +475,11 @@ describe('ScProvider', () => {
expect(cb).toHaveBeenLastCalledWith(null, 2);
});
it.skip('ignores subscription messages that were received before the subscription token', async () => {
const provider = new ScProvider('');
it('ignores subscription messages that were received before the subscription token', async () => {
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
const chain = mockedConnector.latestChain();
const chain = mockSc.latestChain();
setChainSyncyingStatus(false);
@@ -518,11 +513,11 @@ describe('ScProvider', () => {
expect(cb).not.toHaveBeenCalled();
});
it.skip('emits the error when the message has an error', async () => {
const provider = new ScProvider('');
it('emits the error when the message has an error', async () => {
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
const chain = mockedConnector.latestChain();
const chain = mockSc.latestChain();
setChainSyncyingStatus(false);
await wait(0);
@@ -561,7 +556,7 @@ describe('ScProvider', () => {
});
it('errors when subscribing to an unsupported method', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
@@ -576,7 +571,7 @@ describe('ScProvider', () => {
describe('unsubscribe', () => {
it('rejects when trying to unsubscribe from un unexisting subscription', async () => {
const provider = new ScProvider('');
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
@@ -588,11 +583,11 @@ describe('ScProvider', () => {
});
});
it.skip('cleans up the stale subscriptions once it reconnects', async () => {
const provider = new ScProvider('');
it('cleans up the stale subscriptions once it reconnects', async () => {
const provider = new ScProvider(mockSc, '');
await provider.connect(undefined, mockedHealthChecker.healthChecker);
const chain = mockedConnector.latestChain();
const chain = mockSc.latestChain();
// setting the syncing status of the chain to fals so that the Provider
// gets `connected`
@@ -644,7 +639,8 @@ describe('ScProvider', () => {
// stale subscription
expect(chain._recevedRequests()).toEqual([
'{"id":1,"jsonrpc":"2.0","method":"chain_subscribeNewHeads","params":["baz"]}',
`{"id":2,"jsonrpc":"2.0","method":"chain_unsubscribeNewHeads","params":["${token}"]}`
`{"id":2,"jsonrpc":"2.0","method":"chain_unsubscribeNewHeads","params":["${token}"]}`,
'{"id":3,"jsonrpc":"2.0","method":"chain_subscribeNewHeads","params":["baz"]}'
]);
});
});
@@ -1,18 +1,25 @@
// Copyright 2017-2023 @polkadot/rpc-provider authors & contributors
// SPDX-License-Identifier: Apache-2.0
import type { Chain, Config as ScConfig, ScClient } from '@substrate/connect';
import type ScType from '@substrate/connect';
import type { JsonRpcResponse, ProviderInterface, ProviderInterfaceCallback, ProviderInterfaceEmitCb, ProviderInterfaceEmitted } from '../types';
import { createScClient, WellKnownChain } from '@substrate/connect';
import EventEmitter from 'eventemitter3';
import { isError, logger, objectSpread } from '@polkadot/util';
import { isError, isFunction, isObject, logger, objectSpread } from '@polkadot/util';
import { RpcCoder } from '../coder';
import { healthChecker } from './Health';
type ResponseCallback = (response: string | Error) => void
type ResponseCallback = (response: string | Error) => void;
// We define the interface with items we use - this means that we don't really
// need to be passed a full `import * as Sc from '@ubstrate/connect'`, but can
// also make do with a { WellKnownChain, createScClient } interface
interface SubstrateConnect {
WellKnownChain: typeof ScType['WellKnownChain'];
createScClient: typeof ScType['createScClient'];
}
const l = logger('api-substrate-connect');
@@ -34,8 +41,7 @@ const subscriptionUnsubscriptionMethods = new Map<string, string>([
['state_subscribeStorage', 'state_unsubscribeStorage']
]);
const wellKnownChains = new Set(Object.values(WellKnownChain));
const scClients = new WeakMap<ScProvider, ScClient>();
const scClients = new WeakMap<ScProvider, ScType.ScClient>();
interface ActiveSubs {
type: string,
@@ -45,21 +51,27 @@ interface ActiveSubs {
}
export class ScProvider implements ProviderInterface {
readonly #Sc: SubstrateConnect;
readonly #coder: RpcCoder = new RpcCoder();
readonly #spec: string | WellKnownChain;
readonly #spec: string | ScType.WellKnownChain;
readonly #sharedSandbox?: ScProvider;
readonly #subscriptions: Map<string, [ResponseCallback, { unsubscribeMethod: string; id: string | number }]> = new Map();
readonly #resubscribeMethods: Map<string, ActiveSubs> = new Map();
readonly #requests: Map<number, ResponseCallback> = new Map();
readonly #wellKnownChains: Set<ScType.WellKnownChain>;
readonly #eventemitter: EventEmitter = new EventEmitter();
#chain: Promise<Chain> | null = null;
#chain: Promise<ScType.Chain> | null = null;
#isChainReady = false;
static WellKnownChain = WellKnownChain;
public constructor (Sc: SubstrateConnect, spec: string | ScType.WellKnownChain, sharedSandbox?: ScProvider) {
if (!isObject(Sc) || !isObject(Sc.WellKnownChain) || !isFunction(Sc.createScClient)) {
throw new Error('Expected an @substrate/connect interface as first parameter to ScProvider');
}
public constructor (spec: string | WellKnownChain, sharedSandbox?: ScProvider) {
this.#Sc = Sc;
this.#spec = spec;
this.#sharedSandbox = sharedSandbox;
this.#wellKnownChains = new Set(Object.values(Sc.WellKnownChain));
}
public get hasSubscriptions (): boolean {
@@ -81,7 +93,7 @@ export class ScProvider implements ProviderInterface {
// Config details can be found in @substrate/connect repo following the link:
// https://github.com/paritytech/substrate-connect/blob/main/packages/connect/src/connector/index.ts
async connect (config?: ScConfig, checkerFactory = healthChecker): Promise<void> {
async connect (config?: ScType.Config, checkerFactory = healthChecker): Promise<void> {
if (this.isConnected) {
throw new Error('Already connected!');
}
@@ -102,7 +114,7 @@ export class ScProvider implements ProviderInterface {
const client = this.#sharedSandbox
? scClients.get(this.#sharedSandbox)
: createScClient(config);
: this.#Sc.createScClient(config);
if (!client) {
throw new Error('Unkown ScProvider!');
@@ -141,11 +153,11 @@ export class ScProvider implements ProviderInterface {
callback?.(decodedResponse);
};
const addChain = wellKnownChains.has(this.#spec as WellKnownChain)
const addChain = this.#wellKnownChains.has(this.#spec as ScType.WellKnownChain)
? client.addWellKnownChain
: client.addChain;
this.#chain = addChain(this.#spec as WellKnownChain, onResponse).then((chain) => {
this.#chain = addChain(this.#spec as ScType.WellKnownChain, onResponse).then((chain) => {
hc.setSendJsonRpc(chain.sendJsonRpc);
this.#isChainReady = false;
@@ -392,6 +404,3 @@ export class ScProvider implements ProviderInterface {
return this.send(method, [id]);
}
}
export type ScProviderClass = typeof ScProvider
export type Config = ScConfig
@@ -0,0 +1,16 @@
// Copyright 2017-2023 @polkadot/rpc-provider authors & contributors
// SPDX-License-Identifier: Apache-2.0
export interface SmoldotHealth {
isSyncing: boolean
peers: number
shouldHavePeers: boolean
}
export interface HealthChecker {
setSendJsonRpc(sendRequest: (request: string) => void): void
start(healthCallback: (health: SmoldotHealth) => void): void
stop(): void
sendJsonRpc(request: string): void
responsePassThrough(response: string): string | null
}