diff --git a/.changeset/neat-fireants-smell.md b/.changeset/neat-fireants-smell.md new file mode 100644 index 00000000..c97bddf1 --- /dev/null +++ b/.changeset/neat-fireants-smell.md @@ -0,0 +1,5 @@ +--- +"@reactive-dot/core": minor +--- + +Added SubstrateConnect integration. diff --git a/apps/docs/react/getting-started/setup.mdx b/apps/docs/react/getting-started/setup.mdx index bd9683fa..aac1f021 100644 --- a/apps/docs/react/getting-started/setup.mdx +++ b/apps/docs/react/getting-started/setup.mdx @@ -54,23 +54,17 @@ For more information on metadata syncing and type generation, please refer to th // `dot` is the name we gave to `npx papi add` import { dot } from "@polkadot-api/descriptors"; import { defineConfig } from "@reactive-dot/core"; +import { createLightClientProvider } from "@reactive-dot/core/providers/light-client.js"; import { InjectedWalletProvider } from "@reactive-dot/core/wallets.js"; -import { chainSpec } from "polkadot-api/chains/polkadot"; -import { getSmProvider } from "polkadot-api/sm-provider"; -import { startFromWorker } from "polkadot-api/smoldot/from-worker"; -const smoldot = startFromWorker( - new Worker(new URL("polkadot-api/smoldot/worker", import.meta.url), { - type: "module", - }), -); +const lightClientProvider = createLightClientProvider(); export const config = defineConfig({ chains: { // "polkadot" here can be any unique string value polkadot: { descriptor: dot, - provider: getSmProvider(smoldot.addChain({ chainSpec })), + provider: lightClientProvider.addRelayChain({ id: "polkadot" }), }, }, wallets: [new InjectedWalletProvider()], diff --git a/apps/docs/vue/getting-started/setup.mdx b/apps/docs/vue/getting-started/setup.mdx index e4ecd9b0..a0eeb360 100644 --- a/apps/docs/vue/getting-started/setup.mdx +++ b/apps/docs/vue/getting-started/setup.mdx @@ -54,23 +54,17 @@ For more information on metadata syncing and type generation, please refer to th // `dot` is the name we gave to `npx papi add` import { dot } from "@polkadot-api/descriptors"; import { defineConfig } from "@reactive-dot/core"; +import { createLightClientProvider } from "@reactive-dot/core/providers/light-client.js"; import { InjectedWalletProvider } from "@reactive-dot/core/wallets.js"; -import { chainSpec } from "polkadot-api/chains/polkadot"; -import { getSmProvider } from "polkadot-api/sm-provider"; -import { startFromWorker } from "polkadot-api/smoldot/from-worker"; - -const smoldot = startFromWorker( - new Worker(new URL("polkadot-api/smoldot/worker", import.meta.url), { - type: "module", - }), -); + +const lightClientProvider = createLightClientProvider(); export const config = defineConfig({ chains: { // "polkadot" here can be any unique string value polkadot: { descriptor: dot, - provider: getSmProvider(smoldot.addChain({ chainSpec })), + provider: lightClientProvider.addRelayChain({ id: "polkadot" }), }, }, wallets: [new InjectedWalletProvider()], diff --git a/examples/react/.papi/descriptors/package.json b/examples/react/.papi/descriptors/package.json index be9c9382..89dd17f8 100644 --- a/examples/react/.papi/descriptors/package.json +++ b/examples/react/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.2857293709118977602", + "version": "0.1.0-autogenerated.14047238154392085370", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/examples/react/.papi/metadata/polkadot_asset_hub.scale b/examples/react/.papi/metadata/polkadot_asset_hub.scale new file mode 100644 index 00000000..b48010a4 Binary files /dev/null and b/examples/react/.papi/metadata/polkadot_asset_hub.scale differ diff --git a/examples/react/.papi/metadata/polkadot_people.scale b/examples/react/.papi/metadata/polkadot_people.scale new file mode 100644 index 00000000..1cf5a3f8 Binary files /dev/null and b/examples/react/.papi/metadata/polkadot_people.scale differ diff --git a/examples/react/.papi/polkadot-api.json b/examples/react/.papi/polkadot-api.json index 2de6a303..e514aaae 100644 --- a/examples/react/.papi/polkadot-api.json +++ b/examples/react/.papi/polkadot-api.json @@ -6,6 +6,14 @@ "chain": "polkadot", "metadata": ".papi/metadata/polkadot.scale" }, + "polkadot_asset_hub": { + "chain": "polkadot_asset_hub", + "metadata": ".papi/metadata/polkadot_asset_hub.scale" + }, + "polkadot_people": { + "chain": "polkadot_people", + "metadata": ".papi/metadata/polkadot_people.scale" + }, "kusama": { "chain": "ksmcc3", "metadata": ".papi/metadata/kusama.scale" diff --git a/examples/react/src/config.ts b/examples/react/src/config.ts index b09ef6fe..b8ce93df 100644 --- a/examples/react/src/config.ts +++ b/examples/react/src/config.ts @@ -1,42 +1,41 @@ -import { kusama, polkadot, westend } from "@polkadot-api/descriptors"; +import { + kusama, + polkadot, + polkadot_asset_hub, + polkadot_people, + westend, +} from "@polkadot-api/descriptors"; import { defineConfig } from "@reactive-dot/core"; +import { createLightClientProvider } from "@reactive-dot/core/providers/light-client.js"; import { InjectedWalletProvider } from "@reactive-dot/core/wallets.js"; import { LedgerWallet } from "@reactive-dot/wallet-ledger"; import { WalletConnect } from "@reactive-dot/wallet-walletconnect"; -import { getSmProvider } from "polkadot-api/sm-provider"; -import { startFromWorker } from "polkadot-api/smoldot/from-worker"; -const smoldot = startFromWorker( - new Worker(new URL("polkadot-api/smoldot/worker", import.meta.url), { - type: "module", - }), -); +const lightClientProvider = createLightClientProvider(); + +const polkadotProvider = lightClientProvider.addRelayChain({ id: "polkadot" }); export const config = defineConfig({ chains: { polkadot: { descriptor: polkadot, - provider: getSmProvider( - import("polkadot-api/chains/polkadot").then(({ chainSpec }) => - smoldot.addChain({ chainSpec }), - ), - ), + provider: polkadotProvider, + }, + polkadot_asset_hub: { + descriptor: polkadot_asset_hub, + provider: polkadotProvider.addParachain({ id: "polkadot_asset_hub" }), + }, + polkadot_people: { + descriptor: polkadot_people, + provider: polkadotProvider.addParachain({ id: "polkadot_people" }), }, kusama: { descriptor: kusama, - provider: getSmProvider( - import("polkadot-api/chains/ksmcc3").then(({ chainSpec }) => - smoldot.addChain({ chainSpec }), - ), - ), + provider: lightClientProvider.addRelayChain({ id: "kusama" }), }, westend: { descriptor: westend, - provider: getSmProvider( - import("polkadot-api/chains/westend2").then(({ chainSpec }) => - smoldot.addChain({ chainSpec }), - ), - ), + provider: lightClientProvider.addRelayChain({ id: "westend" }), }, }, wallets: [ diff --git a/examples/vue/.papi/descriptors/package.json b/examples/vue/.papi/descriptors/package.json index be9c9382..89dd17f8 100644 --- a/examples/vue/.papi/descriptors/package.json +++ b/examples/vue/.papi/descriptors/package.json @@ -1,5 +1,5 @@ { - "version": "0.1.0-autogenerated.2857293709118977602", + "version": "0.1.0-autogenerated.14047238154392085370", "name": "@polkadot-api/descriptors", "files": [ "dist" diff --git a/examples/vue/.papi/metadata/polkadot_asset_hub.scale b/examples/vue/.papi/metadata/polkadot_asset_hub.scale new file mode 100644 index 00000000..b48010a4 Binary files /dev/null and b/examples/vue/.papi/metadata/polkadot_asset_hub.scale differ diff --git a/examples/vue/.papi/metadata/polkadot_people.scale b/examples/vue/.papi/metadata/polkadot_people.scale new file mode 100644 index 00000000..1cf5a3f8 Binary files /dev/null and b/examples/vue/.papi/metadata/polkadot_people.scale differ diff --git a/examples/vue/.papi/polkadot-api.json b/examples/vue/.papi/polkadot-api.json index 2de6a303..e514aaae 100644 --- a/examples/vue/.papi/polkadot-api.json +++ b/examples/vue/.papi/polkadot-api.json @@ -6,6 +6,14 @@ "chain": "polkadot", "metadata": ".papi/metadata/polkadot.scale" }, + "polkadot_asset_hub": { + "chain": "polkadot_asset_hub", + "metadata": ".papi/metadata/polkadot_asset_hub.scale" + }, + "polkadot_people": { + "chain": "polkadot_people", + "metadata": ".papi/metadata/polkadot_people.scale" + }, "kusama": { "chain": "ksmcc3", "metadata": ".papi/metadata/kusama.scale" diff --git a/examples/vue/src/config.ts b/examples/vue/src/config.ts index a6555402..b8ce93df 100644 --- a/examples/vue/src/config.ts +++ b/examples/vue/src/config.ts @@ -1,43 +1,61 @@ -import { kusama, polkadot, westend } from "@polkadot-api/descriptors"; +import { + kusama, + polkadot, + polkadot_asset_hub, + polkadot_people, + westend, +} from "@polkadot-api/descriptors"; import { defineConfig } from "@reactive-dot/core"; +import { createLightClientProvider } from "@reactive-dot/core/providers/light-client.js"; import { InjectedWalletProvider } from "@reactive-dot/core/wallets.js"; -import { getSmProvider } from "polkadot-api/sm-provider"; -import { startFromWorker } from "polkadot-api/smoldot/from-worker"; +import { LedgerWallet } from "@reactive-dot/wallet-ledger"; +import { WalletConnect } from "@reactive-dot/wallet-walletconnect"; -const smoldot = startFromWorker( - new Worker(new URL("polkadot-api/smoldot/worker", import.meta.url), { - type: "module", - }), -); +const lightClientProvider = createLightClientProvider(); + +const polkadotProvider = lightClientProvider.addRelayChain({ id: "polkadot" }); export const config = defineConfig({ chains: { polkadot: { descriptor: polkadot, - provider: getSmProvider( - import("polkadot-api/chains/polkadot").then(({ chainSpec }) => - smoldot.addChain({ chainSpec }), - ), - ), + provider: polkadotProvider, + }, + polkadot_asset_hub: { + descriptor: polkadot_asset_hub, + provider: polkadotProvider.addParachain({ id: "polkadot_asset_hub" }), + }, + polkadot_people: { + descriptor: polkadot_people, + provider: polkadotProvider.addParachain({ id: "polkadot_people" }), }, kusama: { descriptor: kusama, - provider: getSmProvider( - import("polkadot-api/chains/ksmcc3").then(({ chainSpec }) => - smoldot.addChain({ chainSpec }), - ), - ), + provider: lightClientProvider.addRelayChain({ id: "kusama" }), }, westend: { descriptor: westend, - provider: getSmProvider( - import("polkadot-api/chains/westend2").then(({ chainSpec }) => - smoldot.addChain({ chainSpec }), - ), - ), + provider: lightClientProvider.addRelayChain({ id: "westend" }), }, }, wallets: [ - new InjectedWalletProvider({ originName: "ReactiveDOT Vue Example" }), + new InjectedWalletProvider({ originName: "ReactiveDOT React Example" }), + new LedgerWallet(), + new WalletConnect({ + projectId: "68f5b7e972a51cf379b127f51a791c34", + providerOptions: { + metadata: { + name: "ReactiveDOT example", + description: "Simple React App showcasing ReactiveDOT", + url: globalThis.location.origin, + icons: ["https://walletconnect.com/walletconnect-logo.png"], + }, + }, + chainIds: [ + "polkadot:91b171bb158e2d3848fa23a9f1c25182", // Polkadot + "polkadot:b0a8d493285c2df73290dfb7e61f870f", // Kusama + "polkadot:e143f23803ac50e8f6f8e62695d1ce9e", // Westend + ], + }), ], }); diff --git a/packages/core/package.json b/packages/core/package.json index a32705b3..ddcf5f2d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -25,6 +25,7 @@ ], "exports": { ".": "./build/index.js", + "./providers/light-client.js": "./build/providers/light-client/index.js", "./wallets.js": "./build/wallets/index.js", "./internal/maths.js": "./build/maths/index.js", "./internal.js": "./build/internal.js" @@ -36,7 +37,8 @@ "test": "vitest run" }, "dependencies": { - "@reactive-dot/utils": "workspace:^" + "@reactive-dot/utils": "workspace:^", + "@substrate/smoldot-discovery": "^2.0.1" }, "devDependencies": { "@reactive-dot/eslint-config": "workspace:^", diff --git a/packages/core/src/actions/get-client.ts b/packages/core/src/actions/get-client.ts index 97964a5c..6900cd7e 100644 --- a/packages/core/src/actions/get-client.ts +++ b/packages/core/src/actions/get-client.ts @@ -1,16 +1,32 @@ import type { ChainConfig } from "../config.js"; +import { + createClientFromLightClientProvider, + isLightClientProvider, + type LightClientProvider, +} from "../providers/light-client/provider.js"; import { createClient } from "polkadot-api"; import type { JsonRpcProvider } from "polkadot-api/ws-provider/web"; export async function getClient(chainConfig: ChainConfig) { const providerOrGetter = await chainConfig.provider; + if (isLightClientProvider(providerOrGetter)) { + return createClientFromLightClientProvider(providerOrGetter); + } + // Hack to detect wether function is a `JsonRpcProvider` or a getter of `JsonRpcProvider` - const provider = await (providerOrGetter.length > 0 - ? (providerOrGetter as JsonRpcProvider) + const providerOrController = await (providerOrGetter.length > 0 + ? (providerOrGetter as JsonRpcProvider | LightClientProvider) : ( - providerOrGetter as Exclude + providerOrGetter as Exclude< + typeof providerOrGetter, + JsonRpcProvider | LightClientProvider + > )()); - return createClient(provider); + if (isLightClientProvider(providerOrController)) { + return createClientFromLightClientProvider(providerOrController); + } + + return createClient(providerOrController); } diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index fb8091b8..56efbc45 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -1,3 +1,4 @@ +import type { LightClientProvider } from "./providers/light-client/provider.js"; import type { Gettable } from "./types.js"; import type { Wallet, WalletProvider } from "./wallets/index.js"; import type { JsonRpcProvider } from "@polkadot-api/json-rpc-provider"; @@ -5,7 +6,7 @@ import type { ChainDefinition } from "polkadot-api"; export type ChainConfig = { readonly descriptor: ChainDefinition; - readonly provider: Gettable; + readonly provider: Gettable; }; export type Config< diff --git a/packages/core/src/providers/light-client/index.ts b/packages/core/src/providers/light-client/index.ts new file mode 100644 index 00000000..557af4d0 --- /dev/null +++ b/packages/core/src/providers/light-client/index.ts @@ -0,0 +1 @@ +export { createLightClientProvider } from "./provider.js"; diff --git a/packages/core/src/providers/light-client/provider.ts b/packages/core/src/providers/light-client/provider.ts new file mode 100644 index 00000000..6f7e6797 --- /dev/null +++ b/packages/core/src/providers/light-client/provider.ts @@ -0,0 +1,119 @@ +/* eslint-disable no-unexpected-multiline */ +import { lazy } from "../../utils/lazy.js"; +import { + wellknownChains, + type WellknownParachainId, + type WellknownRelayChainId, +} from "./wellknown-chains.js"; +import { getSmoldotExtensionProviders } from "@substrate/smoldot-discovery"; +import { createClient } from "polkadot-api"; +import { getSmProvider } from "polkadot-api/sm-provider"; +import type { Client } from "polkadot-api/smoldot"; +import { startFromWorker } from "polkadot-api/smoldot/from-worker"; +import type { JsonRpcProvider } from "polkadot-api/ws-provider/web"; + +const getProviderSymbol = Symbol(); + +export type LightClientProvider = { + [getProviderSymbol]: () => Promise; +}; + +type AddChainOptions = + | { 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", + }), + ), + ); + + return { + addRelayChain( + options: AddChainOptions, + ) { + const getChainSpec = lazy(() => + "chainSpec" in options + ? Promise.resolve(options.chainSpec) + : wellknownChains[options.id][0]().then((chain) => chain.chainSpec), + ); + + const getRelayChain = lazy(() => { + const smoldot = getSmoldot(); + + return smoldot instanceof Promise + ? smoldot.then(async (smoldot) => + smoldot.addChain(await getChainSpec()), + ) + : getChainSpec().then((chainSpec) => smoldot.addChain({ chainSpec })); + }); + + return addLightClientProvider({ + async [getProviderSymbol]() { + return getSmProvider(await getRelayChain()); + }, + + addParachain< + TParachainId extends + keyof (typeof wellknownChains)[TRelayChainId][1] extends never + ? WellknownParachainId + : keyof (typeof wellknownChains)[TRelayChainId][1], + >(options: AddChainOptions) { + return addLightClientProvider({ + async [getProviderSymbol]() { + const chainSpecPromise = + "chainSpec" in options + ? Promise.resolve(options.chainSpec) + : // @ts-expect-error TODO: fix this + Object.fromEntries( + Object.values(wellknownChains).flatMap((relayChain) => + Object.entries(relayChain[1]), + ), + ) + [options.id]() + .then((chain) => chain.chainSpec); + + const parachainPromise = Promise.all([ + getRelayChain(), + chainSpecPromise, + ]).then(([relayChain, chainSpec]) => + "addChain" in relayChain + ? relayChain.addChain(chainSpec) + : (getSmoldot() as Client).addChain({ + chainSpec, + potentialRelayChains: [relayChain], + }), + ); + + return getSmProvider(await parachainPromise); + }, + }); + }, + }); + }, + }; +} + +const lightClientProviders = new WeakSet(); + +function addLightClientProvider(provider: T) { + lightClientProviders.add(provider); + return provider; +} + +export function isLightClientProvider( + value: unknown, +): value is LightClientProvider { + return lightClientProviders.has(value as LightClientProvider); +} + +export async function createClientFromLightClientProvider( + provider: LightClientProvider, +) { + return createClient(await provider[getProviderSymbol]()); +} diff --git a/packages/core/src/providers/light-client/wellknown-chains.ts b/packages/core/src/providers/light-client/wellknown-chains.ts new file mode 100644 index 00000000..090324e8 --- /dev/null +++ b/packages/core/src/providers/light-client/wellknown-chains.ts @@ -0,0 +1,46 @@ +export const wellknownChains = { + polkadot: [ + () => import("polkadot-api/chains/polkadot"), + { + polkadot_asset_hub: () => + import("polkadot-api/chains/polkadot_asset_hub"), + polkadot_bridge_hub: () => + import("polkadot-api/chains/polkadot_bridge_hub"), + polkadot_collectives: () => + import("polkadot-api/chains/polkadot_collectives"), + polkadot_people: () => import("polkadot-api/chains/polkadot_people"), + }, + ], + kusama: [ + () => import("polkadot-api/chains/ksmcc3"), + { + kusama_asset_hub: () => import("polkadot-api/chains/ksmcc3_asset_hub"), + kusama_bridge_hub: () => import("polkadot-api/chains/ksmcc3_bridge_hub"), + kusama_encointer: () => import("polkadot-api/chains/ksmcc3_encointer"), + kusama_people: () => import("polkadot-api/chains/ksmcc3_people"), + }, + ], + paseo: [ + () => import("polkadot-api/chains/paseo"), + { paseo_asset_hub: () => import("polkadot-api/chains/paseo_asset_hub") }, + ], + westend: [ + () => import("polkadot-api/chains/westend2"), + { + westend_asset_hub: () => import("polkadot-api/chains/westend2_asset_hub"), + westend_bridge_hub: () => + import("polkadot-api/chains/westend2_bridge_hub"), + westend_collectives: () => + import("polkadot-api/chains/westend2_collectives"), + westend_people: () => import("polkadot-api/chains/westend2_people"), + }, + ], +} as const; + +export type WellknownRelayChainId = keyof typeof wellknownChains; + +type KeysOfUnion = T extends T ? keyof T : never; + +export type WellknownParachainId = KeysOfUnion< + (typeof wellknownChains)[WellknownRelayChainId][1] +>; diff --git a/packages/core/src/utils/lazy.test.ts b/packages/core/src/utils/lazy.test.ts new file mode 100644 index 00000000..8f2797a1 --- /dev/null +++ b/packages/core/src/utils/lazy.test.ts @@ -0,0 +1,8 @@ +import { lazy } from "./lazy"; +import { expect, it } from "vitest"; + +it("only init value once", () => { + const getValue = lazy(() => Symbol()); + + expect(getValue()).toBe(getValue()); +}); diff --git a/packages/core/src/utils/lazy.ts b/packages/core/src/utils/lazy.ts new file mode 100644 index 00000000..ca83eb6f --- /dev/null +++ b/packages/core/src/utils/lazy.ts @@ -0,0 +1,14 @@ +const empty = Symbol(); + +export function lazy(get: () => T) { + let value: T | typeof empty = empty; + + return () => { + if (value !== empty) { + return value; + } + + value = get(); + return value; + }; +} diff --git a/yarn.lock b/yarn.lock index 9157c724..5e47380a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4771,6 +4771,7 @@ __metadata: dependencies: "@reactive-dot/eslint-config": "workspace:^" "@reactive-dot/utils": "workspace:^" + "@substrate/smoldot-discovery": "npm:^2.0.1" "@tsconfig/recommended": "npm:^1.0.8" "@tsconfig/strictest": "npm:^2.0.5" eslint: "npm:^9.16.0" @@ -5455,6 +5456,22 @@ __metadata: languageName: node linkType: hard +"@substrate/discovery@npm:^0.2.1": + version: 0.2.1 + resolution: "@substrate/discovery@npm:0.2.1" + checksum: 10c0/91b101d7cde80a29dfb38fb4b22d92559d1f49f1c6141896f715dab1169f9ee03ce6e8e493c01705cad73d4d82f07b5e6c26d7e5bfea3f9912e2cb7b070b7252 + languageName: node + linkType: hard + +"@substrate/smoldot-discovery@npm:^2.0.1": + version: 2.0.1 + resolution: "@substrate/smoldot-discovery@npm:2.0.1" + dependencies: + "@substrate/discovery": "npm:^0.2.1" + checksum: 10c0/81ef9af7d7398b7b53a28487054636d26b65ae3804c6d3465e488248629d94e9c9ded09a0f5c2a3b72ff6feef4efb8409df8c94b79b2adb0028360e69c7b71da + languageName: node + linkType: hard + "@svgr/babel-plugin-add-jsx-attribute@npm:8.0.0": version: 8.0.0 resolution: "@svgr/babel-plugin-add-jsx-attribute@npm:8.0.0"