diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a9bd07d1d..ea6f83913f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,25 @@ ## master +- **Breaking change** For users of `ScProvider` you now need to explicitly pass `@substrate/connect` as a parameter. This means the code needs to be adjusted as follows - + +```js +import { ScProvider } from '@polkadot/api'; +import * as Sc from '@substrate/connect'; + +const provider = new ScProvider(Sc, Sc.WellKnownChain.polkadot); +``` + Contributed: - Fix for typegen with nested tuples (Thanks to https://github.com/sander2) - Expose blockNumber on submittable results (Thanks to https://github.com/ken-centrality) +Changes: + +- The `ScProvider` interface now needs receive an `@substrate/connect` instance +- Along with the above `ScProvider.WellKnownChains` has been removed + ## 9.10.5 Dec 27, 2022 diff --git a/packages/rpc-provider/README.md b/packages/rpc-provider/README.md index 22029c92fe..e114fb2ddc 100644 --- a/packages/rpc-provider/README.md +++ b/packages/rpc-provider/README.md @@ -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(); diff --git a/packages/rpc-provider/package.json b/packages/rpc-provider/package.json index 8eb29a9568..ac8044deb6 100644 --- a/packages/rpc-provider/package.json +++ b/packages/rpc-provider/package.json @@ -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" } } diff --git a/packages/rpc-provider/src/substrate-connect/Health.ts b/packages/rpc-provider/src/substrate-connect/Health.ts index df9def1676..9758d019da 100644 --- a/packages/rpc-provider/src/substrate-connect/Health.ts +++ b/packages/rpc-provider/src/substrate-connect/Health.ts @@ -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', diff --git a/packages/rpc-provider/src/substrate-connect/index.spec.ts b/packages/rpc-provider/src/substrate-connect/index.spec.ts index 104abc0c32..64c1575b2d 100644 --- a/packages/rpc-provider/src/substrate-connect/index.spec.ts +++ b/packages/rpc-provider/src/substrate-connect/index.spec.ts @@ -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) => { addWellKnownChainInterceptor = interceptor; }, - addChain: (chainSpec: string, cb: JsonRpcCallback): Promise => + addChain: (chainSpec: string, cb: Sc.JsonRpcCallback): Promise => addChainInterceptor.then(() => { const result = getFakeChain(chainSpec, cb); @@ -143,7 +144,7 @@ const getFakeClient = () => { }), addWellKnownChain: ( wellKnownChain: string, - cb: JsonRpcCallback + cb: Sc.JsonRpcCallback ): Promise => addWellKnownChainInterceptor.then(() => { const result = getFakeChain(wellKnownChain, cb); @@ -182,10 +183,7 @@ const connectorFactory = () => { }; }; -jest.mock('@substrate/connect', () => connectorFactory()); - -let ScProvider: ScProviderClass; -let mockedConnector: ReturnType; +let mockSc: typeof Sc & { latestChain: () => MockChain }; let mockedHealthChecker: ReturnType; 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; - 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"]}' ]); }); }); diff --git a/packages/rpc-provider/src/substrate-connect/index.ts b/packages/rpc-provider/src/substrate-connect/index.ts index b841c800b2..148c2b502f 100644 --- a/packages/rpc-provider/src/substrate-connect/index.ts +++ b/packages/rpc-provider/src/substrate-connect/index.ts @@ -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([ ['state_subscribeStorage', 'state_unsubscribeStorage'] ]); -const wellKnownChains = new Set(Object.values(WellKnownChain)); -const scClients = new WeakMap(); +const scClients = new WeakMap(); 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 = new Map(); readonly #resubscribeMethods: Map = new Map(); readonly #requests: Map = new Map(); + readonly #wellKnownChains: Set; readonly #eventemitter: EventEmitter = new EventEmitter(); - #chain: Promise | null = null; + #chain: Promise | 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 { + async connect (config?: ScType.Config, checkerFactory = healthChecker): Promise { 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 diff --git a/packages/rpc-provider/src/substrate-connect/types.ts b/packages/rpc-provider/src/substrate-connect/types.ts new file mode 100644 index 0000000000..6c4e75f001 --- /dev/null +++ b/packages/rpc-provider/src/substrate-connect/types.ts @@ -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 +} diff --git a/yarn.lock b/yarn.lock index 87c55672f5..435cc9ea0a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2249,6 +2249,9 @@ __metadata: eventemitter3: ^4.0.7 mock-socket: ^9.1.5 nock: ^13.2.9 + dependenciesMeta: + "@substrate/connect": + optional: true languageName: unknown linkType: soft