diff --git a/src/codec-components/EditCodec/CAccountId.tsx b/src/codec-components/EditCodec/CAccountId.tsx index a71880e..d8b17b4 100644 --- a/src/codec-components/EditCodec/CAccountId.tsx +++ b/src/codec-components/EditCodec/CAccountId.tsx @@ -17,8 +17,8 @@ import { accountDetail$, accounts$, getPublicKey, -} from "@/extension-accounts.state" -import { identity$, isVerified } from "@/identity.state" +} from "@/state/extension-accounts.state" +import { identity$, isVerified } from "@/state/identity.state" import { cn } from "@/utils/cn" import { EditAccountId, NOTIN } from "@polkadot-api/react-builder" import { getSs58AddressInfo } from "@polkadot-api/substrate-bindings" diff --git a/src/components/AccountIdDisplay.tsx b/src/components/AccountIdDisplay.tsx index 35a0ce1..bf665dd 100644 --- a/src/components/AccountIdDisplay.tsx +++ b/src/components/AccountIdDisplay.tsx @@ -1,6 +1,6 @@ import { PolkadotIdenticon } from "@/components/PolkadotIdenticon" -import { accountDetail$, getPublicKey } from "@/extension-accounts.state" -import { identity$, isVerified } from "@/identity.state" +import { accountDetail$, getPublicKey } from "@/state/extension-accounts.state" +import { identity$, isVerified } from "@/state/identity.state" import { useStateObservable } from "@react-rxjs/core" import { CheckCircle } from "lucide-react" import { SS58String } from "polkadot-api" diff --git a/src/pages/Extrinsics/SubmitTx/AccountProvider.tsx b/src/pages/Extrinsics/SubmitTx/AccountProvider.tsx index bf698f3..142f47a 100644 --- a/src/pages/Extrinsics/SubmitTx/AccountProvider.tsx +++ b/src/pages/Extrinsics/SubmitTx/AccountProvider.tsx @@ -13,11 +13,11 @@ import { accountsByExtension$, extensionAccounts$, selectedExtensions$, -} from "@/extension-accounts.state" +} from "@/state/extension-accounts.state" import { walletConnectAccounts$, walletConnectStatus$, -} from "@/walletconnect.state" +} from "@/state/walletconnect.state" import { state, useStateObservable } from "@react-rxjs/core" import { createSignal } from "@react-rxjs/utils" import { InjectedExtension } from "polkadot-api/pjs-signer" diff --git a/src/pages/Extrinsics/SubmitTx/ExtensionProvider.tsx b/src/pages/Extrinsics/SubmitTx/ExtensionProvider.tsx index c271687..a0c6b7b 100644 --- a/src/pages/Extrinsics/SubmitTx/ExtensionProvider.tsx +++ b/src/pages/Extrinsics/SubmitTx/ExtensionProvider.tsx @@ -5,11 +5,11 @@ import { availableExtensions$, onToggleExtension, selectedExtensions$, -} from "@/extension-accounts.state" +} from "@/state/extension-accounts.state" import { toggleWalletConnect, walletConnectStatus$, -} from "@/walletconnect.state" +} from "@/state/walletconnect.state" import { useStateObservable } from "@react-rxjs/core" export const ExtensionProvider: React.FC = () => { diff --git a/src/pages/Network/Network.tsx b/src/pages/Network/Network.tsx index ce25148..01edba2 100644 --- a/src/pages/Network/Network.tsx +++ b/src/pages/Network/Network.tsx @@ -1,13 +1,3 @@ -import { Check, ChevronDown } from "lucide-react" -import { FC, useEffect, useRef, useState } from "react" - -import { - Network, - networkCategories, - onChangeChain, - SelectedChain, - selectedChain$, -} from "@/state/chains/chain.state" import { Accordion, AccordionContent, @@ -33,8 +23,17 @@ import { import { Label } from "@/components/ui/label" import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group" import { ScrollArea } from "@/components/ui/scroll-area" +import { + Network, + networkCategories, + onChangeChain, + SelectedChain, + selectedChain$, +} from "@/state/chains/chain.state" import { useStateObservable } from "@react-rxjs/core" import { useCommandState } from "cmdk" +import { Check, ChevronDown } from "lucide-react" +import { FC, useEffect, useRef, useState } from "react" const isValidUri = (input: string): boolean => { try { diff --git a/src/state/chains/chain.state.ts b/src/state/chains/chain.state.ts index 7c5d142..b5e82a1 100644 --- a/src/state/chains/chain.state.ts +++ b/src/state/chains/chain.state.ts @@ -1,21 +1,11 @@ import { getDynamicBuilder, getLookupFn } from "@polkadot-api/metadata-builders" import { getObservableClient } from "@polkadot-api/observable-client" import { decAnyMetadata, HexString } from "@polkadot-api/substrate-bindings" -import { - createClient as createSubstrateClient, - JsonRpcProvider, -} from "@polkadot-api/substrate-client" +import { createClient as createSubstrateClient } from "@polkadot-api/substrate-client" import { toHex } from "@polkadot-api/utils" import { sinkSuspense, state, SUSPENSE } from "@react-rxjs/core" import { createSignal } from "@react-rxjs/utils" import { createClient } from "polkadot-api" -import { withLogsRecorder } from "polkadot-api/logs-provider" -import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat" -import { getSmProvider } from "polkadot-api/sm-provider" -import { Chain } from "polkadot-api/smoldot" -import { startFromWorker } from "polkadot-api/smoldot/from-worker" -import SmWorker from "polkadot-api/smoldot/worker?worker" -import { getWsProvider } from "polkadot-api/ws-provider/web" import { concat, EMPTY, @@ -23,155 +13,49 @@ import { finalize, map, NEVER, - Observable, of, startWith, switchMap, tap, } from "rxjs" -import ksmRawNetworks from "./networks/kusama.json" -import paseoRawNetworks from "./networks/paseo.json" -import polkadotRawNetworks from "./networks/polkadot.json" -import westendRawNetworks from "./networks/westend.json" +import { defaultNetwork, Network } from "./networks" +import { + createSmoldotSource, + getSmoldotProvider, + SmoldotSource, +} from "./smoldot" +import { + createWebsocketSource, + getWebsocketProvider, + WebsocketSource, +} from "./websocket" -const [dotChainSpec, ksmChainSpec, paseoChainSpec, westendChainSpec] = [ - import("polkadot-api/chains/polkadot"), - import("polkadot-api/chains/ksmcc3"), - import("polkadot-api/chains/paseo"), - import("polkadot-api/chains/westend2"), -].map((x) => x.then((y) => y.chainSpec)) -const smoldot = startFromWorker(new SmWorker(), { - logCallback: (level, target, message) => { - console.debug("smoldot[%s(%s)] %s", target, level, message) - }, -}) -const relayChains: Record< - string, - { chainSpec: Promise; chain: Promise | null } -> = { - polkadot: { chainSpec: dotChainSpec, chain: null }, - kusama: { chainSpec: ksmChainSpec, chain: null }, - paseo: { chainSpec: paseoChainSpec, chain: null }, - westend: { chainSpec: westendChainSpec, chain: null }, -} -const getRelayChain = async (name: string) => { - if (!relayChains[name].chain) { - relayChains[name].chain = smoldot.addChain({ - chainSpec: await relayChains[name].chainSpec, - }) - } - return relayChains[name].chain -} +export type ChainSource = WebsocketSource | SmoldotSource -export type ChainSource = { id: string } & ( - | { - type: "chainSpec" - value: { - chainSpec: string - relayChain?: string - } - } - | { - type: "websocket" - value: string - } -) - -export type Network = { - id: string - display: string - endpoints: Record - lightclient: boolean - relayChain?: string -} - -export type NetworkCategory = { - name: string - networks: Network[] -} export type SelectedChain = { network: Network endpoint: string } - -const [Polkadot, Kusama, Paseo, Westend] = ( - [ - polkadotRawNetworks, - ksmRawNetworks, - paseoRawNetworks, - westendRawNetworks, - ] as const -).map((n): Network[] => - n.map((x) => ({ - endpoints: x.rpcs as any, - lightclient: x.hasChainSpecs, - id: x.id, - display: x.display, - relayChain: x.relayChainInfo?.id, - })), -) - -const networks = { - Polkadot, - Kusama, - Paseo, - Westend, - Development: [ - { - id: "localhost", - display: "Localhost", - lightclient: false, - endpoints: { - "Local (ws://127.0.0.1:9944)": "ws://127.0.0.1:9944", - }, - } as Network, - ], -} - -export const networkCategories: NetworkCategory[] = Object.entries( - networks, -).map(([name, networks]) => ({ name, networks })) +export const getChainSource = ({ + endpoint, + network: { id, relayChain }, +}: SelectedChain) => + endpoint === "light-client" + ? createSmoldotSource(id, relayChain) + : createWebsocketSource(id, endpoint) +export const getProvider = (source: ChainSource) => + source.type === "websocket" + ? getWebsocketProvider(source) + : getSmoldotProvider(source) export const [selectedChainChanged$, onChangeChain] = createSignal() export const selectedChain$ = state(selectedChainChanged$, { - network: Polkadot[0], + network: defaultNetwork, endpoint: "light-client", }) -selectedChain$.subscribe() -const selectedSource$ = selectedChain$.pipe( - switchMap( - ({ endpoint, network }): Observable | Promise => { - const { id } = network - if (endpoint !== "light-client") - return of({ - id, - type: "websocket", - value: endpoint, - }) - if (id in relayChains) { - return relayChains[id].chainSpec.then((chainSpec) => ({ - id, - type: "chainSpec", - value: { chainSpec }, - })) - } - return import(`./chainspecs/${id}.ts`).then(({ chainSpec }) => { - const parsed = JSON.parse(chainSpec) - return { - id, - type: "chainSpec", - value: { - chainSpec, - relayChain: - network.relayChain || parsed.relayChain || parsed.relay_chain, - }, - } - }) - }, - ), -) +const selectedSource$ = selectedChain$.pipe(switchMap(getChainSource)) export const chainClient$ = state( selectedSource$.pipe( @@ -260,24 +144,4 @@ export const dynamicBuilder$ = runtimeCtx$.pipeState( map((ctx) => ctx.dynamicBuilder), ) -export function getProvider(source: ChainSource): JsonRpcProvider { - if (source.type === "websocket") { - return withPolkadotSdkCompat(getWsProvider(source.value)) - } - - const chain = source.value.relayChain - ? getRelayChain(source.value.relayChain).then((chain) => - smoldot.addChain({ - chainSpec: source.value.chainSpec, - potentialRelayChains: [chain], - }), - ) - : smoldot.addChain({ - chainSpec: source.value.chainSpec, - }) - - return withLogsRecorder( - (v) => v.includes("initialized") && console.debug(v), - getSmProvider(chain), - ) -} +export { networkCategories, type Network } from "./networks" diff --git a/src/state/chains/networks/index.ts b/src/state/chains/networks/index.ts new file mode 100644 index 0000000..b4c8e85 --- /dev/null +++ b/src/state/chains/networks/index.ts @@ -0,0 +1,57 @@ +import ksmRawNetworks from "./kusama.json" +import paseoRawNetworks from "./paseo.json" +import polkadotRawNetworks from "./polkadot.json" +import westendRawNetworks from "./westend.json" + +export type Network = { + id: string + display: string + endpoints: Record + lightclient: boolean + relayChain?: string +} + +export type NetworkCategory = { + name: string + networks: Network[] +} + +const [Polkadot, Kusama, Paseo, Westend] = ( + [ + polkadotRawNetworks, + ksmRawNetworks, + paseoRawNetworks, + westendRawNetworks, + ] as const +).map((n): Network[] => + n.map((x) => ({ + endpoints: x.rpcs as any, + lightclient: x.hasChainSpecs, + id: x.id, + display: x.display, + relayChain: x.relayChainInfo?.id, + })), +) + +const networks = { + Polkadot, + Kusama, + Paseo, + Westend, + Development: [ + { + id: "localhost", + display: "Localhost", + lightclient: false, + endpoints: { + "Local (ws://127.0.0.1:9944)": "ws://127.0.0.1:9944", + }, + } as Network, + ], +} + +export const networkCategories: NetworkCategory[] = Object.entries( + networks, +).map(([name, networks]) => ({ name, networks })) + +export const defaultNetwork = Polkadot[0] diff --git a/src/state/chains/smoldot.ts b/src/state/chains/smoldot.ts new file mode 100644 index 0000000..e2b49ba --- /dev/null +++ b/src/state/chains/smoldot.ts @@ -0,0 +1,86 @@ +import { JsonRpcProvider } from "@polkadot-api/substrate-client" +import { withLogsRecorder } from "polkadot-api/logs-provider" +import { getSmProvider } from "polkadot-api/sm-provider" +import { Chain } from "polkadot-api/smoldot" +import { startFromWorker } from "polkadot-api/smoldot/from-worker" +import SmWorker from "polkadot-api/smoldot/worker?worker" + +const [dotChainSpec, ksmChainSpec, paseoChainSpec, westendChainSpec] = [ + import("polkadot-api/chains/polkadot"), + import("polkadot-api/chains/ksmcc3"), + import("polkadot-api/chains/paseo"), + import("polkadot-api/chains/westend2"), +].map((x) => x.then((y) => y.chainSpec)) +const smoldot = startFromWorker(new SmWorker(), { + logCallback: (level, target, message) => { + console.debug("smoldot[%s(%s)] %s", target, level, message) + }, +}) +const relayChains: Record< + string, + { chainSpec: Promise; chain: Promise | null } +> = { + polkadot: { chainSpec: dotChainSpec, chain: null }, + kusama: { chainSpec: ksmChainSpec, chain: null }, + paseo: { chainSpec: paseoChainSpec, chain: null }, + westend: { chainSpec: westendChainSpec, chain: null }, +} +const getRelayChain = async (name: string) => { + if (!relayChains[name].chain) { + relayChains[name].chain = smoldot.addChain({ + chainSpec: await relayChains[name].chainSpec, + }) + } + return relayChains[name].chain +} + +export interface SmoldotSource { + type: "chainSpec" + id: string + value: { + chainSpec: string + relayChain?: string + } +} + +export function createSmoldotSource( + id: string, + relayChain?: string, +): Promise { + if (id in relayChains) { + return relayChains[id].chainSpec.then((chainSpec) => ({ + id, + type: "chainSpec", + value: { chainSpec }, + })) + } + return import(`./chainspecs/${id}.ts`).then(({ chainSpec }) => { + const parsed = JSON.parse(chainSpec) + return { + id, + type: "chainSpec", + value: { + chainSpec, + relayChain: relayChain || parsed.relayChain || parsed.relay_chain, + }, + } + }) +} + +export function getSmoldotProvider(source: SmoldotSource): JsonRpcProvider { + const chain = source.value.relayChain + ? getRelayChain(source.value.relayChain).then((chain) => + smoldot.addChain({ + chainSpec: source.value.chainSpec, + potentialRelayChains: [chain], + }), + ) + : smoldot.addChain({ + chainSpec: source.value.chainSpec, + }) + + return withLogsRecorder( + (v) => v.includes("initialized") && console.debug(v), + getSmProvider(chain), + ) +} diff --git a/src/state/chains/websocket.ts b/src/state/chains/websocket.ts new file mode 100644 index 0000000..f8462e5 --- /dev/null +++ b/src/state/chains/websocket.ts @@ -0,0 +1,20 @@ +import { JsonRpcProvider } from "@polkadot-api/substrate-client" +import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat" +import { getWsProvider } from "polkadot-api/ws-provider/web" + +export interface WebsocketSource { + type: "websocket" + id: string + endpoint: string +} + +export async function createWebsocketSource( + id: string, + endpoint: string, +): Promise { + return { type: "websocket", id, endpoint } +} + +export function getWebsocketProvider(source: WebsocketSource): JsonRpcProvider { + return withPolkadotSdkCompat(getWsProvider(source.endpoint)) +} diff --git a/src/state/identity.state.ts b/src/state/identity.state.ts index 337c33a..3b61e75 100644 --- a/src/state/identity.state.ts +++ b/src/state/identity.state.ts @@ -1,3 +1,4 @@ +import { localStorageSubject } from "@/utils/localStorageSubject" import { IdentityData, IdentityJudgement, @@ -5,10 +6,9 @@ import { } from "@polkadot-api/descriptors" import { state } from "@react-rxjs/core" import { Binary, createClient, SS58String } from "polkadot-api" -import { chainSpec } from "./chains/chainspecs/polkadot_people" import { catchError, map, of, tap } from "rxjs" import { getProvider } from "./chains/chain.state" -import { localStorageSubject } from "@/utils/localStorageSubject" +import { chainSpec } from "./chains/chainspecs/polkadot_people" export interface Identity { displayName: string diff --git a/src/state/walletconnect.state.ts b/src/state/walletconnect.state.ts index 584994e..0f59b2b 100644 --- a/src/state/walletconnect.state.ts +++ b/src/state/walletconnect.state.ts @@ -1,3 +1,4 @@ +import { localStorageSubject } from "@/utils/localStorageSubject" import { state, withDefault } from "@react-rxjs/core" import { createSignal } from "@react-rxjs/utils" import { WalletConnectModal } from "@walletconnect/modal" @@ -28,7 +29,6 @@ import { takeUntil, tap, } from "rxjs" -import { localStorageSubject } from "./utils/localStorageSubject" // https://docs.reown.com/advanced/multichain/polkadot/dapp-integration-guide const chains = [