Skip to content

Commit

Permalink
feat(core): option to enable Substrate Connect support (#538)
Browse files Browse the repository at this point in the history
  • Loading branch information
tien authored Mar 4, 2025
1 parent 2071712 commit 9304e56
Show file tree
Hide file tree
Showing 3 changed files with 201 additions and 93 deletions.
5 changes: 5 additions & 0 deletions .changeset/cuddly-bugs-switch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@reactive-dot/core": minor
---

Added option to enable Substrate Connect support.
171 changes: 116 additions & 55 deletions packages/core/src/providers/light-client/provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,83 +1,144 @@
import {
createClientFromLightClientProvider,
createLightClientProvider,
isLightClientProvider,
createClientFromLightClientProvider,
type LightClientProvider,
} from "./provider.js";
import { it, expect, beforeEach, vi } from "vitest";
import { beforeEach, describe, expect, it, vi } from "vitest";

// Create fake implementations for dependencies
vi.mock("polkadot-api", () => ({
createClient: vi.fn().mockReturnValue({ mockClient: true }),
}));

const fakeSmoldot = {
addChain: vi.fn((chain: string | { chainSpec: string }) =>
Promise.resolve({ chainSpec: chain, id: "added" }),
),
};
vi.mock("polkadot-api/sm-provider", () => ({
getSmProvider: vi.fn().mockImplementation((chain) => ({
provider: true,
chain,
})),
}));

// Mock @substrate/smoldot-discovery to return a fake smoldot provider
vi.mock("@substrate/smoldot-discovery", () => ({
getSmoldotExtensionProviders: () => [
{ provider: Promise.resolve(fakeSmoldot) },
],
vi.mock("./wellknown-chains.js", () => ({
wellknownChains: {
polkadot: [
vi.fn().mockResolvedValue({ chainSpec: "polkadot-spec" }),
{
polkadot_asset_hub: vi
.fn()
.mockResolvedValue({ chainSpec: "asset-hub-spec" }),
},
],
kusama: [
vi.fn().mockResolvedValue({ chainSpec: "kusama-spec" }),
{
polkadot_asset_hub: vi
.fn()
.mockResolvedValue({ chainSpec: "asset-hub-kusama-spec" }),
},
],
},
}));

// Mock polkadot-api/sm-provider to wrap chains in a fake provider
vi.mock("polkadot-api/sm-provider", () => ({
getSmProvider: vi.fn((chain) => Promise.resolve({ clientChain: chain })),
global.Worker = class {
constructor() {}
postMessage = vi.fn();
addEventListener = vi.fn();
} as unknown as typeof Worker;

// Mock smoldot imports
vi.mock("polkadot-api/smoldot/from-worker", () => ({
startFromWorker: vi.fn().mockResolvedValue({
addChain: vi.fn().mockImplementation(({ chainSpec }) => ({
chainId: "chain-id",
chainSpec,
})),
}),
}));

// Mock polkadot-api to return a fake client with the provided provider
vi.mock("polkadot-api", () => ({
createClient: vi.fn((provider) => ({ sm_client: provider })),
vi.mock("@substrate/smoldot-discovery", () => ({
getSmoldotExtensionProviders: vi.fn().mockReturnValue([
{
provider: {
addChain: vi.fn().mockImplementation((chainSpec) => ({
chainId: "sc-chain-id",
chainSpec,
})),
},
},
]),
}));

beforeEach(() => {
vi.clearAllMocks();
});
describe("Light Client Provider", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("should create a relay chain provider with chainSpec", async () => {
const lightClient = createLightClientProvider();
const relayProvider = lightClient.addRelayChain({
chainSpec: "fakeRelayChainSpec",
it("should create a light client provider without substrate connect", async () => {
const provider = createLightClientProvider();

expect(provider).toBeDefined();
expect(provider.addRelayChain).toBeInstanceOf(Function);
});

const client = await createClientFromLightClientProvider(relayProvider);
it("should create a light client provider with substrate connect enabled", async () => {
const provider = createLightClientProvider({
useExtensionProvider: true,
});

// Expect the fake sm-provider to have been called with the result of smoldot.addChain.
expect(client).toEqual({
sm_client: {
clientChain: { chainSpec: "fakeRelayChainSpec", id: "added" },
},
expect(provider).toBeDefined();
expect(provider.addRelayChain).toBeInstanceOf(Function);
});
expect(fakeSmoldot.addChain).toHaveBeenCalledWith("fakeRelayChainSpec");
});

it("should create a parachain provider with chainSpec", async () => {
const lightClient = createLightClientProvider();
const relayProvider = lightClient.addRelayChain({
chainSpec: "fakeRelayChainSpec",
it("should add relay chain with chain spec", async () => {
const provider = createLightClientProvider();
const relayChain = provider.addRelayChain({ chainSpec: "test-chain-spec" });

expect(relayChain).toBeDefined();
expect(isLightClientProvider(relayChain)).toBeTruthy();
});
const parachainProvider = relayProvider.addParachain({
chainSpec: "fakeParachainSpec",

it("should add relay chain with wellknown id", async () => {
const provider = createLightClientProvider();
const relayChain = provider.addRelayChain({ id: "polkadot" });

expect(relayChain).toBeDefined();
expect(isLightClientProvider(relayChain)).toBeTruthy();
});

const client = await createClientFromLightClientProvider(parachainProvider);
it("should add parachain to relay chain", async () => {
const provider = createLightClientProvider();
const relayChain = provider.addRelayChain({ id: "polkadot" });
const parachain = relayChain.addParachain({ id: "polkadot_asset_hub" });

// Expect the parachain chain addition to use its own chainSpec.
expect(client).toEqual({
sm_client: {
clientChain: { chainSpec: "fakeParachainSpec", id: "added" },
},
expect(parachain).toBeDefined();
expect(isLightClientProvider(parachain)).toBeTruthy();
});
expect(fakeSmoldot.addChain).toHaveBeenCalledTimes(2);
});

it("should recognize a valid LightClientProvider", () => {
const lightClient = createLightClientProvider();
const relayProvider = lightClient.addRelayChain({
chainSpec: "fakeRelayChainSpec",
it("should add parachain with chain spec to relay chain", async () => {
const provider = createLightClientProvider();
const relayChain = provider.addRelayChain({ id: "polkadot" });
const parachain = relayChain.addParachain({ chainSpec: "parachain-spec" });

expect(parachain).toBeDefined();
expect(isLightClientProvider(parachain)).toBeTruthy();
});
const nonProvider = {};

expect(isLightClientProvider(relayProvider)).toBe(true);
expect(isLightClientProvider(nonProvider)).toBe(false);
it("should identify light client providers correctly", () => {
const provider = createLightClientProvider();
const relayChain = provider.addRelayChain({ id: "polkadot" });

expect(isLightClientProvider(relayChain)).toBeTruthy();
expect(isLightClientProvider({})).toBeFalsy();
expect(isLightClientProvider(null)).toBeFalsy();
expect(isLightClientProvider(undefined)).toBeFalsy();
});

it("should create client from light client provider", () => {
const provider = createLightClientProvider();
const relayChain = provider.addRelayChain({ id: "polkadot" });
const client = createClientFromLightClientProvider(
relayChain as LightClientProvider,
);

expect(client).toEqual({ mockClient: true });
});
});
118 changes: 80 additions & 38 deletions packages/core/src/providers/light-client/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,38 @@ import {
type WellknownParachainId,
type WellknownRelayChainId,
} from "./wellknown-chains.js";
import { getSmoldotExtensionProviders } from "@substrate/smoldot-discovery";
import type { getSmoldotExtensionProviders } from "@substrate/smoldot-discovery";
import { createClient } from "polkadot-api";
import { getSmProvider } from "polkadot-api/sm-provider";
import { startFromWorker } from "polkadot-api/smoldot/from-worker";
import type { JsonRpcProvider } from "polkadot-api/ws-provider/web";

const getProviderSymbol = Symbol("getProvider");

export type LightClientProvider = {
[getProviderSymbol]: () => Promise<JsonRpcProvider>;
[getProviderSymbol]: () => JsonRpcProvider;
};

type AddChainOptions<TWellknownChainId> =
| { chainSpec: string }
| { id: TWellknownChainId };

export function createLightClientProvider() {
const getSmoldot = lazy(
() =>
getSmoldotExtensionProviders().at(0)?.provider ??
startFromWorker(
new Worker(new URL("polkadot-api/smoldot/worker", import.meta.url), {
type: "module",
}),
),
);
type LightClientOptions = {
/**
* Connect to the first available {@link https://github.com/paritytech/substrate-connect | Substrate Connect} provider.
*/
useExtensionProvider?: boolean;
};

export function createLightClientProvider({
useExtensionProvider = true,
}: LightClientOptions = {}) {
const getSmoldot = lazy(async () => {
if (!useExtensionProvider) {
return startSmoldotWorker();
}

return (await startSubstrateConnectWorker()) ?? startSmoldotWorker();
});

return {
addRelayChain<TRelayChainId extends WellknownRelayChainId>(
Expand All @@ -42,19 +48,20 @@ export function createLightClientProvider() {
: wellknownChains[options.id][0]().then((chain) => chain.chainSpec),
);

const getRelayChain = lazy(() => {
const smoldot = getSmoldot();
const getRelayChain = lazy(async () => {
const smoldot = await getSmoldot();
const chainSpec = await getChainSpec();

if (isSubstrateConnectProvider(smoldot)) {
return smoldot.addChain(chainSpec);
}

return smoldot instanceof Promise
? smoldot.then(async (smoldot) =>
smoldot.addChain(await getChainSpec()),
)
: getChainSpec().then((chainSpec) => smoldot.addChain({ chainSpec }));
return smoldot.addChain({ chainSpec });
});

return addLightClientProvider({
async [getProviderSymbol]() {
return getSmProvider(await getRelayChain());
[getProviderSymbol]() {
return getSmProvider(getRelayChain());
},

addParachain<
Expand All @@ -64,7 +71,7 @@ export function createLightClientProvider() {
: keyof (typeof wellknownChains)[TRelayChainId][1],
>(options: AddChainOptions<TParachainId>) {
return addLightClientProvider({
async [getProviderSymbol]() {
[getProviderSymbol]() {
const chainSpecPromise =
"chainSpec" in options
? Promise.resolve(options.chainSpec)
Expand All @@ -83,21 +90,19 @@ export function createLightClientProvider() {
]).then(([relayChain, chainSpec]) =>
"addChain" in relayChain
? relayChain.addChain(chainSpec)
: (() => {
const smoldot = getSmoldot();
: (async () => {
const smoldot = await getSmoldot();

return smoldot instanceof Promise
? smoldot.then(async (smoldot) =>
smoldot.addChain(chainSpec),
)
return isSubstrateConnectProvider(smoldot)
? smoldot.addChain(chainSpec)
: smoldot.addChain({
chainSpec,
potentialRelayChains: [relayChain],
});
})(),
);

return getSmProvider(await parachainPromise);
return getSmProvider(parachainPromise);
},
});
},
Expand All @@ -106,21 +111,58 @@ export function createLightClientProvider() {
};
}

export function isLightClientProvider(
value: unknown,
): value is LightClientProvider {
return lightClientProviders.has(value as LightClientProvider);
}

export function createClientFromLightClientProvider(
provider: LightClientProvider,
) {
return createClient(provider[getProviderSymbol]());
}

const lightClientProviders = new WeakSet<LightClientProvider>();

function addLightClientProvider<T extends LightClientProvider>(provider: T) {
lightClientProviders.add(provider);
return provider;
}

export function isLightClientProvider(
value: unknown,
): value is LightClientProvider {
return lightClientProviders.has(value as LightClientProvider);
function startSmoldotWorker() {
return import("polkadot-api/smoldot/from-worker").then(
({ startFromWorker }) =>
startFromWorker(
new Worker(new URL("polkadot-api/smoldot/worker", import.meta.url), {
type: "module",
}),
),
);
}

export async function createClientFromLightClientProvider(
provider: LightClientProvider,
) {
return createClient(await provider[getProviderSymbol]());
const substrateConnectSet = new WeakSet<
Awaited<ReturnType<typeof getSmoldotExtensionProviders>[number]["provider"]>
>();

function startSubstrateConnectWorker() {
return import("@substrate/smoldot-discovery").then(
async ({ getSmoldotExtensionProviders }) => {
const provider = await getSmoldotExtensionProviders().at(0)?.provider;

if (provider !== undefined) {
substrateConnectSet.add(provider);
}

return provider;
},
);
}

function isSubstrateConnectProvider(
value: unknown,
): value is Awaited<
ReturnType<typeof getSmoldotExtensionProviders>[number]["provider"]
> {
return substrateConnectSet.has(value as never);
}

0 comments on commit 9304e56

Please sign in to comment.