diff --git a/rpc/provider/smoldot.test.ts b/rpc/provider/smoldot.test.ts index bd8b37039..96d081695 100644 --- a/rpc/provider/smoldot.test.ts +++ b/rpc/provider/smoldot.test.ts @@ -1,66 +1,143 @@ -// TODO -import { deferred } from "../../deps/std/async.ts"; +import { deferred, delay } from "../../deps/std/async.ts"; import { assertExists, assertNotInstanceOf } from "../../deps/std/testing/asserts.ts"; import { ProviderListener } from "./base.ts"; import { smoldotProvider } from "./smoldot.ts"; -const POLKADOT_CHAIN_SPEC_URL = - "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/polkadot.json"; - Deno.test({ name: "Smoldot Provider", sanitizeOps: false, sanitizeResources: false, - async fn() { - const polkadotChainSpec = await (await fetch(POLKADOT_CHAIN_SPEC_URL)).text(); - const pendingSubscriptionId = deferred(); - const initialized = deferred(); - const unsubscribed = deferred(); - const checks: ProviderListener[] = [ - // check for chainHead_unstable_follow subscription - (message) => { - assertNotInstanceOf(message, Error); - assertExists(message.result); - pendingSubscriptionId.resolve(message.result); - }, - // check for chainHead_unstable_follow initialized event - (message) => { - assertNotInstanceOf(message, Error); - assertExists(message.params?.result); - if (message.params?.result.event === "initialized") { - initialized.resolve(); - } - }, - // check for chainHead_unstable_unfollow unsubscribe - (message) => { - assertNotInstanceOf(message, Error); - if (message?.result === null) { - unsubscribed.resolve(); - } + async fn(t) { + await t.step({ + name: "relay chain connection", + async fn() { + const relayChainSpec = await ( + await fetch( + "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/polkadot.json", + ) + ) + .text(); + const pendingSubscriptionId = deferred(); + const initialized = deferred(); + const unsubscribed = deferred(); + const checks: ProviderListener[] = [ + // check for chainHead_unstable_follow subscription + (message) => { + assertNotInstanceOf(message, Error); + assertExists(message.result); + pendingSubscriptionId.resolve(message.result); + }, + // check for chainHead_unstable_follow initialized event + (message) => { + assertNotInstanceOf(message, Error); + assertExists(message.params?.result); + if (message.params?.result.event === "initialized") { + initialized.resolve(); + } + }, + // check for chainHead_unstable_unfollow unsubscribe + (message) => { + assertNotInstanceOf(message, Error); + if (message?.result === null) { + unsubscribed.resolve(); + } + }, + ]; + const provider = smoldotProvider(relayChainSpec, (message) => { + if (checks.length > 1) { + checks.shift()!(message); + } else { + checks[0]!(message); + } + }); + provider.send({ + jsonrpc: "2.0", + id: provider.nextId(), + method: "chainHead_unstable_follow", + params: [false], + }); + const subscriptionId = await pendingSubscriptionId; + await initialized; + provider.send({ + jsonrpc: "2.0", + id: provider.nextId(), + method: "chainHead_unstable_unfollow", + params: [subscriptionId], + }); + await unsubscribed; + const providerRelease = await provider.release(); + assertNotInstanceOf(providerRelease, Error); }, - ]; - const provider = smoldotProvider(polkadotChainSpec, (message) => { - if (checks.length > 1) { - checks.shift()!(message); - } else { - checks[0]!(message); - } }); - provider.send({ - jsonrpc: "2.0", - id: provider.nextId(), - method: "chainHead_unstable_follow", - params: [true], - }); - const subscriptionId = await pendingSubscriptionId; - await initialized; - provider.send({ - jsonrpc: "2.0", - id: provider.nextId(), - method: "chainHead_unstable_unfollow", - params: [subscriptionId], + await t.step({ + name: "parachain connection", + async fn() { + const relayChainSpec = await ( + await fetch( + "https://raw.githubusercontent.com/paritytech/substrate-connect/main/packages/connect/src/connector/specs/westend2.json", + ) + ) + .text(); + const parachainSpec = await ( + await fetch( + "https://raw.githubusercontent.com/paritytech/substrate-connect/main/projects/demo/src/assets/westend-westmint.json", + ) + ) + .text(); + const pendingSubscriptionId = deferred(); + const initialized = deferred(); + const unsubscribed = deferred(); + const checks: ProviderListener[] = [ + // check for chainHead_unstable_follow subscription + (message) => { + assertNotInstanceOf(message, Error); + assertExists(message.result); + pendingSubscriptionId.resolve(message.result); + }, + // check for chainHead_unstable_follow initialized event + (message) => { + assertNotInstanceOf(message, Error); + assertExists(message.params?.result); + if (message.params?.result.event === "initialized") { + initialized.resolve(); + } + }, + // check for chainHead_unstable_unfollow unsubscribe + (message) => { + assertNotInstanceOf(message, Error); + if (message?.result === null) { + unsubscribed.resolve(); + } + }, + ]; + const provider = smoldotProvider( + [parachainSpec, relayChainSpec], + (message) => { + if (checks.length > 1) { + checks.shift()!(message); + } else { + checks[0]!(message); + } + }, + ); + provider.send({ + jsonrpc: "2.0", + id: provider.nextId(), + method: "chainHead_unstable_follow", + params: [false], + }); + const subscriptionId = await pendingSubscriptionId; + await initialized; + provider.send({ + jsonrpc: "2.0", + id: provider.nextId(), + method: "chainHead_unstable_unfollow", + params: [subscriptionId], + }); + await unsubscribed; + const providerRelease = await provider.release(); + assertNotInstanceOf(providerRelease, Error); + }, }); - await unsubscribed; - await provider.release(); }, }); diff --git a/rpc/provider/smoldot.ts b/rpc/provider/smoldot.ts index f848c2122..98a50c862 100644 --- a/rpc/provider/smoldot.ts +++ b/rpc/provider/smoldot.ts @@ -8,6 +8,7 @@ import { start, } from "../../deps/smoldot.ts"; import { Chain, Client, ClientOptions } from "../../deps/smoldot/client.d.ts"; +import { deferred } from "../../deps/std/async.ts"; import * as msg from "../messages.ts"; import { nextIdFactory, Provider, ProviderConnection, ProviderListener } from "./base.ts"; import { ProviderCloseError, ProviderHandlerError, ProviderSendError } from "./errors.ts"; @@ -26,15 +27,17 @@ type SmoldotHandlerErrorData = type SmoldotCloseErrorData = AlreadyDestroyedError | CrashError; let client: undefined | Client; -const connections = new Map(); +const connections = new Map(); class SmoldotProviderConnection extends ProviderConnection {} const nextId = nextIdFactory(); +type ChainSpecOptions = string | [parachainSpec: string, relayChainSpec: string]; + export const smoldotProvider: Provider< - string, + ChainSpecOptions, SmoldotSendErrorData, SmoldotHandlerErrorData, SmoldotCloseErrorData @@ -75,7 +78,7 @@ export const smoldotProvider: Provider< }; async function connection( - chainSpec: string, + chainSpec: ChainSpecOptions, listener: ProviderListener, ): Promise { if (!client) { @@ -88,17 +91,31 @@ async function connection( } let conn = connections.get(chainSpec); if (!conn) { - const inner = await client.addChain({ chainSpec }); - conn = new SmoldotProviderConnection(inner, () => { - try { - inner.remove(); - } catch (_e) { /* TODO */ } - }); + let inner: Chain; + if (typeof chainSpec === "string") { + inner = await client.addChain({ chainSpec }); + } else { + const [parachainSpec, relayChainSpec] = chainSpec; + const relayChainConnection = await client.addChain({ chainSpec: relayChainSpec }); + inner = await client.addChain({ + chainSpec: parachainSpec, + potentialRelayChains: [relayChainConnection], + }); + } + const stopListening = deferred(); + conn = new SmoldotProviderConnection(inner, () => stopListening.resolve()); connections.set(chainSpec, conn); (async () => { while (true) { try { - const message = msg.parse(await inner.nextJsonRpcResponse()); + const response = await Promise.race([ + stopListening, + inner.nextJsonRpcResponse(), + ]); + if (!response) { + break; + } + const message = msg.parse(response); conn!.forEachListener(message); } catch (e) { conn!.forEachListener(new ProviderHandlerError(e as SmoldotHandlerErrorData));