(
+ initialHashParams.get("recipient") ?? null,
+)
+recipient$.subscribe((recipient) => setHashParams({ recipient }))
+const RecipientPicker = () => {
+ const recipient = useStateObservable(recipient$)
+
+ return (
+
+ )
+}
+
+export const setupConfig$ = state(
+ combineLatest([
+ origin$,
+ selectedAccount$,
+ selectedAsset$,
+ selectedDest$,
+ amount$,
+ recipient$,
+ ]).pipe(
+ map(([origin, account, asset, dest, amount, recipient]) => {
+ if (
+ !origin ||
+ !account ||
+ !asset ||
+ !dest ||
+ amount == null ||
+ !recipient
+ )
+ return null
+
+ return { origin, account, asset, dest, amount, recipient }
+ }),
+ ),
+ null,
+)
+export const paraspellBuilder$ = setupConfig$.pipeState(
+ withLatestFrom(client$),
+ map(([v, client]) =>
+ v
+ ? Builder({
+ abstractDecimals: false,
+ apiOverrides: {
+ [v.origin]: client,
+ },
+ })
+ .from(v.origin)
+ .to(v.dest)
+ .currency({ location: v.asset.location, amount: v.amount })
+ .recipient(v.recipient)
+ .sender(v.account.address)
+ : null,
+ ),
+)
+
+const SetupCard: FC<
+ PropsWithChildren<{
+ title: string
+ icon?: ReactNode
+ }>
+> = ({ title, icon, children }) => (
+
+
+ {icon ? {icon} : null}
+ {title}
+
+ {children}
+
+)
+
+const KeyValue: FC<{ label: string; value: ReactNode }> = ({
+ label,
+ value,
+}) => (
+
+ {label}
+ {value}
+
+)
diff --git a/src/pages/Teleport/Submit.tsx b/src/pages/Teleport/Submit.tsx
new file mode 100644
index 0000000..579252e
--- /dev/null
+++ b/src/pages/Teleport/Submit.tsx
@@ -0,0 +1,372 @@
+import { Link } from "@/hashParams"
+import {
+ TDryRunChainResult,
+ TDryRunResult,
+ TPapiTransaction,
+} from "@paraspell/sdk"
+import { formatToken } from "@polkadot-api/react-components"
+import { Button } from "@polkahub/ui-components"
+import { state, useStateObservable, withDefault } from "@react-rxjs/core"
+import { createSignal } from "@react-rxjs/utils"
+import {
+ CheckCircle2,
+ CircleAlert,
+ ExternalLink,
+ Loader2,
+ LockKeyhole,
+ Play,
+ TriangleAlert,
+ XCircle,
+} from "lucide-react"
+import { PolkadotClient } from "polkadot-api"
+import { jsonSerialize, toHex } from "polkadot-api/utils"
+import { useSelectedAccount } from "polkahub"
+import { FC, ReactNode } from "react"
+import {
+ catchError,
+ filter,
+ from,
+ map,
+ startWith,
+ switchMap,
+ withLatestFrom,
+} from "rxjs"
+import { trackTx } from "../Extrinsics/ExtrinsicsWorkspaceEntry"
+import { routeInfo$ } from "./RoutePreview"
+import { paraspellBuilder$, setupConfig$ } from "./Setup"
+
+export const Submit = () => {
+ return (
+
+
+
Validation & submit
+
+
+
+
+
+
+
+ )
+}
+
+const Validation = () => {
+ const setupConfig = useStateObservable(setupConfig$)
+ const routeInfo = useStateObservable(routeInfo$)
+
+ const formatChecks = setupConfig ? !!routeInfo : null
+ const balanceFeeChecks =
+ routeInfo && routeInfo !== "loading"
+ ? routeInfo.origin.xcmFee.sufficient &&
+ (typeof routeInfo.destination.receivedCurrency.receivedAmount ===
+ "bigint"
+ ? routeInfo.destination.receivedCurrency.receivedAmount > 0n
+ : null)
+ : null
+
+ return (
+
+ )
+}
+
+const [dryRun$, dryRun] = createSignal()
+const dryRunResult$ = state(
+ dryRun$.pipe(
+ withLatestFrom(paraspellBuilder$),
+ map(([, b]) => b),
+ filter((v) => v != null),
+ switchMap((builder) =>
+ from(builder.dryRun()).pipe(
+ map((value) => ({ type: "success" as const, value })),
+ startWith({ type: "loading" as const }),
+ catchError((ex: any) => [
+ { type: "error" as const, value: ex.message },
+ ]),
+ ),
+ ),
+ ),
+ null,
+)
+const DryRun = () => {
+ const builder = useStateObservable(paraspellBuilder$)
+ const dryRunResult = useStateObservable(dryRunResult$)
+
+ return (
+
+
+
Dry run
+
+
+
+ {!dryRunResult ? (
+
+ Not run
+
+ ) : dryRunResult.type === "loading" ? (
+
+
+ Running simulation
+
+ ) : dryRunResult.type === "success" ? (
+
+ ) : (
+
+ )}
+
+
+ )
+}
+
+const DryRunError: FC<{ message: string }> = ({ message }) => (
+
+
+
+
+
Dry run failed
+
+ {message || "Unknown error"}
+
+
+
+
+)
+
+const DryRunResult: FC<{ result: TDryRunResult }> = ({ result }) => (
+
+
+
+
+ {result.hops.map((hop, i) => (
+
+
+
+ ))}
+ {result.destination ? (
+
+
+
+ ) : null}
+
+)
+const DryRunChainResult: FC<{ result: TDryRunChainResult }> = ({ result }) => {
+ if (!result.success) {
+ return (
+
+
+
+ {result.failureReason}
+
+ {result.failureSubReason ?
{result.failureSubReason}
: null}
+
+ )
+ }
+ return (
+
+
+
+ {result.destParaId != null ? (
+
+ ) : null}
+ {result.forwardedXcms &&
+ (!Array.isArray(result.forwardedXcms) ||
+ result.forwardedXcms.length > 0) ? (
+
+
+ Forwarded XCM
+
+
+ {JSON.stringify(result.forwardedXcms, jsonSerialize, 2)}
+
+
+ ) : null}
+
+ )
+}
+
+const resultingTransactions$ = paraspellBuilder$.pipeState(
+ switchMap(
+ (builder) =>
+ builder?.buildAll().catch((ex) => {
+ console.error(ex)
+ return null
+ }) ?? [null],
+ ),
+ switchMap(async (transactions) => {
+ if (!transactions) return null
+
+ const encodedDatas = await Promise.all(
+ transactions.map(({ tx }) => tx.getEncodedData()),
+ )
+
+ return transactions.map(({ tx, api, chain }, i) => ({
+ tx,
+ api,
+ chain,
+ encodedData: encodedDatas[i],
+ }))
+ }),
+ withDefault(null),
+)
+
+const Export = () => {
+ const resultTransactions = useStateObservable(resultingTransactions$)
+
+ return (
+
+
+ {resultTransactions ? (
+ resultTransactions.map((result, i) => (
+ 1 ? i + 1 : null}
+ />
+ ))
+ ) : (
+
+ )}
+
+ )
+}
+
+const DevelopmentDisclaimer = () => (
+
+
+
+
This feature is under development
+
+ Verify the generated call data before signing.
+
+
+
+)
+
+const ExportTx: FC<{
+ tx: TPapiTransaction
+ encodedData: Uint8Array
+ api?: PolkadotClient | null
+ number?: number | null
+}> = ({ tx, encodedData, api, number }) => {
+ const [account] = useSelectedAccount()
+
+ const submit = async () => {
+ if (!account?.signer) return
+ const signed = await tx.sign(account.signer)
+ trackTx(signed, tx.decodedCall, account)
+ }
+
+ return (
+
+ {number != null ?
#{number}
: null}
+
+ {api ? null : (
+
+
+ Open in extrinsics
+
+ )}
+
+ )
+}
+
+const SectionTitle: FC<{ children: ReactNode }> = ({ children }) => (
+
+ {children}
+
+)
+
+const ValidationRow: FC<{ label: string; value: boolean | null }> = ({
+ label,
+ value,
+}) => (
+
+ {label}
+
+
+)
+
+const StatusBadge: FC<{ value: boolean | null }> = ({ value }) => {
+ if (value == null) {
+ return (
+
+
+ Not run
+
+ )
+ }
+
+ return value ? (
+
+
+ Passed
+
+ ) : (
+
+
+ Failed
+
+ )
+}
+
+const DryRunSection: FC<{
+ title: string
+ children: ReactNode
+}> = ({ title, children }) => (
+
+)
+
+const DetailRow: FC<{
+ label: string
+ value: ReactNode
+}> = ({ label, value }) => (
+
+ {label}
+
+ {value}
+
+
+)
diff --git a/src/pages/Teleport/Teleport.tsx b/src/pages/Teleport/Teleport.tsx
new file mode 100644
index 0000000..1b47667
--- /dev/null
+++ b/src/pages/Teleport/Teleport.tsx
@@ -0,0 +1,63 @@
+import { withSubscribe } from "@/components/withSuspense"
+import { useStateObservable } from "@react-rxjs/core"
+import { AlertTriangle } from "lucide-react"
+import { CenteredScrollContainer } from "../AppShell"
+import { RoutePreview } from "./RoutePreview"
+import { origin$, Setup } from "./Setup"
+import { Submit } from "./Submit"
+
+const Teleport = withSubscribe(() => {
+ const origin = useStateObservable(origin$)
+
+ if (!origin) {
+ // TODO explore custom chains
+ return (
+
+
+
+
+
+
Chain not supported
+
+ Teleport is currently available only for chains supported by
+ Paraspell.
+
+
+
+
+
+ )
+ }
+
+ return (
+
+
+
+ )
+})
+export default Teleport
diff --git a/src/pages/Teleport/genesisToParaspell.ts b/src/pages/Teleport/genesisToParaspell.ts
new file mode 100644
index 0000000..074de82
--- /dev/null
+++ b/src/pages/Teleport/genesisToParaspell.ts
@@ -0,0 +1,57 @@
+import { TChain } from "@paraspell/sdk"
+
+export const genesisHashToParaspell: Record = {
+ "0xfc41b9bd": "Acala",
+ "0xe358eb1d": "Ajuna",
+ "0x48239ef6": "AssetHubKusama",
+ "0xd6eec261": "AssetHubPaseo",
+ "0x68d56f15": "AssetHubPolkadot",
+ "0x67f97233": "AssetHubWestend",
+ "0x9eb76c51": "Astar",
+ "0xa85cfb9b": "Basilisk",
+ "0x9f28c6a6": "BifrostKusama",
+ "0xec39b15e": "BifrostPaseo",
+ "0x262e1b2a": "BifrostPolkadot",
+ "0x00dcb981": "BridgeHubKusama",
+ "0xcc624979": "BridgeHubPaseo",
+ "0xdcf691b5": "BridgeHubPolkadot",
+ "0x0441383e": "BridgeHubWestend",
+ "0xb3db4142": "Centrifuge",
+ "0x46ee89aa": "Collectives",
+ "0x713daf19": "CollectivesWestend",
+ "0x638cd2b9": "CoretimeKusama",
+ "0xc806038c": "CoretimePaseo",
+ "0xefb56e30": "CoretimePolkadot",
+ "0xf938510e": "CoretimeWestend",
+ "0x4319cc49": "Crust",
+ "0xd4c0c08c": "CrustShadow",
+ "0xf0b8924b": "Darwinia",
+ "0x7dd99936": "Encointer",
+ "0x5a51e04b": "EnergyWebX",
+ "0x2fc8bb6e": "Heima",
+ "0x40d175ca": "HeimaPaseo",
+ "0xafdc188f": "Hydration",
+ "0x5f52a76d": "HydrationPaseo",
+ "0xbf88efe7": "Interlay",
+ "0xbb9233e2": "Jamton",
+ "0xbaf5aabe": "Karura",
+ "0x9af9a64e": "Kintsugi",
+ "0xb0a8d493": "Kusama",
+ "0xfe58ea77": "Moonbeam",
+ "0x401a1f9d": "Moonriver",
+ "0xe7e09623": "NeuroWeb",
+ "0xf2b8faef": "NeuroWebPaseo",
+ "0x77afd619": "Paseo",
+ "0x5d3c2986": "Pendulum",
+ "0xafb18a62": "Penpal",
+ "0xc1af4cb4": "PeopleKusama",
+ "0xe6c30d6e": "PeoplePaseo",
+ "0x67fa177a": "PeoplePolkadot",
+ "0x1eb6fb0b": "PeopleWestend",
+ "0x91b171bb": "Polkadot",
+ "0x29f4371d": "RobonomicsPolkadot",
+ "0xf1cf9022": "Shiden",
+ "0x84322d9c": "Unique",
+ "0xe143f238": "Westend",
+ "0xb2985e77": "Xode",
+}
diff --git a/src/state/chains/chain.state.ts b/src/state/chains/chain.state.ts
index e7571ce..c64ea15 100644
--- a/src/state/chains/chain.state.ts
+++ b/src/state/chains/chain.state.ts
@@ -210,6 +210,7 @@ export const chainClient$ = state(
getMetadata: (id) => firstValueFrom(getMetadata(id)),
setMetadata,
})
+
const chainHead: ChainHead$ = (client as any).___INTERNAL_DO_NOT_USE
return concat(
i === 0 ? EMPTY : of(SUSPENSE),