From 6e124454cd1196ebb65a7da8c8cee30265412d33 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Tue, 10 Mar 2026 22:26:56 +0000 Subject: [PATCH 01/26] "UPGRADE: Upgrading 3 package(s)\n\nPackages:\n @galacticcouncil/xc-core@0.13.0-pr294-5865627\n @galacticcouncil/xc-cfg@0.17.0-pr294-5865627\n @galacticcouncil/xc-sdk@0.8.1-pr294-5865627" --- package.json | 6 +++--- yarn.lock | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index e86fa4878..4764d99f8 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "@galacticcouncil/descriptors": "^1.14.0", "@galacticcouncil/sdk-next": "^0.37.0", "@galacticcouncil/xc": "^0.4.0", - "@galacticcouncil/xc-cfg": "^0.15.0", - "@galacticcouncil/xc-core": "^0.12.0", + "@galacticcouncil/xc-cfg": "0.17.0-pr294-5865627", + "@galacticcouncil/xc-core": "0.13.0-pr294-5865627", "@galacticcouncil/xc-scan": "^0.3.0", - "@galacticcouncil/xc-sdk": "^0.6.1", + "@galacticcouncil/xc-sdk": "0.8.1-pr294-5865627", "big.js": "^6.2.2", "date-fns": "^4.1.0", "immer": "^10.0.3", diff --git a/yarn.lock b/yarn.lock index fe6680fa5..387f1ce10 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2089,17 +2089,17 @@ "@thi.ng/memoize" "^4.0.2" big.js "^6.2.1" -"@galacticcouncil/xc-cfg@^0.15.0": - version "0.15.0" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-cfg/-/xc-cfg-0.15.0.tgz#4ca92acf30bd9a062d05fceba336939b23c6b477" - integrity sha512-niIwNqgHIauMAOp2Q+jbZjdc24hmPRb6eSVLEybphnDPxsQ40cM+EfJLQP5s9yXA4H6VNhEgfpyEtdfl9+VSxg== +"@galacticcouncil/xc-cfg@0.17.0-pr294-5865627": + version "0.17.0-pr294-5865627" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-cfg/-/xc-cfg-0.17.0-pr294-5865627.tgz#e1fe0a735b469473c07259402ce27d61a8bb5f8a" + integrity sha512-m0GzYnlwmUTZ8btzCzkf5s8WftgOgHMs/qCNwaApVqwteX6YQrtM724etB0z9DKdwSFV9rV7y0Az6/uIARcbhg== dependencies: - "@galacticcouncil/xc-core" "^0.12.0" + "@galacticcouncil/xc-core" "0.13.0-pr294-5865627" -"@galacticcouncil/xc-core@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-core/-/xc-core-0.12.0.tgz#91e19726f2bdf5469d1e87874173ce726aca6d2a" - integrity sha512-aGkTkGpHDYlzLVg29i4IwhC0ntmZkpf73+WsjiaY3h/iSjGSwRNMU48gbKoNjvni8a7pSBKD2bWMOO6Uu2vU9A== +"@galacticcouncil/xc-core@0.13.0-pr294-5865627": + version "0.13.0-pr294-5865627" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-core/-/xc-core-0.13.0-pr294-5865627.tgz#3927787d3fcd1f0f23c72b6351011fc13f494257" + integrity sha512-U7KHG6lV+qyQwZNjupwAC6pA4nW5a2qfkG1W9PIp5raz2XL2FjKD6TjpGN7hY/dCkFW+iobjaUv3Bb88tHbi8A== dependencies: "@noble/hashes" "^1.6.1" "@wormhole-foundation/sdk-base" "3.2.0" @@ -2120,12 +2120,12 @@ resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-scan/-/xc-scan-0.3.0.tgz#662bb011c53f056c230d1d1057d31b94424758f7" integrity sha512-onH/zcGdA5JQx/tE7fcnhMUwosPnGflkJgcujgKeyuJPL15onvMPPKlDzvvNnUN8clfnefRtmEY9SNPo4md9uw== -"@galacticcouncil/xc-sdk@^0.6.1": - version "0.6.1" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.6.1.tgz#4a21761f5aed81175c35a1759584c082d1f956bc" - integrity sha512-q3ob67il96ozpiUus0zA3i54f0Ypw32TFffxjXND4XVyLfRSFpmbCAQDPRQ31i09dBI7698RWAJbzM4OSADjJg== +"@galacticcouncil/xc-sdk@0.8.1-pr294-5865627": + version "0.8.1-pr294-5865627" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.8.1-pr294-5865627.tgz#d52c71b1551d7ccc0a70d7e96371af05ef868b8e" + integrity sha512-4KrDyjso1RuunAkrMzTD8SPq0dsV7dI7alvazB49G+ZWd2iIwbs+s4bmHVKrpUzLiidBR9Jv1mUDajSNG9X5BA== dependencies: - "@galacticcouncil/xc-core" "^0.12.0" + "@galacticcouncil/xc-core" "0.13.0-pr294-5865627" "@galacticcouncil/xc@^0.4.0": version "0.4.0" From 50eae08f7262f9596c921468de3ab58e8b9072e3 Mon Sep 17 00:00:00 2001 From: mrq Date: Wed, 11 Mar 2026 15:56:29 +0100 Subject: [PATCH 02/26] base jumpa --- apps/main/src/api/xcm.ts | 51 +++++++++-- apps/main/src/i18n/locales/en/xcm.json | 5 ++ .../main/src/modules/xcm/transfer/XcmForm.tsx | 16 ++-- .../src/modules/xcm/transfer/XcmProvider.tsx | 84 ++++++++++++++++--- .../src/modules/xcm/transfer/XcmSummary.tsx | 11 ++- .../BridgeSelector/BridgeSelector.styled.ts | 31 +++++++ .../BridgeSelector/BridgeSelector.tsx | 63 ++++++++++++++ .../components/BridgeSelector/index.ts | 1 + .../transfer/hooks/useSubmitXcmTransfer.ts | 28 ++++++- .../modules/xcm/transfer/hooks/useXcmForm.ts | 1 + .../xcm/transfer/hooks/useXcmFormSchema.ts | 1 + .../xcm/transfer/hooks/useXcmProvider.ts | 4 +- .../transfer/hooks/useXcmTransferConfigs.ts | 21 ++++- .../xcm/transfer/utils/bridge-routes.ts | 65 ++++++++++++++ .../modules/xcm/transfer/utils/transfer.ts | 23 ++++- apps/main/src/states/transactions.ts | 6 +- package.json | 6 +- 17 files changed, 381 insertions(+), 36 deletions(-) create mode 100644 apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts create mode 100644 apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx create mode 100644 apps/main/src/modules/xcm/transfer/components/BridgeSelector/index.ts create mode 100644 apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts diff --git a/apps/main/src/api/xcm.ts b/apps/main/src/api/xcm.ts index 3c11d64dc..022b0e008 100644 --- a/apps/main/src/api/xcm.ts +++ b/apps/main/src/api/xcm.ts @@ -1,8 +1,10 @@ import { formatSourceChainAddress } from "@galacticcouncil/utils" import { createXcContext } from "@galacticcouncil/xc" import { chainsMap } from "@galacticcouncil/xc-cfg" -import { AnyChain, AssetAmount } from "@galacticcouncil/xc-core" +import { AnyChain, AssetAmount, ConfigBuilder } from "@galacticcouncil/xc-core" import { Transfer, TransferBuilder, Wallet } from "@galacticcouncil/xc-sdk" + +import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { keepPreviousData, queryOptions, @@ -135,6 +137,7 @@ export type XcmTransferArgs = { readonly destAddress: string readonly destAsset: string readonly destChain: string + readonly bridgeTag?: string } export const xcmTransferQuery = ( @@ -146,6 +149,7 @@ export const xcmTransferQuery = ( destAddress, destChain, destAsset, + bridgeTag, }: XcmTransferArgs, options?: UseQueryOptions, ) => { @@ -162,17 +166,48 @@ export const xcmTransferQuery = ( destAsset, srcChain, destChain, + bridgeTag, ], - queryFn: () => - TransferBuilder(wallet) + queryFn: async () => { + const builder = TransferBuilder(wallet) .withAsset(srcAsset) .withSource(srcChain) .withDestination(destChain) - .build({ - srcAddress: srcAddress, - dstAddress: destAddress, - dstAsset: destAsset, - }), + + if (bridgeTag) { + const selectedRoute = + builder.routes.find((r) => + (r.tags as string[] | undefined)?.includes(bridgeTag), + ) ?? + getSupplementalBridgeRoutes(srcChain, destChain, srcAsset).find( + (r) => (r.tags as string[] | undefined)?.includes(bridgeTag), + ) + + if (selectedRoute) { + const configs = ConfigBuilder(wallet.config) + .assets() + .asset(srcAsset) + .source(srcChain) + .destination(destChain) + .build(destAsset) + + return wallet.getTransferData( + { + origin: { chain: configs.origin.chain, route: selectedRoute }, + reverse: configs.reverse, + }, + srcAddress, + destAddress, + ) + } + } + + return builder.build({ + srcAddress: srcAddress, + dstAddress: destAddress, + dstAsset: destAsset, + }) + }, enabled: !!srcAddress && !!destAddress && diff --git a/apps/main/src/i18n/locales/en/xcm.json b/apps/main/src/i18n/locales/en/xcm.json index 896575382..7f3b84982 100644 --- a/apps/main/src/i18n/locales/en/xcm.json +++ b/apps/main/src/i18n/locales/en/xcm.json @@ -8,6 +8,11 @@ "approve.toast.success": "Approved {{ amount, number }} {{ symbol }} spending cap on {{ srcChain }}", "bridge.wormhole": "Wormhole", "bridge.snowbridge": "Snowbridge", + "bridge.instabridge": "Base Jumper", + "bridge.selector.label": "Via", + "bridge.provider.instabridge": "Base Jumper", + "bridge.provider.wormhole": "Wormhole", + "bridge.provider.snowbridge": "Snowbridge", "chainAssetSelect.button.selectAssetChain": "Select asset & chain", "chainAssetSelect.emptyState.noAssets": "No assets found", "chainAssetSelect.modal.title": "Chain & asset", diff --git a/apps/main/src/modules/xcm/transfer/XcmForm.tsx b/apps/main/src/modules/xcm/transfer/XcmForm.tsx index e3fda8a4e..c329b4bbe 100644 --- a/apps/main/src/modules/xcm/transfer/XcmForm.tsx +++ b/apps/main/src/modules/xcm/transfer/XcmForm.tsx @@ -21,6 +21,7 @@ import { removeOptimisticJourney, } from "@/modules/xcm/history/utils/optimistic" import { ChainAssetSelectModalSelectionChange } from "@/modules/xcm/transfer/components/ChainAssetSelect" +import { BridgeSelector } from "@/modules/xcm/transfer/components/BridgeSelector" import { ChainSwitch } from "@/modules/xcm/transfer/components/ChainSwitch" import { ConnectButton } from "@/modules/xcm/transfer/components/ConnectButton" import { @@ -51,6 +52,7 @@ export const XcmForm = () => { dryRunError, sourceChainAssetPairs, destChainAssetPairs, + availableBridgeRoutes, isLoading, isLoadingCall, isLoadingTransfer, @@ -278,6 +280,14 @@ export const XcmForm = () => { /> + {availableBridgeRoutes.length > 1 && ( + <> + + + + + + )} @@ -307,11 +317,7 @@ export const XcmForm = () => { - + ) diff --git a/apps/main/src/modules/xcm/transfer/XcmProvider.tsx b/apps/main/src/modules/xcm/transfer/XcmProvider.tsx index cd956317a..f1e736de1 100644 --- a/apps/main/src/modules/xcm/transfer/XcmProvider.tsx +++ b/apps/main/src/modules/xcm/transfer/XcmProvider.tsx @@ -26,10 +26,13 @@ import { isAccountValidOnChain, XCM_CHAINS, } from "@/modules/xcm/transfer/utils/chain" +import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { calculateTransferDestAmount, + getPrimaryBridgeTag, getTransferStatus, } from "@/modules/xcm/transfer/utils/transfer" +import { XcmTag } from "@/states/transactions" type XcmProviderProps = { children: React.ReactNode @@ -44,15 +47,23 @@ export const XcmProvider: React.FC = ({ children }) => { const configService = useCrossChainConfigService() - const [srcChain, srcAsset, destChain, destAsset, srcAmount, destAddress] = - form.watch([ - "srcChain", - "srcAsset", - "destChain", - "destAsset", - "srcAmount", - "destAddress", - ]) + const [ + srcChain, + srcAsset, + destChain, + destAsset, + srcAmount, + destAddress, + bridgeProvider, + ] = form.watch([ + "srcChain", + "srcAsset", + "destChain", + "destAsset", + "srcAmount", + "destAddress", + "bridgeProvider", + ]) const config = useMemo( () => ConfigBuilder(configService).assets(), @@ -96,10 +107,62 @@ export const XcmProvider: React.FC = ({ children }) => { .source(srcChain) .destination(chain) - return { chain, routes, assets: routes.map((r) => r.destination.asset) } + // Deduplicate assets - multiple bridge routes may share the same destination asset + const seenKeys = new Set() + const assets = routes + .map((r) => r.destination.asset) + .filter((a) => (seenKeys.has(a.key) ? false : seenKeys.add(a.key))) + + return { chain, routes, assets } }) }, [config, srcAsset, srcChain, configService]) + const availableBridgeRoutes = useMemo(() => { + if (!srcChain || !srcAsset || !destChain || !destAsset) return [] + const destPair = destChainAssetPairs.find( + (p) => p.chain.key === destChain.key, + ) + if (!destPair) return [] + + const configRoutes = destPair.routes.filter( + (r) => + r.destination.asset.key === destAsset.key && + getPrimaryBridgeTag(r) !== null, + ) + const existingTags = new Set(configRoutes.map((r) => getPrimaryBridgeTag(r))) + const supplemental = getSupplementalBridgeRoutes( + srcChain.key, + destChain.key, + srcAsset.key, + ).filter( + (r) => + r.destination.asset.key === destAsset.key && + !existingTags.has(getPrimaryBridgeTag(r)), + ) + return [...configRoutes, ...supplemental] + }, [srcChain, srcAsset, destChain, destAsset, destChainAssetPairs]) + + useEffect(() => { + if (availableBridgeRoutes.length <= 1) { + if (bridgeProvider !== null) { + form.setValue("bridgeProvider", null) + } + return + } + + const isCurrentValid = availableBridgeRoutes.some( + (r) => getPrimaryBridgeTag(r) === bridgeProvider, + ) + if (isCurrentValid) return + + const defaultRoute = + availableBridgeRoutes.find( + (r) => getPrimaryBridgeTag(r) === XcmTag.InstaBridge, + ) ?? availableBridgeRoutes[0] + if (!defaultRoute) return + form.setValue("bridgeProvider", getPrimaryBridgeTag(defaultRoute)) + }, [availableBridgeRoutes, bridgeProvider, form]) + useEffect(() => { const validRoutes = pipe( destChainAssetPairs, @@ -189,6 +252,7 @@ export const XcmProvider: React.FC = ({ children }) => { isConnectedAccountValid, sourceChainAssetPairs, destChainAssetPairs, + availableBridgeRoutes, alerts, transfer, call, diff --git a/apps/main/src/modules/xcm/transfer/XcmSummary.tsx b/apps/main/src/modules/xcm/transfer/XcmSummary.tsx index fb592d1b2..f0a6958c3 100644 --- a/apps/main/src/modules/xcm/transfer/XcmSummary.tsx +++ b/apps/main/src/modules/xcm/transfer/XcmSummary.tsx @@ -27,14 +27,21 @@ export const XcmSummary = () => { const { source, destination } = transfer || {} - const [srcAsset, destAsset, srcChain, destChain] = watch([ + const [srcAsset, destAsset, srcChain, destChain, bridgeProvider] = watch([ "srcAsset", "destAsset", "srcChain", "destChain", + "bridgeProvider", ]) - const config = useXcmTransferConfigs(srcAsset, srcChain, destChain, destAsset) + const config = useXcmTransferConfigs( + srcAsset, + srcChain, + destChain, + destAsset, + bridgeProvider, + ) const { origin } = config ?? {} const sourceFeeValue = (() => { diff --git a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts new file mode 100644 index 000000000..b30c42222 --- /dev/null +++ b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts @@ -0,0 +1,31 @@ +import { css } from "@emotion/react" +import styled from "@emotion/styled" + +export const SBridgeOption = styled.button<{ active: boolean }>( + ({ theme, active }) => css` + display: flex; + justify-content: space-between; + align-items: center; + gap: ${theme.space.base}; + + border: 1px solid ${theme.buttons.outlineDark.onOutline}; + border-radius: 8px; + + padding: 16px 12px; + + cursor: pointer; + + transition: ${theme.transitions.colors}; + + ${active + ? css` + background-color: ${theme.buttons.secondary.outline.fill}; + border-color: ${theme.buttons.secondary.outline.outline}; + ` + : css` + &:hover:not(:disabled) { + background-color: ${theme.buttons.outlineDark.rest}; + } + `} + `, +) diff --git a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx new file mode 100644 index 000000000..7b0512d65 --- /dev/null +++ b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx @@ -0,0 +1,63 @@ +import { Flex, Text } from "@galacticcouncil/ui/components" +import { getToken } from "@galacticcouncil/ui/utils" +import { AssetRoute } from "@galacticcouncil/xc-core" +import { useFormContext } from "react-hook-form" +import { useTranslation } from "react-i18next" + +import { XcmFormValues } from "@/modules/xcm/transfer/hooks/useXcmFormSchema" +import { getPrimaryBridgeTag } from "@/modules/xcm/transfer/utils/transfer" +import { XcmTag } from "@/states/transactions" + +import { SBridgeOption } from "./BridgeSelector.styled" + +const BRIDGE_TIME_ESTIMATES: Partial> = { + [XcmTag.InstaBridge]: "~42 sec", + [XcmTag.Wormhole]: "~18 min", + [XcmTag.Snowbridge]: "~25 min", +} + +type BridgeSelectorProps = { + routes: AssetRoute[] +} + +export const BridgeSelector: React.FC = ({ routes }) => { + const { t } = useTranslation(["xcm"]) + const { watch, setValue } = useFormContext() + const bridgeProvider = watch("bridgeProvider") + + const options = routes + .map((route) => { + const tag = getPrimaryBridgeTag(route) + if (!tag) return null + return { + id: tag, + label: t(`xcm:bridge.provider.${tag.toLowerCase()}`, tag), + time: BRIDGE_TIME_ESTIMATES[tag], + } + }) + .filter(Boolean) as { id: string; label: string; time?: string }[] + + if (options.length < 2) return null + + return ( + + {options.map((option) => ( + setValue("bridgeProvider", option.id)} + > + + {option.label} + + {option.time && ( + + {option.time} + + )} + + ))} + + ) +} diff --git a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/index.ts b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/index.ts new file mode 100644 index 000000000..0ace04a2f --- /dev/null +++ b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/index.ts @@ -0,0 +1 @@ +export { BridgeSelector } from "./BridgeSelector" diff --git a/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts b/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts index 8084b8834..f1c3be5d8 100644 --- a/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts +++ b/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts @@ -12,6 +12,7 @@ import { Binary } from "polkadot-api" import { useTranslation } from "react-i18next" import { useCrossChainConfigService } from "@/api/xcm" +import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { AnyPapiTx } from "@/modules/transactions/types" import { isEvmApproveCall, isEvmCall } from "@/modules/transactions/utils/xcm" import { useApprovalTrackingStore } from "@/modules/xcm/transfer/hooks/useApprovalTrackingStore" @@ -55,7 +56,14 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { return useMutation({ mutationFn: async ([values, transfer]: [XcmFormValues, Transfer]) => { - const { srcAmount, srcChain, destChain, srcAsset, destAsset } = values + const { + srcAmount, + srcChain, + destChain, + srcAsset, + destAsset, + bridgeProvider, + } = values if (!account) throw new Error("Account is required") if (!destChain) throw new Error("Destination chain is required") @@ -72,7 +80,7 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { destChain: destChain.name, } - const { build } = ConfigBuilder(configService) + const { routes, build } = ConfigBuilder(configService) .assets() .asset(srcAsset) .source(srcChain) @@ -80,6 +88,20 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { const { origin } = build(destAsset) + const selectedRoute = bridgeProvider + ? (routes.find((r) => + (r.tags as string[] | undefined)?.includes(bridgeProvider), + ) ?? + getSupplementalBridgeRoutes( + srcChain.key, + destChain.key, + srcAsset.key, + ).find((r) => + (r.tags as string[] | undefined)?.includes(bridgeProvider), + ) ?? + origin.route) + : origin.route + const call = await transfer.buildCall(srcAmount) const isApprove = isEvmApproveCall(call) @@ -120,7 +142,7 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { destination.fee.decimals, ), dstChainFeeSymbol: destination.fee.symbol, - tags: (origin.route.tags as XcmTags) || [], + tags: (selectedRoute.tags as XcmTags) || [], }, } } diff --git a/apps/main/src/modules/xcm/transfer/hooks/useXcmForm.ts b/apps/main/src/modules/xcm/transfer/hooks/useXcmForm.ts index bb2a35c46..282617fdd 100644 --- a/apps/main/src/modules/xcm/transfer/hooks/useXcmForm.ts +++ b/apps/main/src/modules/xcm/transfer/hooks/useXcmForm.ts @@ -32,6 +32,7 @@ export const useXcmForm = (transfer: Transfer | null) => { destAddress: defaults.destAddress ?? "", destAccount: defaults.destAccount ?? null, + bridgeProvider: null, }, }) diff --git a/apps/main/src/modules/xcm/transfer/hooks/useXcmFormSchema.ts b/apps/main/src/modules/xcm/transfer/hooks/useXcmFormSchema.ts index 597b1f0dd..45edd8da1 100644 --- a/apps/main/src/modules/xcm/transfer/hooks/useXcmFormSchema.ts +++ b/apps/main/src/modules/xcm/transfer/hooks/useXcmFormSchema.ts @@ -70,6 +70,7 @@ const createSchema = (transfer: Transfer | null) => { destAmount: z.string(), destAddress: required, destAccount: z.custom((val) => isObjectType(val)).nullable(), + bridgeProvider: z.string().nullable(), }) } diff --git a/apps/main/src/modules/xcm/transfer/hooks/useXcmProvider.ts b/apps/main/src/modules/xcm/transfer/hooks/useXcmProvider.ts index e0a8f23c6..7b6cbd4e7 100644 --- a/apps/main/src/modules/xcm/transfer/hooks/useXcmProvider.ts +++ b/apps/main/src/modules/xcm/transfer/hooks/useXcmProvider.ts @@ -1,5 +1,5 @@ import { DryRunError } from "@galacticcouncil/utils" -import { EvmParachain } from "@galacticcouncil/xc-core" +import { AssetRoute, EvmParachain } from "@galacticcouncil/xc-core" import { Call, Transfer } from "@galacticcouncil/xc-sdk" import { createContext, useContext } from "react" @@ -22,6 +22,7 @@ type XcmContextValue = { readonly alerts: XcmAlert[] readonly sourceChainAssetPairs: ChainAssetPair[] readonly destChainAssetPairs: ChainAssetPair[] + readonly availableBridgeRoutes: AssetRoute[] readonly registryChain: EvmParachain readonly status: XcmTransferStatus } @@ -37,6 +38,7 @@ export const XcmContext = createContext({ alerts: [], sourceChainAssetPairs: [], destChainAssetPairs: [], + availableBridgeRoutes: [], registryChain: {} as EvmParachain, status: XcmTransferStatus.Default, }) diff --git a/apps/main/src/modules/xcm/transfer/hooks/useXcmTransferConfigs.ts b/apps/main/src/modules/xcm/transfer/hooks/useXcmTransferConfigs.ts index 2d85e6f49..da8b2fdf1 100644 --- a/apps/main/src/modules/xcm/transfer/hooks/useXcmTransferConfigs.ts +++ b/apps/main/src/modules/xcm/transfer/hooks/useXcmTransferConfigs.ts @@ -6,12 +6,14 @@ import { } from "@galacticcouncil/xc-core" import { useCrossChainConfigService } from "@/api/xcm" +import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" export const useXcmTransferConfigs = ( srcAsset: Asset | null, srcChain: AnyChain | null, destChain: AnyChain | null, destAsset: Asset | null, + bridgeProvider?: string | null, ): TransferConfigs | null => { const configService = useCrossChainConfigService() if (!srcAsset || !srcChain || !destChain || !destAsset) return null @@ -37,5 +39,22 @@ export const useXcmTransferConfigs = ( return null } - return build(destAsset) + const configs = build(destAsset) + + if (bridgeProvider) { + const selectedRoute = + routes.find((r) => + (r.tags as string[] | undefined)?.includes(bridgeProvider), + ) ?? + getSupplementalBridgeRoutes( + srcChain.key, + destChain.key, + srcAsset.key, + ).find((r) => (r.tags as string[] | undefined)?.includes(bridgeProvider)) + if (selectedRoute) { + return { ...configs, origin: { ...configs.origin, route: selectedRoute } } + } + } + + return configs } diff --git a/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts b/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts new file mode 100644 index 000000000..b30475292 --- /dev/null +++ b/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts @@ -0,0 +1,65 @@ +import { builders, chainsMap } from "@galacticcouncil/xc-cfg" +import { AssetRoute, EvmParachain } from "@galacticcouncil/xc-core" + +const { ContractBuilder, BalanceBuilder } = builders + +/** + * Supplemental bridge routes for asset pairs where the configService only stores + * one route due to ChainRoutes Map deduplication (keyed by srcAsset-destChain-destAsset). + * + * These are the "lost" routes that are defined in xc-cfg source but overwritten + * at runtime. We reconstruct them using the public contract/balance builders. + * + * Key format: `${srcChainKey}-${destChainKey}-${srcAssetKey}` + */ +const buildSupplementalRoutes = (): Map => { + const base = chainsMap.get("base") + const hydration = chainsMap.get("hydration") + const moonbeam = chainsMap.get("moonbeam") + + if (!base || !hydration || !moonbeam) return new Map() + + const eurc = base.assetsData.get("eurc")?.asset + const eurcMwh = hydration.assetsData.get("eurc_mwh")?.asset + const eth = base.assetsData.get("eth")?.asset + + if (!eurc || !eurcMwh || !eth) return new Map() + + // Base → Hydration EURC via Wormhole+MRL + // This route is overwritten in ChainRoutes by the InstaBridge route (same map key) + const wormholeRoute = new AssetRoute({ + source: { + asset: eurc, + balance: BalanceBuilder().evm().erc20(), + fee: { asset: eth, balance: BalanceBuilder().evm().native() }, + destinationFee: { asset: eurc, balance: BalanceBuilder().evm().erc20() }, + }, + destination: { + chain: hydration, + asset: eurcMwh, + fee: { amount: 0, asset: eurcMwh }, + }, + contract: ContractBuilder() + .Wormhole() + .TokenBridge() + .transferTokensWithPayload() + .viaMrl({ moonchain: moonbeam as EvmParachain }), + tags: ["Mrl", "Wormhole"], + }) + + return new Map([["base-hydration-eurc", [wormholeRoute]]]) +} + +const SUPPLEMENTAL_ROUTES = buildSupplementalRoutes() + +/** + * Returns bridge routes for a given asset pair that are NOT present in the + * configService (because ChainRoutes overwrites them with a later duplicate key). + */ +export const getSupplementalBridgeRoutes = ( + srcChainKey: string, + destChainKey: string, + srcAssetKey: string, +): AssetRoute[] => { + return SUPPLEMENTAL_ROUTES.get(`${srcChainKey}-${destChainKey}-${srcAssetKey}`) ?? [] +} diff --git a/apps/main/src/modules/xcm/transfer/utils/transfer.ts b/apps/main/src/modules/xcm/transfer/utils/transfer.ts index c88fda197..94824b925 100644 --- a/apps/main/src/modules/xcm/transfer/utils/transfer.ts +++ b/apps/main/src/modules/xcm/transfer/utils/transfer.ts @@ -16,9 +16,26 @@ import { isEvmApproveCall } from "@/modules/transactions/utils/xcm" import { useApprovalTrackingStore } from "@/modules/xcm/transfer/hooks/useApprovalTrackingStore" import { XcmFormValues } from "@/modules/xcm/transfer/hooks/useXcmFormSchema" import { XcmAlert } from "@/modules/xcm/transfer/hooks/useXcmProvider" -import { XCM_BRIDGE_TAGS, XcmTags } from "@/states/transactions" +import { XCM_BRIDGE_TAGS, XcmTag, XcmTags } from "@/states/transactions" import { toDecimal } from "@/utils/formatting" +/** + * Bridge provider tags in priority order (InstaBridge first as the faster option). + */ +export const BRIDGE_PROVIDER_TAGS = [ + XcmTag.InstaBridge, + XcmTag.Wormhole, + XcmTag.Snowbridge, +] as const + +/** + * Returns the primary bridge provider tag for a given route. + */ +export const getPrimaryBridgeTag = (route: AssetRoute): string | null => { + const tags = (route.tags ?? []) as string[] + return BRIDGE_PROVIDER_TAGS.find((tag) => tags.includes(tag)) ?? null +} + export enum XcmTransferStatus { Default = "DEFAULT", TransferValid = "TRANSFER_VALID", @@ -78,7 +95,8 @@ export const getXcmTransferArgs = ( account: Account | null, values: XcmFormValues, ): XcmTransferArgs => { - const { srcChain, srcAsset, destChain, destAsset, destAddress } = values + const { srcChain, srcAsset, destChain, destAsset, destAddress, bridgeProvider } = + values const isValidPair = srcChain && srcAsset ? srcChain.assetsData.values().some((a) => a.asset.key === srcAsset.key) @@ -98,6 +116,7 @@ export const getXcmTransferArgs = ( : "", destAsset: isValidAsset ? destAsset.key : "", destChain: destChain?.key ?? "", + bridgeTag: bridgeProvider ?? undefined, } } diff --git a/apps/main/src/states/transactions.ts b/apps/main/src/states/transactions.ts index 71df83ff4..972f568c1 100644 --- a/apps/main/src/states/transactions.ts +++ b/apps/main/src/states/transactions.ts @@ -14,7 +14,11 @@ import { export const XcmTag = tags.Tag export type XcmTags = Array -export const XCM_BRIDGE_TAGS: XcmTags = [XcmTag.Wormhole, XcmTag.Snowbridge] +export const XCM_BRIDGE_TAGS: XcmTags = [ + XcmTag.Wormhole, + XcmTag.Snowbridge, + XcmTag.InstaBridge, +] export enum TransactionType { Onchain = "Onchain", diff --git a/package.json b/package.json index 4764d99f8..34bed0866 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "@galacticcouncil/descriptors": "^1.14.0", "@galacticcouncil/sdk-next": "^0.37.0", "@galacticcouncil/xc": "^0.4.0", - "@galacticcouncil/xc-cfg": "0.17.0-pr294-5865627", - "@galacticcouncil/xc-core": "0.13.0-pr294-5865627", + "@galacticcouncil/xc-cfg": "0.17.0-pr294-9b91595", + "@galacticcouncil/xc-core": "0.13.0-pr294-9b91595", "@galacticcouncil/xc-scan": "^0.3.0", - "@galacticcouncil/xc-sdk": "0.8.1-pr294-5865627", + "@galacticcouncil/xc-sdk": "0.8.1-pr294-9b91595", "big.js": "^6.2.2", "date-fns": "^4.1.0", "immer": "^10.0.3", From aabae982594311c55ebbf2a72467b6c41e235710 Mon Sep 17 00:00:00 2001 From: mrq Date: Wed, 11 Mar 2026 16:34:31 +0100 Subject: [PATCH 03/26] good ol sdk --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 34bed0866..4764d99f8 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "@galacticcouncil/descriptors": "^1.14.0", "@galacticcouncil/sdk-next": "^0.37.0", "@galacticcouncil/xc": "^0.4.0", - "@galacticcouncil/xc-cfg": "0.17.0-pr294-9b91595", - "@galacticcouncil/xc-core": "0.13.0-pr294-9b91595", + "@galacticcouncil/xc-cfg": "0.17.0-pr294-5865627", + "@galacticcouncil/xc-core": "0.13.0-pr294-5865627", "@galacticcouncil/xc-scan": "^0.3.0", - "@galacticcouncil/xc-sdk": "0.8.1-pr294-9b91595", + "@galacticcouncil/xc-sdk": "0.8.1-pr294-5865627", "big.js": "^6.2.2", "date-fns": "^4.1.0", "immer": "^10.0.3", From cb8e23a726b5f5ff5738461a6a87172cf45cb99c Mon Sep 17 00:00:00 2001 From: mrq Date: Wed, 11 Mar 2026 16:36:43 +0100 Subject: [PATCH 04/26] c9 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 4764d99f8..9564c930c 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "@galacticcouncil/descriptors": "^1.14.0", "@galacticcouncil/sdk-next": "^0.37.0", "@galacticcouncil/xc": "^0.4.0", - "@galacticcouncil/xc-cfg": "0.17.0-pr294-5865627", - "@galacticcouncil/xc-core": "0.13.0-pr294-5865627", + "@galacticcouncil/xc-cfg": "0.17.0-pr294-c9b7cd3", + "@galacticcouncil/xc-core": "0.13.0-pr294-c9b7cd3", "@galacticcouncil/xc-scan": "^0.3.0", - "@galacticcouncil/xc-sdk": "0.8.1-pr294-5865627", + "@galacticcouncil/xc-sdk": "0.8.1-pr294-c9b7cd3", "big.js": "^6.2.2", "date-fns": "^4.1.0", "immer": "^10.0.3", From 313ff6d544dc212af197431d554cdd7bc0958c92 Mon Sep 17 00:00:00 2001 From: mrq Date: Wed, 11 Mar 2026 16:40:04 +0100 Subject: [PATCH 05/26] 9b --- package.json | 6 +++--- yarn.lock | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 9564c930c..34bed0866 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "@galacticcouncil/descriptors": "^1.14.0", "@galacticcouncil/sdk-next": "^0.37.0", "@galacticcouncil/xc": "^0.4.0", - "@galacticcouncil/xc-cfg": "0.17.0-pr294-c9b7cd3", - "@galacticcouncil/xc-core": "0.13.0-pr294-c9b7cd3", + "@galacticcouncil/xc-cfg": "0.17.0-pr294-9b91595", + "@galacticcouncil/xc-core": "0.13.0-pr294-9b91595", "@galacticcouncil/xc-scan": "^0.3.0", - "@galacticcouncil/xc-sdk": "0.8.1-pr294-c9b7cd3", + "@galacticcouncil/xc-sdk": "0.8.1-pr294-9b91595", "big.js": "^6.2.2", "date-fns": "^4.1.0", "immer": "^10.0.3", diff --git a/yarn.lock b/yarn.lock index 387f1ce10..5d81d1b4f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2089,17 +2089,17 @@ "@thi.ng/memoize" "^4.0.2" big.js "^6.2.1" -"@galacticcouncil/xc-cfg@0.17.0-pr294-5865627": - version "0.17.0-pr294-5865627" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-cfg/-/xc-cfg-0.17.0-pr294-5865627.tgz#e1fe0a735b469473c07259402ce27d61a8bb5f8a" - integrity sha512-m0GzYnlwmUTZ8btzCzkf5s8WftgOgHMs/qCNwaApVqwteX6YQrtM724etB0z9DKdwSFV9rV7y0Az6/uIARcbhg== +"@galacticcouncil/xc-cfg@0.17.0-pr294-9b91595": + version "0.17.0-pr294-9b91595" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-cfg/-/xc-cfg-0.17.0-pr294-9b91595.tgz#8af1206f34ff5d03ff3b0c78f78bd334243b08bb" + integrity sha512-BFZu7AbMsUUmUS8TCJeTxQAuj6PAEJMbwIRsYkD7X2QOIalkjPT0krmLLsCZgvoWJgkXosw7KYRXXOZGWP9AuA== dependencies: - "@galacticcouncil/xc-core" "0.13.0-pr294-5865627" + "@galacticcouncil/xc-core" "0.13.0-pr294-9b91595" -"@galacticcouncil/xc-core@0.13.0-pr294-5865627": - version "0.13.0-pr294-5865627" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-core/-/xc-core-0.13.0-pr294-5865627.tgz#3927787d3fcd1f0f23c72b6351011fc13f494257" - integrity sha512-U7KHG6lV+qyQwZNjupwAC6pA4nW5a2qfkG1W9PIp5raz2XL2FjKD6TjpGN7hY/dCkFW+iobjaUv3Bb88tHbi8A== +"@galacticcouncil/xc-core@0.13.0-pr294-9b91595": + version "0.13.0-pr294-9b91595" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-core/-/xc-core-0.13.0-pr294-9b91595.tgz#34494c679b0a671eaa2bca850e828f6a822c1e18" + integrity sha512-PrXdPlGOkfOxsgm4u3YUsCRtSSVlFgoRyEBO3NCzsjTyZz7gDv4zhZBbyw47yyzsCJl3fw7xCt/0Nkgphqsu5g== dependencies: "@noble/hashes" "^1.6.1" "@wormhole-foundation/sdk-base" "3.2.0" @@ -2120,12 +2120,12 @@ resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-scan/-/xc-scan-0.3.0.tgz#662bb011c53f056c230d1d1057d31b94424758f7" integrity sha512-onH/zcGdA5JQx/tE7fcnhMUwosPnGflkJgcujgKeyuJPL15onvMPPKlDzvvNnUN8clfnefRtmEY9SNPo4md9uw== -"@galacticcouncil/xc-sdk@0.8.1-pr294-5865627": - version "0.8.1-pr294-5865627" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.8.1-pr294-5865627.tgz#d52c71b1551d7ccc0a70d7e96371af05ef868b8e" - integrity sha512-4KrDyjso1RuunAkrMzTD8SPq0dsV7dI7alvazB49G+ZWd2iIwbs+s4bmHVKrpUzLiidBR9Jv1mUDajSNG9X5BA== +"@galacticcouncil/xc-sdk@0.8.1-pr294-9b91595": + version "0.8.1-pr294-9b91595" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.8.1-pr294-9b91595.tgz#8dcc682d77f0f01ae9d34273e371d1f57d6cbc32" + integrity sha512-mpgtAiirejOx+GXmYK/C/L6T02kL40LZcP7MlHQjMaECAJweMCIAoHHlV6BYlJZmrUEENE7BaTCwSPmrl6qfOw== dependencies: - "@galacticcouncil/xc-core" "0.13.0-pr294-5865627" + "@galacticcouncil/xc-core" "0.13.0-pr294-9b91595" "@galacticcouncil/xc@^0.4.0": version "0.4.0" From c66eee2e1404e98684da69cb3757d9a477bb913f Mon Sep 17 00:00:00 2001 From: mrq Date: Wed, 11 Mar 2026 22:28:35 +0100 Subject: [PATCH 06/26] Basejump --- apps/main/src/i18n/locales/en/xcm.json | 4 +- .../BridgeSelector/BridgeSelector.styled.ts | 36 +++++++++- .../BridgeSelector/BridgeSelector.tsx | 71 ++++++++++++++----- package.json | 6 +- yarn.lock | 28 ++++---- 5 files changed, 107 insertions(+), 38 deletions(-) diff --git a/apps/main/src/i18n/locales/en/xcm.json b/apps/main/src/i18n/locales/en/xcm.json index 7f3b84982..5b1a26832 100644 --- a/apps/main/src/i18n/locales/en/xcm.json +++ b/apps/main/src/i18n/locales/en/xcm.json @@ -8,9 +8,9 @@ "approve.toast.success": "Approved {{ amount, number }} {{ symbol }} spending cap on {{ srcChain }}", "bridge.wormhole": "Wormhole", "bridge.snowbridge": "Snowbridge", - "bridge.instabridge": "Base Jumper", + "bridge.instabridge": "Basejump", "bridge.selector.label": "Via", - "bridge.provider.instabridge": "Base Jumper", + "bridge.provider.instabridge": "Basejump", "bridge.provider.wormhole": "Wormhole", "bridge.provider.snowbridge": "Snowbridge", "chainAssetSelect.button.selectAssetChain": "Select asset & chain", diff --git a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts index b30c42222..321b3ece1 100644 --- a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts +++ b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts @@ -1,6 +1,13 @@ -import { css } from "@emotion/react" +import { css, keyframes } from "@emotion/react" import styled from "@emotion/styled" +const slide = keyframes` + 0% { transform: translateX(-100%); opacity: 0; } + 8% { opacity: 1; } + 88% { opacity: 1; } + 100% { transform: translateX(100vw); opacity: 0; } +` + export const SBridgeOption = styled.button<{ active: boolean }>( ({ theme, active }) => css` display: flex; @@ -8,6 +15,9 @@ export const SBridgeOption = styled.button<{ active: boolean }>( align-items: center; gap: ${theme.space.base}; + position: relative; + overflow: hidden; + border: 1px solid ${theme.buttons.outlineDark.onOutline}; border-radius: 8px; @@ -29,3 +39,27 @@ export const SBridgeOption = styled.button<{ active: boolean }>( `} `, ) + +export const SParticle = styled.div<{ + color: string + duration: string + delay: string + active: boolean +}>( + ({ color, duration, delay, active }) => css` + position: absolute; + top: 0; + left: 0; + width: 48px; + height: 100%; + background: linear-gradient( + 90deg, + transparent 0%, + ${color}28 60%, + ${color}80 100% + ); + opacity: ${active ? 1 : 0.2}; + animation: ${slide} ${duration} ${delay} linear infinite; + pointer-events: none; + `, +) diff --git a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx index 7b0512d65..c07d3befa 100644 --- a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx +++ b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx @@ -8,7 +8,7 @@ import { XcmFormValues } from "@/modules/xcm/transfer/hooks/useXcmFormSchema" import { getPrimaryBridgeTag } from "@/modules/xcm/transfer/utils/transfer" import { XcmTag } from "@/states/transactions" -import { SBridgeOption } from "./BridgeSelector.styled" +import { SBridgeOption, SParticle } from "./BridgeSelector.styled" const BRIDGE_TIME_ESTIMATES: Partial> = { [XcmTag.InstaBridge]: "~42 sec", @@ -16,6 +16,20 @@ const BRIDGE_TIME_ESTIMATES: Partial> = { [XcmTag.Snowbridge]: "~25 min", } +// Particle animation durations — ratio matches real-world transfer times +// Base Jumper: 42 sec, Wormhole: 18 min (1080 sec) → ~25.7× slower +const BRIDGE_PARTICLE_DURATION: Partial> = { + [XcmTag.InstaBridge]: ["0.9s", "0.9s", "0.9s"], // 3 staggered packets = rapid stream + [XcmTag.Wormhole]: ["23s"], + [XcmTag.Snowbridge]: ["36s"], +} + +const BRIDGE_PARTICLE_COLOR: Partial> = { + [XcmTag.InstaBridge]: "#4fc4f9", + [XcmTag.Wormhole]: "#a78bfa", + [XcmTag.Snowbridge]: "#60a5fa", +} + type BridgeSelectorProps = { routes: AssetRoute[] } @@ -33,31 +47,52 @@ export const BridgeSelector: React.FC = ({ routes }) => { id: tag, label: t(`xcm:bridge.provider.${tag.toLowerCase()}`, tag), time: BRIDGE_TIME_ESTIMATES[tag], + durations: BRIDGE_PARTICLE_DURATION[tag] ?? ["5s"], + color: BRIDGE_PARTICLE_COLOR[tag] ?? "#ffffff", } }) - .filter(Boolean) as { id: string; label: string; time?: string }[] + .filter(Boolean) as { + id: string + label: string + time?: string + durations: string[] + color: string + }[] if (options.length < 2) return null return ( - {options.map((option) => ( - setValue("bridgeProvider", option.id)} - > - - {option.label} - - {option.time && ( - - {option.time} + {options.map((option) => { + const active = bridgeProvider === option.id + const count = option.durations.length + return ( + setValue("bridgeProvider", option.id)} + > + + {option.label} - )} - - ))} + {option.time && ( + + {option.time} + + )} + {option.durations.map((duration, i) => ( + + ))} + + ) + })} ) } diff --git a/package.json b/package.json index 34bed0866..6594b73a5 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "@galacticcouncil/descriptors": "^1.14.0", "@galacticcouncil/sdk-next": "^0.37.0", "@galacticcouncil/xc": "^0.4.0", - "@galacticcouncil/xc-cfg": "0.17.0-pr294-9b91595", - "@galacticcouncil/xc-core": "0.13.0-pr294-9b91595", + "@galacticcouncil/xc-cfg": "0.17.0-pr294-e720946", + "@galacticcouncil/xc-core": "0.13.0-pr294-e720946", "@galacticcouncil/xc-scan": "^0.3.0", - "@galacticcouncil/xc-sdk": "0.8.1-pr294-9b91595", + "@galacticcouncil/xc-sdk": "0.8.1-pr294-e720946", "big.js": "^6.2.2", "date-fns": "^4.1.0", "immer": "^10.0.3", diff --git a/yarn.lock b/yarn.lock index 5d81d1b4f..1690a1359 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2089,17 +2089,17 @@ "@thi.ng/memoize" "^4.0.2" big.js "^6.2.1" -"@galacticcouncil/xc-cfg@0.17.0-pr294-9b91595": - version "0.17.0-pr294-9b91595" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-cfg/-/xc-cfg-0.17.0-pr294-9b91595.tgz#8af1206f34ff5d03ff3b0c78f78bd334243b08bb" - integrity sha512-BFZu7AbMsUUmUS8TCJeTxQAuj6PAEJMbwIRsYkD7X2QOIalkjPT0krmLLsCZgvoWJgkXosw7KYRXXOZGWP9AuA== +"@galacticcouncil/xc-cfg@0.17.0-pr294-e720946": + version "0.17.0-pr294-e720946" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-cfg/-/xc-cfg-0.17.0-pr294-e720946.tgz#2a0329324ff7e9fb2631d80944a597ed5fd18c3d" + integrity sha512-I0kgjIEbRaqwsa7YqTcuMWG97QHm8wjZtT86bPwVJK5Z9gf+AMcVTvkPGSJCKJiICP53ldSQwnWxck2FLk6LEw== dependencies: - "@galacticcouncil/xc-core" "0.13.0-pr294-9b91595" + "@galacticcouncil/xc-core" "0.13.0-pr294-e720946" -"@galacticcouncil/xc-core@0.13.0-pr294-9b91595": - version "0.13.0-pr294-9b91595" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-core/-/xc-core-0.13.0-pr294-9b91595.tgz#34494c679b0a671eaa2bca850e828f6a822c1e18" - integrity sha512-PrXdPlGOkfOxsgm4u3YUsCRtSSVlFgoRyEBO3NCzsjTyZz7gDv4zhZBbyw47yyzsCJl3fw7xCt/0Nkgphqsu5g== +"@galacticcouncil/xc-core@0.13.0-pr294-e720946": + version "0.13.0-pr294-e720946" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-core/-/xc-core-0.13.0-pr294-e720946.tgz#38854c852542b91cddc6607a62c10eab852e90c0" + integrity sha512-zmi73ZDcHQ47Fmouaw3jGDKM55RUt1N7OAiJm3xXJgyiu+OBlW4rhWQlOPQgOEkIrEhBAx8H59YZDlFIfGwW9Q== dependencies: "@noble/hashes" "^1.6.1" "@wormhole-foundation/sdk-base" "3.2.0" @@ -2120,12 +2120,12 @@ resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-scan/-/xc-scan-0.3.0.tgz#662bb011c53f056c230d1d1057d31b94424758f7" integrity sha512-onH/zcGdA5JQx/tE7fcnhMUwosPnGflkJgcujgKeyuJPL15onvMPPKlDzvvNnUN8clfnefRtmEY9SNPo4md9uw== -"@galacticcouncil/xc-sdk@0.8.1-pr294-9b91595": - version "0.8.1-pr294-9b91595" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.8.1-pr294-9b91595.tgz#8dcc682d77f0f01ae9d34273e371d1f57d6cbc32" - integrity sha512-mpgtAiirejOx+GXmYK/C/L6T02kL40LZcP7MlHQjMaECAJweMCIAoHHlV6BYlJZmrUEENE7BaTCwSPmrl6qfOw== +"@galacticcouncil/xc-sdk@0.8.1-pr294-e720946": + version "0.8.1-pr294-e720946" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.8.1-pr294-e720946.tgz#ddc4dd912df6a0f219e69c76ed88d6624422fe2f" + integrity sha512-ij5wrXIl3woKGPpBiYdB9r9QI+Q2G2+Dv4zOVChos35v5bVt/BhP0Xg2mcuXm35xu9vO44RCwBxMK+Nr9WuQ1g== dependencies: - "@galacticcouncil/xc-core" "0.13.0-pr294-9b91595" + "@galacticcouncil/xc-core" "0.13.0-pr294-e720946" "@galacticcouncil/xc@^0.4.0": version "0.4.0" From 0b1842f238d74b26bb6af8fe51749659e88a0d04 Mon Sep 17 00:00:00 2001 From: mrq Date: Wed, 11 Mar 2026 22:34:52 +0100 Subject: [PATCH 07/26] =?UTF-8?q?=F0=9F=AA=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/main/src/i18n/locales/en/xcm.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/main/src/i18n/locales/en/xcm.json b/apps/main/src/i18n/locales/en/xcm.json index 5b1a26832..e4a941731 100644 --- a/apps/main/src/i18n/locales/en/xcm.json +++ b/apps/main/src/i18n/locales/en/xcm.json @@ -8,9 +8,9 @@ "approve.toast.success": "Approved {{ amount, number }} {{ symbol }} spending cap on {{ srcChain }}", "bridge.wormhole": "Wormhole", "bridge.snowbridge": "Snowbridge", - "bridge.instabridge": "Basejump", + "bridge.instabridge": "Basejump 🪂", "bridge.selector.label": "Via", - "bridge.provider.instabridge": "Basejump", + "bridge.provider.instabridge": "Basejump 🪂", "bridge.provider.wormhole": "Wormhole", "bridge.provider.snowbridge": "Snowbridge", "chainAssetSelect.button.selectAssetChain": "Select asset & chain", From 70240f689f8f871157cd74c195e16c73d9ae2a67 Mon Sep 17 00:00:00 2001 From: mrq Date: Wed, 11 Mar 2026 23:09:43 +0100 Subject: [PATCH 08/26] bump ci From 508d3d39ae90efa4166acaf4d23169634874970f Mon Sep 17 00:00:00 2001 From: mrq Date: Wed, 11 Mar 2026 23:28:34 +0100 Subject: [PATCH 09/26] xcm form defaults --- apps/main/src/modules/xcm/transfer/utils/chain.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/main/src/modules/xcm/transfer/utils/chain.ts b/apps/main/src/modules/xcm/transfer/utils/chain.ts index 32daef58e..a65cef65a 100644 --- a/apps/main/src/modules/xcm/transfer/utils/chain.ts +++ b/apps/main/src/modules/xcm/transfer/utils/chain.ts @@ -98,6 +98,7 @@ export const getXcmFormDefaults = (account: Account | null): XcmFormValues => { destAmount: "", destAddress: destAccount?.rawAddress ?? "", destAccount: destAccount, + bridgeProvider: null, } } From f6a2a11c781aa384c7a109504ad653c542fda1fe Mon Sep 17 00:00:00 2001 From: mrq Date: Mon, 16 Mar 2026 00:43:47 +0100 Subject: [PATCH 10/26] tracking basejump status --- apps/main/src/i18n/locales/en/xcm.json | 2 ++ .../src/modules/xcm/history/XcJourneyCard.tsx | 35 ++++++++++++++++--- .../history/XcScanHistoryTable.columns.tsx | 11 +++++- .../xcm/history/hooks/useXcmBridgeTxStore.ts | 27 ++++++++++++++ .../modules/xcm/history/utils/optimistic.ts | 2 +- .../modules/xcm/history/utils/protocols.ts | 4 +++ .../main/src/modules/xcm/transfer/XcmForm.tsx | 10 ++++++ 7 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 apps/main/src/modules/xcm/history/hooks/useXcmBridgeTxStore.ts diff --git a/apps/main/src/i18n/locales/en/xcm.json b/apps/main/src/i18n/locales/en/xcm.json index e4a941731..06679869b 100644 --- a/apps/main/src/i18n/locales/en/xcm.json +++ b/apps/main/src/i18n/locales/en/xcm.json @@ -52,6 +52,8 @@ "report.destFee.insufficientBalance": "You need to have at least {{ amount, number }} {{ symbol }} on {{ chain }}", "report.asset.frozen": "Your account on {{ chain }} has frozen balance for {{ symbol }}", "report.account.insufficientDeposit": "You need to have {{ amount, number }} {{ symbol }} on {{ chain }} for existential deposit", + "journey.fastDelivery": "fast delivery", + "journey.delivered": "Delivered", "journey.status.sent": "In Progress", "journey.status.pending": "In Progress", "journey.status.received": "Completed", diff --git a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx index 2da1a953f..0e832d83b 100644 --- a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx +++ b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx @@ -23,15 +23,27 @@ import { JourneyAssetLogo } from "@/modules/xcm/history/components/JourneyAssetL import { JourneyChainLogo } from "@/modules/xcm/history/components/JourneyChainLogo" import { JourneyDate } from "@/modules/xcm/history/components/JourneyDate" import { JourneyStatus } from "@/modules/xcm/history/components/JourneyStatus" +import { useXcmBridgeTxStore } from "@/modules/xcm/history/hooks/useXcmBridgeTxStore" import { usePendingClaimsStore } from "@/modules/xcm/history/hooks/usePendingClaimsStore" import { getTransferAsset, resolveNetwork, } from "@/modules/xcm/history/utils/assets" import { isJourneyClaimable } from "@/modules/xcm/history/utils/claim" +import { + FAILED_STATUSES, + TJourneyStatus, +} from "@/modules/xcm/history/utils/journey" import { isOptimisticJourney } from "@/modules/xcm/history/utils/optimistic" +import { XcmTag } from "@/states/transactions" import { toDecimal } from "@/utils/formatting" +function getBasejumpStatus(status: TJourneyStatus): TJourneyStatus { + if (FAILED_STATUSES.includes(status)) return status + if (status === "pending" || status === "sent") return status + return "completed" +} + export const XcJourneyCard: React.FC = (journey) => { const { origin, @@ -45,18 +57,26 @@ export const XcJourneyCard: React.FC = (journey) => { toFormatted, to, totalUsd, + originTxPrimary, } = journey const { t } = useTranslation(["common", "xcm"]) const { pendingCorrelationIds } = usePendingClaimsStore() + const { entries } = useXcmBridgeTxStore() + + const entry = originTxPrimary ? entries[originTxPrimary] : undefined + const isBasejump = entry?.bridgeProvider === XcmTag.InstaBridge + + const displayDestination = (isBasejump && entry?.destUrn) ? entry.destUrn : destination + const displayStatus = isBasejump ? getBasejumpStatus(status) : status const originNetwork = resolveNetwork(origin) - const destinationNetwork = resolveNetwork(destination) + const destinationNetwork = resolveNetwork(displayDestination) const transferAsset = getTransferAsset(assets) const link = xcscan.tx(correlationId) const isNotPending = !pendingCorrelationIds.includes(journey.correlationId) - const isClaimable = isNotPending && isJourneyClaimable(journey) + const isClaimable = isNotPending && !isBasejump && isJourneyClaimable(journey) return ( @@ -71,7 +91,7 @@ export const XcJourneyCard: React.FC = (journey) => { mx="s" color={getToken("icons.onSurface")} /> - + ) : ( = (journey) => { sx={{ alignSelf: "stretch" }} /> - - + + {isBasejump && ( + + {t("xcm:bridge.provider.instabridge", "Basejump 🪂")} + + )} + {sentAt && ( diff --git a/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx b/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx index 3faea0c91..a271e3ab6 100644 --- a/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx +++ b/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx @@ -24,8 +24,10 @@ import { JourneyDate } from "@/modules/xcm/history/components/JourneyDate" import { JourneyProtocol } from "@/modules/xcm/history/components/JourneyProtocol" import { JourneyStatus } from "@/modules/xcm/history/components/JourneyStatus" import { usePendingClaimsStore } from "@/modules/xcm/history/hooks/usePendingClaimsStore" +import { useXcmBridgeTxStore } from "@/modules/xcm/history/hooks/useXcmBridgeTxStore" import { getTransferAsset } from "@/modules/xcm/history/utils/assets" import { isJourneyClaimable } from "@/modules/xcm/history/utils/claim" +import { XcmTag } from "@/states/transactions" import { toDecimal } from "@/utils/formatting" const columnHelper = createColumnHelper() @@ -45,6 +47,7 @@ export enum XcScanHistoryTableColumnId { export const useXcScanHistoryColumns = () => { const { t } = useTranslation(["common"]) const { pendingCorrelationIds } = usePendingClaimsStore() + const { entries: bridgeTxEntries } = useXcmBridgeTxStore() return useMemo(() => { const fromColumn = columnHelper.accessor("from", { @@ -140,6 +143,12 @@ export const useXcScanHistoryColumns = () => { cell: ({ row }) => { const originProtocol = row.original.originProtocol const destinationProtocol = row.original.destinationProtocol + const txHash = row.original.originTxPrimary + const bridgeTag = txHash ? bridgeTxEntries[txHash] : undefined + + if (bridgeTag?.bridgeProvider === XcmTag.InstaBridge) { + return + } if (!originProtocol && !destinationProtocol) { return null @@ -230,5 +239,5 @@ export const useXcScanHistoryColumns = () => { durationColumn, actionColumn, ] - }, [t, pendingCorrelationIds]) + }, [t, pendingCorrelationIds, bridgeTxEntries]) } diff --git a/apps/main/src/modules/xcm/history/hooks/useXcmBridgeTxStore.ts b/apps/main/src/modules/xcm/history/hooks/useXcmBridgeTxStore.ts new file mode 100644 index 000000000..0f37193c5 --- /dev/null +++ b/apps/main/src/modules/xcm/history/hooks/useXcmBridgeTxStore.ts @@ -0,0 +1,27 @@ +import { create } from "zustand" +import { persist } from "zustand/middleware" + +export type XcmBridgeTxEntry = { + bridgeProvider: string + /** Intended destination chain URN (may differ from xc-scan's tracked destination) */ + destUrn?: string +} + +type XcmBridgeTxStore = { + /** Maps originTxPrimary (txHash on source chain) → entry */ + entries: Record + addEntry: (txHash: string, entry: XcmBridgeTxEntry) => void +} + +export const useXcmBridgeTxStore = create()( + persist( + (set) => ({ + entries: {}, + addEntry: (txHash, entry) => + set((state) => ({ + entries: { ...state.entries, [txHash]: entry }, + })), + }), + { name: "xcm-bridge-tx-store", version: 2 }, + ), +) diff --git a/apps/main/src/modules/xcm/history/utils/optimistic.ts b/apps/main/src/modules/xcm/history/utils/optimistic.ts index 738861578..1d340715a 100644 --- a/apps/main/src/modules/xcm/history/utils/optimistic.ts +++ b/apps/main/src/modules/xcm/history/utils/optimistic.ts @@ -18,7 +18,7 @@ export function isOptimisticJourney(journey: XcJourney): boolean { return journey.correlationId.startsWith(OPTIMISTIC_JOURNEY_PREFIX) } -function chainToUrn(chain: AnyChain): string { +export function chainToUrn(chain: AnyChain): string { const ecosystem = chain.ecosystem if (!ecosystem) return "" return `urn:ocn:${ecosystem.toLowerCase()}:${getChainId(chain)}` diff --git a/apps/main/src/modules/xcm/history/utils/protocols.ts b/apps/main/src/modules/xcm/history/utils/protocols.ts index bc92e8aa0..6b990dc6b 100644 --- a/apps/main/src/modules/xcm/history/utils/protocols.ts +++ b/apps/main/src/modules/xcm/history/utils/protocols.ts @@ -2,6 +2,10 @@ import { ThemeToken } from "@galacticcouncil/ui/theme" const XC_SCAN_PROTOCOLS: Record = { + instabridge: { + label: "Basejump 🪂", + color: "colors.skyBlue.600", + }, xcm: { label: "XCM", color: "colors.coral.400", diff --git a/apps/main/src/modules/xcm/transfer/XcmForm.tsx b/apps/main/src/modules/xcm/transfer/XcmForm.tsx index c329b4bbe..8f48319af 100644 --- a/apps/main/src/modules/xcm/transfer/XcmForm.tsx +++ b/apps/main/src/modules/xcm/transfer/XcmForm.tsx @@ -16,7 +16,9 @@ import { useFormContext } from "react-hook-form" import { useTranslation } from "react-i18next" import { useCrossChainBalance } from "@/api/xcm" +import { useXcmBridgeTxStore } from "@/modules/xcm/history/hooks/useXcmBridgeTxStore" import { + chainToUrn, insertOptimisticJourney, removeOptimisticJourney, } from "@/modules/xcm/history/utils/optimistic" @@ -83,6 +85,8 @@ export const XcmForm = () => { const queryClient = useQueryClient() + const { addEntry: addBridgeTxEntry } = useXcmBridgeTxStore() + const submit = useSubmitXcmTransfer({ onTransferSubmitted: (txHash, values, transfer) => { if (account) { @@ -94,6 +98,12 @@ export const XcmForm = () => { transfer, ) } + if (values.bridgeProvider) { + addBridgeTxEntry(txHash, { + bridgeProvider: values.bridgeProvider, + destUrn: values.destChain ? chainToUrn(values.destChain) : undefined, + }) + } resetAmounts() }, onTransferError: (txHash) => { From dded083bd1a894d3f66971eb693108eb7b3e7fc6 Mon Sep 17 00:00:00 2001 From: mrq Date: Tue, 31 Mar 2026 03:52:21 +0200 Subject: [PATCH 11/26] basejump --- apps/main/src/i18n/locales/en/xcm.json | 4 ++-- apps/main/src/modules/xcm/history/XcJourneyCard.tsx | 4 ++-- apps/main/src/modules/xcm/history/utils/protocols.ts | 2 +- apps/main/src/modules/xcm/transfer/XcmProvider.tsx | 2 +- .../transfer/components/BridgeSelector/BridgeSelector.tsx | 6 +++--- apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts | 2 +- apps/main/src/modules/xcm/transfer/utils/transfer.ts | 4 ++-- apps/main/src/states/transactions.ts | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/apps/main/src/i18n/locales/en/xcm.json b/apps/main/src/i18n/locales/en/xcm.json index e3949611a..db0a556c0 100644 --- a/apps/main/src/i18n/locales/en/xcm.json +++ b/apps/main/src/i18n/locales/en/xcm.json @@ -6,9 +6,9 @@ "approve.toast.success": "Approved {{ amount, number }} {{ symbol }} spending cap on {{ srcChain }}", "bridge.wormhole": "Wormhole", "bridge.snowbridge": "Snowbridge", - "bridge.instabridge": "Basejump 🪂", + "bridge.basejump": "Basejump 🪂", "bridge.selector.label": "Via", - "bridge.provider.instabridge": "Basejump 🪂", + "bridge.provider.basejump": "Basejump 🪂", "bridge.provider.wormhole": "Wormhole", "bridge.provider.snowbridge": "Snowbridge", "chainAssetSelect.button.selectAssetChain": "Select asset & chain", diff --git a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx index bc9b8b82b..434cb456c 100644 --- a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx +++ b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx @@ -49,7 +49,7 @@ export const XcJourneyCard: React.FC = (journey) => { const { entries } = useXcmBridgeTxStore() const entry = originTxPrimary ? entries[originTxPrimary] : undefined - const isBasejump = entry?.bridgeProvider === XcmTag.InstaBridge + const isBasejump = entry?.bridgeProvider === XcmTag.Basejump const displayDestination = (isBasejump && entry?.destUrn) ? entry.destUrn : destination const displayStatus = isBasejump ? getBasejumpStatus(status) : status @@ -98,7 +98,7 @@ export const XcJourneyCard: React.FC = (journey) => { {isBasejump && ( - {t("xcm:bridge.provider.instabridge", "Basejump 🪂")} + {t("xcm:bridge.provider.basejump", "Basejump 🪂")} )} diff --git a/apps/main/src/modules/xcm/history/utils/protocols.ts b/apps/main/src/modules/xcm/history/utils/protocols.ts index 6b990dc6b..27027cf07 100644 --- a/apps/main/src/modules/xcm/history/utils/protocols.ts +++ b/apps/main/src/modules/xcm/history/utils/protocols.ts @@ -2,7 +2,7 @@ import { ThemeToken } from "@galacticcouncil/ui/theme" const XC_SCAN_PROTOCOLS: Record = { - instabridge: { + basejump: { label: "Basejump 🪂", color: "colors.skyBlue.600", }, diff --git a/apps/main/src/modules/xcm/transfer/XcmProvider.tsx b/apps/main/src/modules/xcm/transfer/XcmProvider.tsx index f1e736de1..e3243c5b1 100644 --- a/apps/main/src/modules/xcm/transfer/XcmProvider.tsx +++ b/apps/main/src/modules/xcm/transfer/XcmProvider.tsx @@ -157,7 +157,7 @@ export const XcmProvider: React.FC = ({ children }) => { const defaultRoute = availableBridgeRoutes.find( - (r) => getPrimaryBridgeTag(r) === XcmTag.InstaBridge, + (r) => getPrimaryBridgeTag(r) === XcmTag.Basejump, ) ?? availableBridgeRoutes[0] if (!defaultRoute) return form.setValue("bridgeProvider", getPrimaryBridgeTag(defaultRoute)) diff --git a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx index c07d3befa..f0614bdbc 100644 --- a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx +++ b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx @@ -11,7 +11,7 @@ import { XcmTag } from "@/states/transactions" import { SBridgeOption, SParticle } from "./BridgeSelector.styled" const BRIDGE_TIME_ESTIMATES: Partial> = { - [XcmTag.InstaBridge]: "~42 sec", + [XcmTag.Basejump]: "~42 sec", [XcmTag.Wormhole]: "~18 min", [XcmTag.Snowbridge]: "~25 min", } @@ -19,13 +19,13 @@ const BRIDGE_TIME_ESTIMATES: Partial> = { // Particle animation durations — ratio matches real-world transfer times // Base Jumper: 42 sec, Wormhole: 18 min (1080 sec) → ~25.7× slower const BRIDGE_PARTICLE_DURATION: Partial> = { - [XcmTag.InstaBridge]: ["0.9s", "0.9s", "0.9s"], // 3 staggered packets = rapid stream + [XcmTag.Basejump]: ["0.9s", "0.9s", "0.9s"], // 3 staggered packets = rapid stream [XcmTag.Wormhole]: ["23s"], [XcmTag.Snowbridge]: ["36s"], } const BRIDGE_PARTICLE_COLOR: Partial> = { - [XcmTag.InstaBridge]: "#4fc4f9", + [XcmTag.Basejump]: "#4fc4f9", [XcmTag.Wormhole]: "#a78bfa", [XcmTag.Snowbridge]: "#60a5fa", } diff --git a/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts b/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts index b30475292..5e9d7af5f 100644 --- a/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts +++ b/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts @@ -26,7 +26,7 @@ const buildSupplementalRoutes = (): Map => { if (!eurc || !eurcMwh || !eth) return new Map() // Base → Hydration EURC via Wormhole+MRL - // This route is overwritten in ChainRoutes by the InstaBridge route (same map key) + // This route is overwritten in ChainRoutes by the Basejump route (same map key) const wormholeRoute = new AssetRoute({ source: { asset: eurc, diff --git a/apps/main/src/modules/xcm/transfer/utils/transfer.ts b/apps/main/src/modules/xcm/transfer/utils/transfer.ts index 94824b925..38caaa1a3 100644 --- a/apps/main/src/modules/xcm/transfer/utils/transfer.ts +++ b/apps/main/src/modules/xcm/transfer/utils/transfer.ts @@ -20,10 +20,10 @@ import { XCM_BRIDGE_TAGS, XcmTag, XcmTags } from "@/states/transactions" import { toDecimal } from "@/utils/formatting" /** - * Bridge provider tags in priority order (InstaBridge first as the faster option). + * Bridge provider tags in priority order (Basejump first as the faster option). */ export const BRIDGE_PROVIDER_TAGS = [ - XcmTag.InstaBridge, + XcmTag.Basejump, XcmTag.Wormhole, XcmTag.Snowbridge, ] as const diff --git a/apps/main/src/states/transactions.ts b/apps/main/src/states/transactions.ts index d00eb9484..34cf02a56 100644 --- a/apps/main/src/states/transactions.ts +++ b/apps/main/src/states/transactions.ts @@ -17,7 +17,7 @@ export type XcmTags = Array export const XCM_BRIDGE_TAGS: XcmTags = [ XcmTag.Wormhole, XcmTag.Snowbridge, - XcmTag.InstaBridge, + XcmTag.Basejump, ] export enum TransactionType { From e40767a7a49c03bbd300f6a1070506be01e7f432 Mon Sep 17 00:00:00 2001 From: mrq Date: Tue, 31 Mar 2026 03:58:57 +0200 Subject: [PATCH 12/26] clenup --- .../src/modules/xcm/history/XcJourneyCard.tsx | 30 ++++--------------- yarn.lock | 28 ++++++++--------- 2 files changed, 19 insertions(+), 39 deletions(-) diff --git a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx index 434cb456c..e9e43a347 100644 --- a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx +++ b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx @@ -23,7 +23,6 @@ import { JourneyAssetLogo } from "@/modules/xcm/history/components/JourneyAssetL import { JourneyChainLogo } from "@/modules/xcm/history/components/JourneyChainLogo" import { JourneyDate } from "@/modules/xcm/history/components/JourneyDate" import { JourneyStatus } from "@/modules/xcm/history/components/JourneyStatus" -import { useXcmBridgeTxStore } from "@/modules/xcm/history/hooks/useXcmBridgeTxStore" import { usePendingClaimsStore } from "@/modules/xcm/history/hooks/usePendingClaimsStore" import { getTransferAsset, @@ -32,37 +31,23 @@ import { import { isJourneyClaimable } from "@/modules/xcm/history/utils/claim" import { getFormattedAddresses } from "@/modules/xcm/history/utils/journey" import { isOptimisticJourney } from "@/modules/xcm/history/utils/optimistic" -import { XcmTag } from "@/states/transactions" import { toDecimal } from "@/utils/formatting" -function getBasejumpStatus(status: TJourneyStatus): TJourneyStatus { - if (FAILED_STATUSES.includes(status)) return status - if (status === "pending" || status === "sent") return status - return "completed" -} - export const XcJourneyCard: React.FC = (journey) => { const { origin, destination, sentAt, correlationId, status, totalUsd } = journey const { t } = useTranslation(["common", "xcm"]) const { pendingCorrelationIds } = usePendingClaimsStore() - const { entries } = useXcmBridgeTxStore() - - const entry = originTxPrimary ? entries[originTxPrimary] : undefined - const isBasejump = entry?.bridgeProvider === XcmTag.Basejump - - const displayDestination = (isBasejump && entry?.destUrn) ? entry.destUrn : destination - const displayStatus = isBasejump ? getBasejumpStatus(status) : status const originNetwork = resolveNetwork(origin) - const destinationNetwork = resolveNetwork(displayDestination) + const destinationNetwork = resolveNetwork(destination) const transferAsset = getTransferAsset(journey) const { from, to } = getFormattedAddresses(journey) const link = xcscan.tx(correlationId) const isNotPending = !pendingCorrelationIds.includes(journey.correlationId) - const isClaimable = isNotPending && !isBasejump && isJourneyClaimable(journey) + const isClaimable = isNotPending && isJourneyClaimable(journey) return ( @@ -77,7 +62,7 @@ export const XcJourneyCard: React.FC = (journey) => { mx="s" color={getToken("icons.onSurface")} /> - + ) : ( = (journey) => { sx={{ alignSelf: "stretch" }} /> - - {isBasejump && ( - - {t("xcm:bridge.provider.basejump", "Basejump 🪂")} - - )} - + + {sentAt && ( diff --git a/yarn.lock b/yarn.lock index 286d00803..5d7f4f057 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2089,17 +2089,17 @@ "@thi.ng/memoize" "^4.0.2" big.js "^6.2.1" -"@galacticcouncil/xc-cfg@^0.18.0": - version "0.18.0" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-cfg/-/xc-cfg-0.18.0.tgz#ca31cb2232f46d27de87c37890480dee92e3d143" - integrity sha512-5L7LqcErM25RA3T0cm/LTjiMs9kGFStZV3JLF6fES0fH9DSZB9Ev+8Cl9bjYZmLza5W+o1MqtDu/IdeyHJo7QA== +"@galacticcouncil/xc-cfg@0.19.0-pr304-7025779": + version "0.19.0-pr304-7025779" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-cfg/-/xc-cfg-0.19.0-pr304-7025779.tgz#b55b68e8e7b61314e9442a7cc8df5645e768e561" + integrity sha512-knA+azbYk/3oIne8d60ACPqoc+rEwzA+LAixMXCEycxWpmj9HTivGV17y23h622QJsRJ5ggjZ61errvpX5maPA== dependencies: - "@galacticcouncil/xc-core" "^0.12.0" + "@galacticcouncil/xc-core" "0.14.0-pr304-7025779" -"@galacticcouncil/xc-core@^0.12.0": - version "0.12.0" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-core/-/xc-core-0.12.0.tgz#91e19726f2bdf5469d1e87874173ce726aca6d2a" - integrity sha512-aGkTkGpHDYlzLVg29i4IwhC0ntmZkpf73+WsjiaY3h/iSjGSwRNMU48gbKoNjvni8a7pSBKD2bWMOO6Uu2vU9A== +"@galacticcouncil/xc-core@0.14.0-pr304-7025779": + version "0.14.0-pr304-7025779" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-core/-/xc-core-0.14.0-pr304-7025779.tgz#350dac5dfbb6fa002e659fe9b1c489110088b138" + integrity sha512-If652Ag96VDr5NjUs5h80f5pQIbBkC99LNFjNzApRoiMorG3sTNYZr4R/RS2XtakH7Ca1YSBwU9PpLufBOIlpA== dependencies: "@noble/hashes" "^1.6.1" "@wormhole-foundation/sdk-base" "3.2.0" @@ -2120,12 +2120,12 @@ resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-scan/-/xc-scan-0.3.0.tgz#662bb011c53f056c230d1d1057d31b94424758f7" integrity sha512-onH/zcGdA5JQx/tE7fcnhMUwosPnGflkJgcujgKeyuJPL15onvMPPKlDzvvNnUN8clfnefRtmEY9SNPo4md9uw== -"@galacticcouncil/xc-sdk@^0.9.0": - version "0.9.0" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.9.0.tgz#1e9f08cb54f6486b260781ff9b4681d83ad75fc0" - integrity sha512-/3Bq5HFnQfBCrYiubrmW7pXuAsekaZ0zEAIFc1+1T+aZ0aOVHHHUUsR0HHRj37ayNA50tVMB+/KL+pdeHRgTng== +"@galacticcouncil/xc-sdk@0.9.2-pr304-7025779": + version "0.9.2-pr304-7025779" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.9.2-pr304-7025779.tgz#fe9a404d5c11c20103b94ca3cfcc1ecee1c2ed9f" + integrity sha512-Lt8ykg1669u9+4w4hWbfGQt8rnsQ2UhbzXxGEVOOBi5d2Pvyf74kkhQ5TpTbAMDYnskRAuz710o2XwKyP4j/aw== dependencies: - "@galacticcouncil/xc-core" "^0.12.0" + "@galacticcouncil/xc-core" "0.14.0-pr304-7025779" "@galacticcouncil/xc@^0.4.0": version "0.4.0" From 2437383c9aabda5b78388db342d096ca8beb74ea Mon Sep 17 00:00:00 2001 From: mrq Date: Tue, 31 Mar 2026 04:09:26 +0200 Subject: [PATCH 13/26] lint:fix --- apps/main/src/api/xcm.ts | 7 +++---- apps/main/src/modules/xcm/transfer/XcmForm.tsx | 2 +- .../src/modules/xcm/transfer/XcmProvider.tsx | 6 ++++-- .../xcm/transfer/hooks/useSubmitXcmTransfer.ts | 18 +++++++++--------- .../xcm/transfer/utils/bridge-routes.ts | 5 ++++- .../src/modules/xcm/transfer/utils/transfer.ts | 10 ++++++++-- 6 files changed, 29 insertions(+), 19 deletions(-) diff --git a/apps/main/src/api/xcm.ts b/apps/main/src/api/xcm.ts index 022b0e008..f72386f03 100644 --- a/apps/main/src/api/xcm.ts +++ b/apps/main/src/api/xcm.ts @@ -3,8 +3,6 @@ import { createXcContext } from "@galacticcouncil/xc" import { chainsMap } from "@galacticcouncil/xc-cfg" import { AnyChain, AssetAmount, ConfigBuilder } from "@galacticcouncil/xc-core" import { Transfer, TransferBuilder, Wallet } from "@galacticcouncil/xc-sdk" - -import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { keepPreviousData, queryOptions, @@ -16,6 +14,7 @@ import { import { secondsToMilliseconds } from "date-fns" import { useEffect, useRef, useState } from "react" +import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { TProviderContext, useRpcProvider } from "@/providers/rpcProvider" export const useCrossChainConfig = () => { @@ -179,8 +178,8 @@ export const xcmTransferQuery = ( builder.routes.find((r) => (r.tags as string[] | undefined)?.includes(bridgeTag), ) ?? - getSupplementalBridgeRoutes(srcChain, destChain, srcAsset).find( - (r) => (r.tags as string[] | undefined)?.includes(bridgeTag), + getSupplementalBridgeRoutes(srcChain, destChain, srcAsset).find((r) => + (r.tags as string[] | undefined)?.includes(bridgeTag), ) if (selectedRoute) { diff --git a/apps/main/src/modules/xcm/transfer/XcmForm.tsx b/apps/main/src/modules/xcm/transfer/XcmForm.tsx index ca520a66f..d222b3ee2 100644 --- a/apps/main/src/modules/xcm/transfer/XcmForm.tsx +++ b/apps/main/src/modules/xcm/transfer/XcmForm.tsx @@ -22,8 +22,8 @@ import { insertOptimisticJourney, removeOptimisticJourney, } from "@/modules/xcm/history/utils/optimistic" -import { ChainAssetSelectModalSelectionChange } from "@/modules/xcm/transfer/components/ChainAssetSelect" import { BridgeSelector } from "@/modules/xcm/transfer/components/BridgeSelector" +import { ChainAssetSelectModalSelectionChange } from "@/modules/xcm/transfer/components/ChainAssetSelect" import { ChainSwitch } from "@/modules/xcm/transfer/components/ChainSwitch" import { ConnectButton } from "@/modules/xcm/transfer/components/ConnectButton" import { diff --git a/apps/main/src/modules/xcm/transfer/XcmProvider.tsx b/apps/main/src/modules/xcm/transfer/XcmProvider.tsx index e3243c5b1..2d41e647c 100644 --- a/apps/main/src/modules/xcm/transfer/XcmProvider.tsx +++ b/apps/main/src/modules/xcm/transfer/XcmProvider.tsx @@ -21,12 +21,12 @@ import { useXcmForm } from "@/modules/xcm/transfer/hooks/useXcmForm" import { XcmContext } from "@/modules/xcm/transfer/hooks/useXcmProvider" import { useXcmTransfer } from "@/modules/xcm/transfer/hooks/useXcmTransfer" import { useXcmTransferAlerts } from "@/modules/xcm/transfer/hooks/useXcmTransferAlerts" +import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { getChainPriority, isAccountValidOnChain, XCM_CHAINS, } from "@/modules/xcm/transfer/utils/chain" -import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { calculateTransferDestAmount, getPrimaryBridgeTag, @@ -129,7 +129,9 @@ export const XcmProvider: React.FC = ({ children }) => { r.destination.asset.key === destAsset.key && getPrimaryBridgeTag(r) !== null, ) - const existingTags = new Set(configRoutes.map((r) => getPrimaryBridgeTag(r))) + const existingTags = new Set( + configRoutes.map((r) => getPrimaryBridgeTag(r)), + ) const supplemental = getSupplementalBridgeRoutes( srcChain.key, destChain.key, diff --git a/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts b/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts index f1c3be5d8..a1279af09 100644 --- a/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts +++ b/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts @@ -12,11 +12,11 @@ import { Binary } from "polkadot-api" import { useTranslation } from "react-i18next" import { useCrossChainConfigService } from "@/api/xcm" -import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { AnyPapiTx } from "@/modules/transactions/types" import { isEvmApproveCall, isEvmCall } from "@/modules/transactions/utils/xcm" import { useApprovalTrackingStore } from "@/modules/xcm/transfer/hooks/useApprovalTrackingStore" import { XcmFormValues } from "@/modules/xcm/transfer/hooks/useXcmFormSchema" +import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { buildTransferCall } from "@/modules/xcm/transfer/utils/transfer" import { useRpcProvider } from "@/providers/rpcProvider" import { @@ -92,14 +92,14 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { ? (routes.find((r) => (r.tags as string[] | undefined)?.includes(bridgeProvider), ) ?? - getSupplementalBridgeRoutes( - srcChain.key, - destChain.key, - srcAsset.key, - ).find((r) => - (r.tags as string[] | undefined)?.includes(bridgeProvider), - ) ?? - origin.route) + getSupplementalBridgeRoutes( + srcChain.key, + destChain.key, + srcAsset.key, + ).find((r) => + (r.tags as string[] | undefined)?.includes(bridgeProvider), + ) ?? + origin.route) : origin.route const call = await transfer.buildCall(srcAmount) diff --git a/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts b/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts index 5e9d7af5f..cbda745ce 100644 --- a/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts +++ b/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts @@ -61,5 +61,8 @@ export const getSupplementalBridgeRoutes = ( destChainKey: string, srcAssetKey: string, ): AssetRoute[] => { - return SUPPLEMENTAL_ROUTES.get(`${srcChainKey}-${destChainKey}-${srcAssetKey}`) ?? [] + return ( + SUPPLEMENTAL_ROUTES.get(`${srcChainKey}-${destChainKey}-${srcAssetKey}`) ?? + [] + ) } diff --git a/apps/main/src/modules/xcm/transfer/utils/transfer.ts b/apps/main/src/modules/xcm/transfer/utils/transfer.ts index 38caaa1a3..37311b02c 100644 --- a/apps/main/src/modules/xcm/transfer/utils/transfer.ts +++ b/apps/main/src/modules/xcm/transfer/utils/transfer.ts @@ -95,8 +95,14 @@ export const getXcmTransferArgs = ( account: Account | null, values: XcmFormValues, ): XcmTransferArgs => { - const { srcChain, srcAsset, destChain, destAsset, destAddress, bridgeProvider } = - values + const { + srcChain, + srcAsset, + destChain, + destAsset, + destAddress, + bridgeProvider, + } = values const isValidPair = srcChain && srcAsset ? srcChain.assetsData.values().some((a) => a.asset.key === srcAsset.key) From ce8434f0b7f05972a4cc96d9ba3cf3f9984df498 Mon Sep 17 00:00:00 2001 From: mrq Date: Tue, 31 Mar 2026 04:22:20 +0200 Subject: [PATCH 14/26] merge? --- package.json | 8 ++++---- yarn.lock | 20 ++++---------------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index a803759a2..827eef1fa 100644 --- a/package.json +++ b/package.json @@ -19,13 +19,13 @@ "strip-ansi": "6.0.1" }, "dependencies": { - "@galacticcouncil/common": "^0.5.1", + "@galacticcouncil/common": "^0.6.0", "@galacticcouncil/descriptors": "^1.15.0", - "@galacticcouncil/sdk-next": "^0.37.0", - "@galacticcouncil/xc": "^0.4.0", + "@galacticcouncil/sdk-next": "^0.38.0", + "@galacticcouncil/xc": "^0.5.0", "@galacticcouncil/xc-cfg": "0.19.0-pr304-7025779", "@galacticcouncil/xc-core": "0.14.0-pr304-7025779", - "@galacticcouncil/xc-scan": "^0.3.0", + "@galacticcouncil/xc-scan": "^0.4.0", "@galacticcouncil/xc-sdk": "0.9.2-pr304-7025779", "big.js": "^6.2.2", "date-fns": "^4.1.0", diff --git a/yarn.lock b/yarn.lock index 372a4478b..e3a2061f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2115,10 +2115,10 @@ bs58 "^6.0.0" buffer "^6.0.3" -"@galacticcouncil/xc-scan@^0.3.0": - version "0.3.0" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-scan/-/xc-scan-0.3.0.tgz#662bb011c53f056c230d1d1057d31b94424758f7" - integrity sha512-onH/zcGdA5JQx/tE7fcnhMUwosPnGflkJgcujgKeyuJPL15onvMPPKlDzvvNnUN8clfnefRtmEY9SNPo4md9uw== +"@galacticcouncil/xc-scan@^0.4.0": + version "0.4.0" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-scan/-/xc-scan-0.4.0.tgz#1c12eba5b8d7fd6f6614d79617f73a6f157a20a0" + integrity sha512-RilIVpyvU4Dv4+neAE3+9UtuKNxtoTkS3RDXKOAMK2mAGWbJ2+cmeX6ahZxvXLIOowB2TDssFOFDykLFIcHd0g== "@galacticcouncil/xc-sdk@0.9.2-pr304-7025779": version "0.9.2-pr304-7025779" @@ -2127,18 +2127,6 @@ dependencies: "@galacticcouncil/xc-core" "0.14.0-pr304-7025779" -"@galacticcouncil/xc@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-scan/-/xc-scan-0.4.0.tgz#1c12eba5b8d7fd6f6614d79617f73a6f157a20a0" - integrity sha512-RilIVpyvU4Dv4+neAE3+9UtuKNxtoTkS3RDXKOAMK2mAGWbJ2+cmeX6ahZxvXLIOowB2TDssFOFDykLFIcHd0g== - -"@galacticcouncil/xc-sdk@^0.9.1": - version "0.9.1" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.9.1.tgz#145ba1c3c2458e55b2f2c02c4f7a67c365bc671a" - integrity sha512-upRR4abs7ZrQp1Qphn+zziXbpX2V6UyBffDCn+ekj/WpOJxELfwPbkzh8tfRHlurrwUgvCU1cv9f3fhvjXWt4w== - dependencies: - "@galacticcouncil/xc-core" "^0.13.0" - "@galacticcouncil/xc@^0.5.0": version "0.5.0" resolved "https://registry.yarnpkg.com/@galacticcouncil/xc/-/xc-0.5.0.tgz#67dee6a85d314246a4d2ffaba4e82ffd2337c883" From 52b376b44dffc29f3b85c05e0fd6e207b5e6fb69 Mon Sep 17 00:00:00 2001 From: mrq Date: Wed, 1 Apr 2026 07:10:45 +0200 Subject: [PATCH 15/26] fixed abi --- package.json | 6 +++--- yarn.lock | 28 ++++++++++++++-------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 827eef1fa..d8fb6a6d9 100644 --- a/package.json +++ b/package.json @@ -23,10 +23,10 @@ "@galacticcouncil/descriptors": "^1.15.0", "@galacticcouncil/sdk-next": "^0.38.0", "@galacticcouncil/xc": "^0.5.0", - "@galacticcouncil/xc-cfg": "0.19.0-pr304-7025779", - "@galacticcouncil/xc-core": "0.14.0-pr304-7025779", + "@galacticcouncil/xc-cfg": "0.19.0-pr304-f6c6ae3", + "@galacticcouncil/xc-core": "0.14.0-pr304-f6c6ae3", "@galacticcouncil/xc-scan": "^0.4.0", - "@galacticcouncil/xc-sdk": "0.9.2-pr304-7025779", + "@galacticcouncil/xc-sdk": "0.9.2-pr304-f6c6ae3", "big.js": "^6.2.2", "date-fns": "^4.1.0", "immer": "^10.0.3", diff --git a/yarn.lock b/yarn.lock index e3a2061f1..8ab4a2a28 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2089,17 +2089,17 @@ "@thi.ng/memoize" "^4.0.2" big.js "^6.2.1" -"@galacticcouncil/xc-cfg@0.19.0-pr304-7025779": - version "0.19.0-pr304-7025779" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-cfg/-/xc-cfg-0.19.0-pr304-7025779.tgz#b55b68e8e7b61314e9442a7cc8df5645e768e561" - integrity sha512-knA+azbYk/3oIne8d60ACPqoc+rEwzA+LAixMXCEycxWpmj9HTivGV17y23h622QJsRJ5ggjZ61errvpX5maPA== +"@galacticcouncil/xc-cfg@0.19.0-pr304-f6c6ae3": + version "0.19.0-pr304-f6c6ae3" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-cfg/-/xc-cfg-0.19.0-pr304-f6c6ae3.tgz#0cc192549df835707a1afe28386cfc8b5546d105" + integrity sha512-zIWXLjfiFoSHWuNmGGxhGa3HeaI3+uI2PA8yhEztWNmD4XvWtqyQpN+0D5jXwIffJVIKXK/DX6YOwtFC4na03w== dependencies: - "@galacticcouncil/xc-core" "0.14.0-pr304-7025779" + "@galacticcouncil/xc-core" "0.14.0-pr304-f6c6ae3" -"@galacticcouncil/xc-core@0.14.0-pr304-7025779": - version "0.14.0-pr304-7025779" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-core/-/xc-core-0.14.0-pr304-7025779.tgz#350dac5dfbb6fa002e659fe9b1c489110088b138" - integrity sha512-If652Ag96VDr5NjUs5h80f5pQIbBkC99LNFjNzApRoiMorG3sTNYZr4R/RS2XtakH7Ca1YSBwU9PpLufBOIlpA== +"@galacticcouncil/xc-core@0.14.0-pr304-f6c6ae3": + version "0.14.0-pr304-f6c6ae3" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-core/-/xc-core-0.14.0-pr304-f6c6ae3.tgz#886bea240c9a2b730eb28f0b546ae988148e5c39" + integrity sha512-FLOkdtDdxbRcf5ZhzfKi6a18eqBw3+IDBoCZTUM7nEyx43tno+Rs8AQhPeH2fsK+64dSNfj+fK3YOF2h+TYQdA== dependencies: "@noble/hashes" "^1.6.1" "@wormhole-foundation/sdk-base" "3.2.0" @@ -2120,12 +2120,12 @@ resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-scan/-/xc-scan-0.4.0.tgz#1c12eba5b8d7fd6f6614d79617f73a6f157a20a0" integrity sha512-RilIVpyvU4Dv4+neAE3+9UtuKNxtoTkS3RDXKOAMK2mAGWbJ2+cmeX6ahZxvXLIOowB2TDssFOFDykLFIcHd0g== -"@galacticcouncil/xc-sdk@0.9.2-pr304-7025779": - version "0.9.2-pr304-7025779" - resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.9.2-pr304-7025779.tgz#fe9a404d5c11c20103b94ca3cfcc1ecee1c2ed9f" - integrity sha512-Lt8ykg1669u9+4w4hWbfGQt8rnsQ2UhbzXxGEVOOBi5d2Pvyf74kkhQ5TpTbAMDYnskRAuz710o2XwKyP4j/aw== +"@galacticcouncil/xc-sdk@0.9.2-pr304-f6c6ae3": + version "0.9.2-pr304-f6c6ae3" + resolved "https://registry.yarnpkg.com/@galacticcouncil/xc-sdk/-/xc-sdk-0.9.2-pr304-f6c6ae3.tgz#4b57fe7239f1236ae2998584da36966f3cf6f199" + integrity sha512-n5HlAcrgOf/nh1XBzTpyAXuqU3wHPMIMLZaOqTUkl1iHVMhi5I1aSEYGMg8Eaw55+3fiQHq+Ff6u522M9dSBug== dependencies: - "@galacticcouncil/xc-core" "0.14.0-pr304-7025779" + "@galacticcouncil/xc-core" "0.14.0-pr304-f6c6ae3" "@galacticcouncil/xc@^0.5.0": version "0.5.0" From 0a7807b7446e4cf1724da68b7f1bfa1a35221402 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Von=C3=A1=C5=A1ek?= Date: Thu, 9 Apr 2026 14:36:15 +0200 Subject: [PATCH 16/26] Adjust design --- apps/main/src/api/xcm.ts | 6 +- apps/main/src/i18n/locales/en/xcm.json | 7 +- .../review/ReviewMultiTransaction.tsx | 18 ++++- .../review/ReviewTransactionSummary.tsx | 23 ++++-- .../modules/xcm/history/XcScanJourneyList.tsx | 11 ++- .../BridgeSelector/BridgeSelector.styled.ts | 9 ++- .../BridgeSelector/BridgeSelector.tsx | 75 +++++++++---------- .../ChainAssetSelectButton.tsx | 2 +- .../PendingApproval/PendingApproval.tsx | 20 +++++ .../transfer/hooks/useSubmitXcmTransfer.ts | 15 +++- .../modules/xcm/transfer/utils/transfer.ts | 13 +--- apps/main/src/states/transactions.ts | 8 +- packages/ui/src/assets/icons/JetSki.svg | 8 ++ packages/ui/src/assets/icons/Swimmer.svg | 3 + packages/ui/src/assets/icons/index.ts | 2 + 15 files changed, 136 insertions(+), 84 deletions(-) create mode 100644 apps/main/src/modules/xcm/transfer/components/PendingApproval/PendingApproval.tsx create mode 100644 packages/ui/src/assets/icons/JetSki.svg create mode 100644 packages/ui/src/assets/icons/Swimmer.svg diff --git a/apps/main/src/api/xcm.ts b/apps/main/src/api/xcm.ts index f72386f03..de808008b 100644 --- a/apps/main/src/api/xcm.ts +++ b/apps/main/src/api/xcm.ts @@ -175,11 +175,9 @@ export const xcmTransferQuery = ( if (bridgeTag) { const selectedRoute = - builder.routes.find((r) => - (r.tags as string[] | undefined)?.includes(bridgeTag), - ) ?? + builder.routes.find((r) => r.tags?.includes(bridgeTag)) ?? getSupplementalBridgeRoutes(srcChain, destChain, srcAsset).find((r) => - (r.tags as string[] | undefined)?.includes(bridgeTag), + r.tags?.includes(bridgeTag), ) if (selectedRoute) { diff --git a/apps/main/src/i18n/locales/en/xcm.json b/apps/main/src/i18n/locales/en/xcm.json index db0a556c0..a50831a0a 100644 --- a/apps/main/src/i18n/locales/en/xcm.json +++ b/apps/main/src/i18n/locales/en/xcm.json @@ -4,13 +4,12 @@ "approve.title": "Approve spending cap", "approve.toast.submitted": "Approving {{ amount, number }} {{ symbol }} spending cap on {{ srcChain }}", "approve.toast.success": "Approved {{ amount, number }} {{ symbol }} spending cap on {{ srcChain }}", + "approve.pending.title": "Approval Pending", + "approve.pending.description": "Your approval transaction is being confirmed on the blockchain.", "bridge.wormhole": "Wormhole", "bridge.snowbridge": "Snowbridge", - "bridge.basejump": "Basejump 🪂", + "bridge.basejump": "Basejump", "bridge.selector.label": "Via", - "bridge.provider.basejump": "Basejump 🪂", - "bridge.provider.wormhole": "Wormhole", - "bridge.provider.snowbridge": "Snowbridge", "chainAssetSelect.button.selectAssetChain": "Select asset & chain", "chainAssetSelect.emptyState.noAssets": "No assets found", "chainAssetSelect.modal.title": "Chain & asset", diff --git a/apps/main/src/modules/transactions/review/ReviewMultiTransaction.tsx b/apps/main/src/modules/transactions/review/ReviewMultiTransaction.tsx index 2877e7e3c..e6ab6815f 100644 --- a/apps/main/src/modules/transactions/review/ReviewMultiTransaction.tsx +++ b/apps/main/src/modules/transactions/review/ReviewMultiTransaction.tsx @@ -1,10 +1,11 @@ import { Modal, + ModalBody, ModalFooter, ModalHeader, Stepper, } from "@galacticcouncil/ui/components" -import { useEffect, useState } from "react" +import React, { useEffect, useState } from "react" import { useTranslation } from "react-i18next" import { isFunction, omit } from "remeda" @@ -41,6 +42,7 @@ export const ReviewMultiTransaction: React.FC = ({ const [resolvedTx, setResolvedTx] = useState(null) const [resolvedConfig, setResolvedConfig] = useState(null) + const [isPendingResolution, setIsPendingResolution] = useState(false) const [isLoading, setIsLoading] = useState(false) const [isLastSubmitted, setIsLastSubmitted] = useState(false) const [hasUserClosedModal, setHasUserClosedModal] = useState(false) @@ -63,12 +65,15 @@ export const ReviewMultiTransaction: React.FC = ({ const tx = currentBaseConfig.tx if (isFunction(tx)) { + setIsPendingResolution(true) const previousResults = transactionResults.slice(0, currentIndex) Promise.resolve(tx(previousResults)).then((resolved) => { + setIsPendingResolution(false) setResolvedTx(resolved.tx) setResolvedConfig(omit(resolved, ["tx"])) }) } else { + setIsPendingResolution(false) setResolvedTx(tx) setResolvedConfig(null) } @@ -138,6 +143,9 @@ export const ReviewMultiTransaction: React.FC = ({ const { title, description } = currentConfig + const PendingComponent = + isPendingResolution && currentBaseConfig?.pendingComponent + return ( = ({ title={title ?? t("transaction.title")} description={description ?? t("transaction.description")} /> - + {PendingComponent ? ( + + + + ) : ( + + )} diff --git a/apps/main/src/modules/transactions/review/ReviewTransactionSummary.tsx b/apps/main/src/modules/transactions/review/ReviewTransactionSummary.tsx index 7ee747e40..514517b2c 100644 --- a/apps/main/src/modules/transactions/review/ReviewTransactionSummary.tsx +++ b/apps/main/src/modules/transactions/review/ReviewTransactionSummary.tsx @@ -4,7 +4,7 @@ import { Stack, SummaryRow, } from "@galacticcouncil/ui/components" -import { HYDRATION_CHAIN_KEY } from "@galacticcouncil/utils" +import { HYDRATION_CHAIN_KEY, isValidBigSource } from "@galacticcouncil/utils" import { chainsMap } from "@galacticcouncil/xc-cfg" import { ChainEcosystem } from "@galacticcouncil/xc-core" import Big from "big.js" @@ -70,19 +70,26 @@ const XcmSummary = () => { const srcChain = chainsMap.get(meta.srcChainKey) const isPolkadotEcosystem = srcChain?.ecosystem === ChainEcosystem.Polkadot + return ( } sx={{ mb: "var(--modal-content-inset)" }} > - + {!!meta.srcChainFee && ( + + )} {Big(meta.dstChainFee || "0").gt(0) && ( { {paginatedData.map((journey) => ( ))} + - - ) } diff --git a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts index 321b3ece1..2f24d1419 100644 --- a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts +++ b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.styled.ts @@ -19,9 +19,10 @@ export const SBridgeOption = styled.button<{ active: boolean }>( overflow: hidden; border: 1px solid ${theme.buttons.outlineDark.onOutline}; - border-radius: 8px; + border-radius: ${theme.radii.m}; - padding: 16px 12px; + padding-block: ${theme.space.l}; + padding-inline: ${theme.space.m}; cursor: pointer; @@ -29,8 +30,8 @@ export const SBridgeOption = styled.button<{ active: boolean }>( ${active ? css` - background-color: ${theme.buttons.secondary.outline.fill}; - border-color: ${theme.buttons.secondary.outline.outline}; + background-color: ${theme.controls.dim.active}; + border-color: ${theme.controls.dim.active}; ` : css` &:hover:not(:disabled) { diff --git a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx index f0614bdbc..0a47f383a 100644 --- a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx +++ b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx @@ -1,33 +1,34 @@ -import { Flex, Text } from "@galacticcouncil/ui/components" +import { JetSki, Swimmer } from "@galacticcouncil/ui/assets/icons" +import { Flex, Icon, Text } from "@galacticcouncil/ui/components" import { getToken } from "@galacticcouncil/ui/utils" import { AssetRoute } from "@galacticcouncil/xc-core" import { useFormContext } from "react-hook-form" import { useTranslation } from "react-i18next" +import { isNonNullish } from "remeda" import { XcmFormValues } from "@/modules/xcm/transfer/hooks/useXcmFormSchema" import { getPrimaryBridgeTag } from "@/modules/xcm/transfer/utils/transfer" import { XcmTag } from "@/states/transactions" -import { SBridgeOption, SParticle } from "./BridgeSelector.styled" +import { SBridgeOption } from "./BridgeSelector.styled" -const BRIDGE_TIME_ESTIMATES: Partial> = { - [XcmTag.Basejump]: "~42 sec", - [XcmTag.Wormhole]: "~18 min", - [XcmTag.Snowbridge]: "~25 min", +type BridgeOption = { + id: string + label: string + time: string + icon: React.ComponentType } -// Particle animation durations — ratio matches real-world transfer times -// Base Jumper: 42 sec, Wormhole: 18 min (1080 sec) → ~25.7× slower -const BRIDGE_PARTICLE_DURATION: Partial> = { - [XcmTag.Basejump]: ["0.9s", "0.9s", "0.9s"], // 3 staggered packets = rapid stream - [XcmTag.Wormhole]: ["23s"], - [XcmTag.Snowbridge]: ["36s"], +const BRIDGE_TIME_ESTIMATES: Partial> = { + [XcmTag.Basejump]: "~1 min", + [XcmTag.Wormhole]: "~30 min", + [XcmTag.Snowbridge]: "~25 min", } -const BRIDGE_PARTICLE_COLOR: Partial> = { - [XcmTag.Basejump]: "#4fc4f9", - [XcmTag.Wormhole]: "#a78bfa", - [XcmTag.Snowbridge]: "#60a5fa", +const BRIDGE_ICONS: Partial> = { + [XcmTag.Basejump]: JetSki, + [XcmTag.Wormhole]: Swimmer, + [XcmTag.Snowbridge]: Swimmer, } type BridgeSelectorProps = { @@ -46,18 +47,11 @@ export const BridgeSelector: React.FC = ({ routes }) => { return { id: tag, label: t(`xcm:bridge.provider.${tag.toLowerCase()}`, tag), - time: BRIDGE_TIME_ESTIMATES[tag], - durations: BRIDGE_PARTICLE_DURATION[tag] ?? ["5s"], - color: BRIDGE_PARTICLE_COLOR[tag] ?? "#ffffff", - } + time: BRIDGE_TIME_ESTIMATES[tag] ?? "", + icon: BRIDGE_ICONS[tag] ?? Swimmer, + } satisfies BridgeOption }) - .filter(Boolean) as { - id: string - label: string - time?: string - durations: string[] - color: string - }[] + .filter(isNonNullish) if (options.length < 2) return null @@ -65,7 +59,6 @@ export const BridgeSelector: React.FC = ({ routes }) => { {options.map((option) => { const active = bridgeProvider === option.id - const count = option.durations.length return ( = ({ routes }) => { {option.label} - {option.time && ( - + + {option.time} - )} - {option.durations.map((duration, i) => ( - - ))} + {option.icon && ( + + )} + ) })} diff --git a/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelectButton.tsx b/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelectButton.tsx index 8c1d93d7b..7885b7dd8 100644 --- a/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelectButton.tsx +++ b/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelectButton.tsx @@ -38,7 +38,7 @@ export const ChainAssetSelectButton: React.FC = ({ {currentSelection.asset.originSymbol} diff --git a/apps/main/src/modules/xcm/transfer/components/PendingApproval/PendingApproval.tsx b/apps/main/src/modules/xcm/transfer/components/PendingApproval/PendingApproval.tsx new file mode 100644 index 000000000..016113244 --- /dev/null +++ b/apps/main/src/modules/xcm/transfer/components/PendingApproval/PendingApproval.tsx @@ -0,0 +1,20 @@ +import { Flex, Spinner, Stack, Text } from "@galacticcouncil/ui/components" +import { getToken } from "@galacticcouncil/ui/utils" +import { useTranslation } from "react-i18next" + +export const PendingApproval = () => { + const { t } = useTranslation(["xcm"]) + return ( + + + + + {t("approve.pending.title")} + + + {t("approve.pending.description")} + + + + ) +} diff --git a/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts b/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts index a1279af09..dc6262bcd 100644 --- a/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts +++ b/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts @@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next" import { useCrossChainConfigService } from "@/api/xcm" import { AnyPapiTx } from "@/modules/transactions/types" import { isEvmApproveCall, isEvmCall } from "@/modules/transactions/utils/xcm" +import { PendingApproval } from "@/modules/xcm/transfer/components/PendingApproval/PendingApproval" import { useApprovalTrackingStore } from "@/modules/xcm/transfer/hooks/useApprovalTrackingStore" import { XcmFormValues } from "@/modules/xcm/transfer/hooks/useXcmFormSchema" import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" @@ -118,6 +119,17 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { srcChain.key === HYDRATION_CHAIN_KEY ? await papi.txFromCallData(Binary.fromHex(transferCall.data)) : await getExternalChainTx(srcChain, transferCall) + + const sourceFeeValue = (() => { + if (!source) return "" + if (source.fee.amount === 0n) + return t("xcm:summary.feeEstimationNotAvailable") + return t("common:currency", { + value: toDecimal(source.fee.amount, source.fee.decimals), + symbol: source.fee.originSymbol, + }) + })() + return { title: t("form.title"), description: t("tx.description", i18nVars), @@ -134,7 +146,7 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { meta: { type: TransactionType.Xcm, srcChainKey: srcChain.key, - srcChainFee: toDecimal(source.fee.amount, source.fee.decimals), + srcChainFee: sourceFeeValue, srcChainFeeSymbol: source.fee.symbol, dstChainKey: destChain.key, dstChainFee: toDecimal( @@ -186,6 +198,7 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { }, { stepTitle: t("common:transfer"), + pendingComponent: PendingApproval, tx: buildTransferTransaction, onSubmitted: (txHash: string) => { transferTxHash = txHash diff --git a/apps/main/src/modules/xcm/transfer/utils/transfer.ts b/apps/main/src/modules/xcm/transfer/utils/transfer.ts index 37311b02c..1b3d61130 100644 --- a/apps/main/src/modules/xcm/transfer/utils/transfer.ts +++ b/apps/main/src/modules/xcm/transfer/utils/transfer.ts @@ -16,18 +16,9 @@ import { isEvmApproveCall } from "@/modules/transactions/utils/xcm" import { useApprovalTrackingStore } from "@/modules/xcm/transfer/hooks/useApprovalTrackingStore" import { XcmFormValues } from "@/modules/xcm/transfer/hooks/useXcmFormSchema" import { XcmAlert } from "@/modules/xcm/transfer/hooks/useXcmProvider" -import { XCM_BRIDGE_TAGS, XcmTag, XcmTags } from "@/states/transactions" +import { BRIDGE_PROVIDER_TAGS, XcmTags } from "@/states/transactions" import { toDecimal } from "@/utils/formatting" -/** - * Bridge provider tags in priority order (Basejump first as the faster option). - */ -export const BRIDGE_PROVIDER_TAGS = [ - XcmTag.Basejump, - XcmTag.Wormhole, - XcmTag.Snowbridge, -] as const - /** * Returns the primary bridge provider tag for a given route. */ @@ -88,7 +79,7 @@ export const calculateTransferDestAmount = ( export const isBridgeAssetRoute = (route: AssetRoute | null): boolean => { const tags = (route?.tags ?? []) as XcmTags - return tags.some((tag) => XCM_BRIDGE_TAGS.includes(tag)) + return tags.some((tag) => BRIDGE_PROVIDER_TAGS.includes(tag)) } export const getXcmTransferArgs = ( diff --git a/apps/main/src/states/transactions.ts b/apps/main/src/states/transactions.ts index 34cf02a56..b06fa245f 100644 --- a/apps/main/src/states/transactions.ts +++ b/apps/main/src/states/transactions.ts @@ -2,6 +2,7 @@ import { HYDRATION_CHAIN_KEY, uuid } from "@galacticcouncil/utils" import { SolanaTxStatus } from "@galacticcouncil/web3-connect/src/signers/SolanaSigner" import { SuiTxStatus } from "@galacticcouncil/web3-connect/src/signers/SuiSigner" import { tags } from "@galacticcouncil/xc-cfg" +import { ComponentType } from "react" import { TransactionReceipt } from "viem" import { create } from "zustand" @@ -14,10 +15,10 @@ import { export const XcmTag = tags.Tag export type XcmTags = Array -export const XCM_BRIDGE_TAGS: XcmTags = [ +export const BRIDGE_PROVIDER_TAGS: XcmTags = [ + XcmTag.Basejump, XcmTag.Wormhole, XcmTag.Snowbridge, - XcmTag.Basejump, ] export enum TransactionType { @@ -52,6 +53,7 @@ type MultiTransactionConfig = ( | SingleTransactionInputDynamic ) & { stepTitle: string + pendingComponent?: ComponentType //@TODO consider separate all transaction actions per tx onSubmitted?: (txHash: string) => void } @@ -164,7 +166,7 @@ export const isSubstrateTxResult = ( export const isBridgeTransaction = (meta: TransactionMeta) => { return ( meta.type === TransactionType.Xcm && - meta.tags.some((tag) => XCM_BRIDGE_TAGS.includes(tag)) + meta.tags.some((tag) => BRIDGE_PROVIDER_TAGS.includes(tag)) ) } diff --git a/packages/ui/src/assets/icons/JetSki.svg b/packages/ui/src/assets/icons/JetSki.svg new file mode 100644 index 000000000..b89a09bc0 --- /dev/null +++ b/packages/ui/src/assets/icons/JetSki.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/packages/ui/src/assets/icons/Swimmer.svg b/packages/ui/src/assets/icons/Swimmer.svg new file mode 100644 index 000000000..90fe917f9 --- /dev/null +++ b/packages/ui/src/assets/icons/Swimmer.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/ui/src/assets/icons/index.ts b/packages/ui/src/assets/icons/index.ts index 757631cb5..9a9ce31ed 100644 --- a/packages/ui/src/assets/icons/index.ts +++ b/packages/ui/src/assets/icons/index.ts @@ -14,6 +14,7 @@ export { default as Farm } from "./Farm.svg?react" export { default as HydrationLogo } from "./HydrationLogo.svg?react" export { default as HydrationLogoFull } from "./HydrationLogoFull.svg?react" export { default as IconPlaceholder } from "./IconPlaceholder.svg?react" +export { default as JetSki } from "./JetSki.svg?react" export { default as LiquidityIcon } from "./LiquidityIcon.svg?react" export { default as MenuSlanted } from "./MenuSlanted.svg?react" export { default as PartialFill } from "./PartialFill.svg?react" @@ -29,6 +30,7 @@ export { default as StylizedAdd } from "./StylizedAdd.svg?react" export { default as SubScan } from "./SubScan.svg?react" export { default as SubSquare } from "./SubSquare.svg?react" export { default as SuppliedLiquidityIcon } from "./SuppliedLiquidityIcon.svg?react" +export { default as Swimmer } from "./Swimmer.svg?react" export { default as TriangleAlert } from "./TriangleAlert.svg?react" export { default as TwoColorClock } from "./TwoColorClock.svg?react" export { default as Union } from "./Union.svg?react" From 710937f26c502957e07d46ba5cab8e2051d68bd3 Mon Sep 17 00:00:00 2001 From: Pavol Noha Date: Wed, 15 Apr 2026 16:09:41 +0200 Subject: [PATCH 17/26] remove suplementary routes logic --- apps/main/src/api/xcm.ts | 43 +++--------- .../src/modules/xcm/transfer/XcmProvider.tsx | 67 +++++------------- .../BridgeSelector/BridgeSelector.tsx | 7 ++ .../ChainAssetSelect/ChainAssetSelect.tsx | 1 + .../transfer/hooks/useSubmitXcmTransfer.ts | 22 +----- .../transfer/hooks/useXcmTransferConfigs.ts | 20 +----- .../xcm/transfer/utils/bridge-routes.ts | 68 ------------------- 7 files changed, 36 insertions(+), 192 deletions(-) delete mode 100644 apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts diff --git a/apps/main/src/api/xcm.ts b/apps/main/src/api/xcm.ts index de808008b..47566743e 100644 --- a/apps/main/src/api/xcm.ts +++ b/apps/main/src/api/xcm.ts @@ -1,7 +1,7 @@ import { formatSourceChainAddress } from "@galacticcouncil/utils" import { createXcContext } from "@galacticcouncil/xc" import { chainsMap } from "@galacticcouncil/xc-cfg" -import { AnyChain, AssetAmount, ConfigBuilder } from "@galacticcouncil/xc-core" +import { AnyChain, AssetAmount } from "@galacticcouncil/xc-core" import { Transfer, TransferBuilder, Wallet } from "@galacticcouncil/xc-sdk" import { keepPreviousData, @@ -14,7 +14,6 @@ import { import { secondsToMilliseconds } from "date-fns" import { useEffect, useRef, useState } from "react" -import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { TProviderContext, useRpcProvider } from "@/providers/rpcProvider" export const useCrossChainConfig = () => { @@ -168,42 +167,16 @@ export const xcmTransferQuery = ( bridgeTag, ], queryFn: async () => { - const builder = TransferBuilder(wallet) + return TransferBuilder(wallet) .withAsset(srcAsset) .withSource(srcChain) .withDestination(destChain) - - if (bridgeTag) { - const selectedRoute = - builder.routes.find((r) => r.tags?.includes(bridgeTag)) ?? - getSupplementalBridgeRoutes(srcChain, destChain, srcAsset).find((r) => - r.tags?.includes(bridgeTag), - ) - - if (selectedRoute) { - const configs = ConfigBuilder(wallet.config) - .assets() - .asset(srcAsset) - .source(srcChain) - .destination(destChain) - .build(destAsset) - - return wallet.getTransferData( - { - origin: { chain: configs.origin.chain, route: selectedRoute }, - reverse: configs.reverse, - }, - srcAddress, - destAddress, - ) - } - } - - return builder.build({ - srcAddress: srcAddress, - dstAddress: destAddress, - dstAsset: destAsset, - }) + .build({ + srcAddress, + dstAddress: destAddress, + dstAsset: destAsset, + tag: bridgeTag, + }) }, enabled: !!srcAddress && diff --git a/apps/main/src/modules/xcm/transfer/XcmProvider.tsx b/apps/main/src/modules/xcm/transfer/XcmProvider.tsx index 2d41e647c..70124f27b 100644 --- a/apps/main/src/modules/xcm/transfer/XcmProvider.tsx +++ b/apps/main/src/modules/xcm/transfer/XcmProvider.tsx @@ -21,7 +21,6 @@ import { useXcmForm } from "@/modules/xcm/transfer/hooks/useXcmForm" import { XcmContext } from "@/modules/xcm/transfer/hooks/useXcmProvider" import { useXcmTransfer } from "@/modules/xcm/transfer/hooks/useXcmTransfer" import { useXcmTransferAlerts } from "@/modules/xcm/transfer/hooks/useXcmTransferAlerts" -import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { getChainPriority, isAccountValidOnChain, @@ -78,7 +77,7 @@ export const XcmProvider: React.FC = ({ children }) => { const assetSource = config.asset(asset).source(chain) return assetSource.destinationChains.length > 0 }) - return { chain, routes: [], assets } + return { chain, routes: [], assets, isTagSelect: false } }) }, [config]) @@ -102,68 +101,34 @@ export const XcmProvider: React.FC = ({ children }) => { .map((a) => a.destination.chain) return unique(destChains).map((chain) => { - const { routes } = config + const { routes, destinationAssets, isTagSelect } = config .asset(srcAsset) .source(srcChain) .destination(chain) - // Deduplicate assets - multiple bridge routes may share the same destination asset - const seenKeys = new Set() - const assets = routes - .map((r) => r.destination.asset) - .filter((a) => (seenKeys.has(a.key) ? false : seenKeys.add(a.key))) - - return { chain, routes, assets } + return { chain, routes, assets: destinationAssets, isTagSelect } }) }, [config, srcAsset, srcChain, configService]) - const availableBridgeRoutes = useMemo(() => { - if (!srcChain || !srcAsset || !destChain || !destAsset) return [] - const destPair = destChainAssetPairs.find( - (p) => p.chain.key === destChain.key, - ) - if (!destPair) return [] - - const configRoutes = destPair.routes.filter( - (r) => - r.destination.asset.key === destAsset.key && - getPrimaryBridgeTag(r) !== null, - ) - const existingTags = new Set( - configRoutes.map((r) => getPrimaryBridgeTag(r)), - ) - const supplemental = getSupplementalBridgeRoutes( - srcChain.key, - destChain.key, - srcAsset.key, - ).filter( - (r) => - r.destination.asset.key === destAsset.key && - !existingTags.has(getPrimaryBridgeTag(r)), - ) - return [...configRoutes, ...supplemental] - }, [srcChain, srcAsset, destChain, destAsset, destChainAssetPairs]) + const destPair = destChainAssetPairs.find( + (p) => p.chain.key === destChain?.key, + ) useEffect(() => { - if (availableBridgeRoutes.length <= 1) { - if (bridgeProvider !== null) { - form.setValue("bridgeProvider", null) - } + if (!destPair?.isTagSelect) { + form.setValue("bridgeProvider", null) return } - const isCurrentValid = availableBridgeRoutes.some( - (r) => getPrimaryBridgeTag(r) === bridgeProvider, - ) - if (isCurrentValid) return + if (destPair.routes.some((r) => getPrimaryBridgeTag(r) === bridgeProvider)) + return const defaultRoute = - availableBridgeRoutes.find( - (r) => getPrimaryBridgeTag(r) === XcmTag.Basejump, - ) ?? availableBridgeRoutes[0] - if (!defaultRoute) return - form.setValue("bridgeProvider", getPrimaryBridgeTag(defaultRoute)) - }, [availableBridgeRoutes, bridgeProvider, form]) + destPair.routes.find((r) => getPrimaryBridgeTag(r) === XcmTag.Basejump) ?? + destPair.routes[0] + if (defaultRoute) + form.setValue("bridgeProvider", getPrimaryBridgeTag(defaultRoute)) + }, [destPair, bridgeProvider, form]) useEffect(() => { const validRoutes = pipe( @@ -254,7 +219,7 @@ export const XcmProvider: React.FC = ({ children }) => { isConnectedAccountValid, sourceChainAssetPairs, destChainAssetPairs, - availableBridgeRoutes, + availableBridgeRoutes: destPair?.isTagSelect ? destPair.routes : [], alerts, transfer, call, diff --git a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx index 0a47f383a..168a36da6 100644 --- a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx +++ b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx @@ -19,6 +19,12 @@ type BridgeOption = { icon: React.ComponentType } +const BRIDGE_PRIORITY: Record = { + [XcmTag.Basejump]: 0, + [XcmTag.Wormhole]: 1, + [XcmTag.Snowbridge]: 2, +} + const BRIDGE_TIME_ESTIMATES: Partial> = { [XcmTag.Basejump]: "~1 min", [XcmTag.Wormhole]: "~30 min", @@ -52,6 +58,7 @@ export const BridgeSelector: React.FC = ({ routes }) => { } satisfies BridgeOption }) .filter(isNonNullish) + .sort((a, b) => (BRIDGE_PRIORITY[a.id] ?? 99) - (BRIDGE_PRIORITY[b.id] ?? 99)) if (options.length < 2) return null diff --git a/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelect.tsx b/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelect.tsx index ac8f25c14..4eed14290 100644 --- a/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelect.tsx +++ b/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelect.tsx @@ -32,6 +32,7 @@ export type ChainAssetPair = { chain: AnyChain assets: Asset[] routes: AssetRoute[] + isTagSelect: boolean } export type ChainAssetSelection = { diff --git a/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts b/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts index dc6262bcd..172f5d978 100644 --- a/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts +++ b/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts @@ -17,7 +17,6 @@ import { isEvmApproveCall, isEvmCall } from "@/modules/transactions/utils/xcm" import { PendingApproval } from "@/modules/xcm/transfer/components/PendingApproval/PendingApproval" import { useApprovalTrackingStore } from "@/modules/xcm/transfer/hooks/useApprovalTrackingStore" import { XcmFormValues } from "@/modules/xcm/transfer/hooks/useXcmFormSchema" -import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" import { buildTransferCall } from "@/modules/xcm/transfer/utils/transfer" import { useRpcProvider } from "@/providers/rpcProvider" import { @@ -81,27 +80,12 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { destChain: destChain.name, } - const { routes, build } = ConfigBuilder(configService) + const { origin } = ConfigBuilder(configService) .assets() .asset(srcAsset) .source(srcChain) .destination(destChain) - - const { origin } = build(destAsset) - - const selectedRoute = bridgeProvider - ? (routes.find((r) => - (r.tags as string[] | undefined)?.includes(bridgeProvider), - ) ?? - getSupplementalBridgeRoutes( - srcChain.key, - destChain.key, - srcAsset.key, - ).find((r) => - (r.tags as string[] | undefined)?.includes(bridgeProvider), - ) ?? - origin.route) - : origin.route + .build(destAsset, bridgeProvider ?? undefined) const call = await transfer.buildCall(srcAmount) const isApprove = isEvmApproveCall(call) @@ -154,7 +138,7 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { destination.fee.decimals, ), dstChainFeeSymbol: destination.fee.symbol, - tags: (selectedRoute.tags as XcmTags) || [], + tags: (origin.route.tags as XcmTags) || [], }, } } diff --git a/apps/main/src/modules/xcm/transfer/hooks/useXcmTransferConfigs.ts b/apps/main/src/modules/xcm/transfer/hooks/useXcmTransferConfigs.ts index da8b2fdf1..ad0fa9f95 100644 --- a/apps/main/src/modules/xcm/transfer/hooks/useXcmTransferConfigs.ts +++ b/apps/main/src/modules/xcm/transfer/hooks/useXcmTransferConfigs.ts @@ -6,7 +6,6 @@ import { } from "@galacticcouncil/xc-core" import { useCrossChainConfigService } from "@/api/xcm" -import { getSupplementalBridgeRoutes } from "@/modules/xcm/transfer/utils/bridge-routes" export const useXcmTransferConfigs = ( srcAsset: Asset | null, @@ -39,22 +38,5 @@ export const useXcmTransferConfigs = ( return null } - const configs = build(destAsset) - - if (bridgeProvider) { - const selectedRoute = - routes.find((r) => - (r.tags as string[] | undefined)?.includes(bridgeProvider), - ) ?? - getSupplementalBridgeRoutes( - srcChain.key, - destChain.key, - srcAsset.key, - ).find((r) => (r.tags as string[] | undefined)?.includes(bridgeProvider)) - if (selectedRoute) { - return { ...configs, origin: { ...configs.origin, route: selectedRoute } } - } - } - - return configs + return build(destAsset, bridgeProvider ?? undefined) } diff --git a/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts b/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts deleted file mode 100644 index cbda745ce..000000000 --- a/apps/main/src/modules/xcm/transfer/utils/bridge-routes.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { builders, chainsMap } from "@galacticcouncil/xc-cfg" -import { AssetRoute, EvmParachain } from "@galacticcouncil/xc-core" - -const { ContractBuilder, BalanceBuilder } = builders - -/** - * Supplemental bridge routes for asset pairs where the configService only stores - * one route due to ChainRoutes Map deduplication (keyed by srcAsset-destChain-destAsset). - * - * These are the "lost" routes that are defined in xc-cfg source but overwritten - * at runtime. We reconstruct them using the public contract/balance builders. - * - * Key format: `${srcChainKey}-${destChainKey}-${srcAssetKey}` - */ -const buildSupplementalRoutes = (): Map => { - const base = chainsMap.get("base") - const hydration = chainsMap.get("hydration") - const moonbeam = chainsMap.get("moonbeam") - - if (!base || !hydration || !moonbeam) return new Map() - - const eurc = base.assetsData.get("eurc")?.asset - const eurcMwh = hydration.assetsData.get("eurc_mwh")?.asset - const eth = base.assetsData.get("eth")?.asset - - if (!eurc || !eurcMwh || !eth) return new Map() - - // Base → Hydration EURC via Wormhole+MRL - // This route is overwritten in ChainRoutes by the Basejump route (same map key) - const wormholeRoute = new AssetRoute({ - source: { - asset: eurc, - balance: BalanceBuilder().evm().erc20(), - fee: { asset: eth, balance: BalanceBuilder().evm().native() }, - destinationFee: { asset: eurc, balance: BalanceBuilder().evm().erc20() }, - }, - destination: { - chain: hydration, - asset: eurcMwh, - fee: { amount: 0, asset: eurcMwh }, - }, - contract: ContractBuilder() - .Wormhole() - .TokenBridge() - .transferTokensWithPayload() - .viaMrl({ moonchain: moonbeam as EvmParachain }), - tags: ["Mrl", "Wormhole"], - }) - - return new Map([["base-hydration-eurc", [wormholeRoute]]]) -} - -const SUPPLEMENTAL_ROUTES = buildSupplementalRoutes() - -/** - * Returns bridge routes for a given asset pair that are NOT present in the - * configService (because ChainRoutes overwrites them with a later duplicate key). - */ -export const getSupplementalBridgeRoutes = ( - srcChainKey: string, - destChainKey: string, - srcAssetKey: string, -): AssetRoute[] => { - return ( - SUPPLEMENTAL_ROUTES.get(`${srcChainKey}-${destChainKey}-${srcAssetKey}`) ?? - [] - ) -} From 202078dbc9a04ce1aa64b7199f7a670079ce5cf5 Mon Sep 17 00:00:00 2001 From: Pavol Noha Date: Wed, 15 Apr 2026 17:15:15 +0200 Subject: [PATCH 18/26] shutup lint --- .../xcm/transfer/components/BridgeSelector/BridgeSelector.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx index 168a36da6..afad86bfa 100644 --- a/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx +++ b/apps/main/src/modules/xcm/transfer/components/BridgeSelector/BridgeSelector.tsx @@ -58,7 +58,9 @@ export const BridgeSelector: React.FC = ({ routes }) => { } satisfies BridgeOption }) .filter(isNonNullish) - .sort((a, b) => (BRIDGE_PRIORITY[a.id] ?? 99) - (BRIDGE_PRIORITY[b.id] ?? 99)) + .sort( + (a, b) => (BRIDGE_PRIORITY[a.id] ?? 99) - (BRIDGE_PRIORITY[b.id] ?? 99), + ) if (options.length < 2) return null From 57a50a2b25278089884910716079e5b4b4109f8c Mon Sep 17 00:00:00 2001 From: Pavol Noha Date: Thu, 16 Apr 2026 10:46:28 +0200 Subject: [PATCH 19/26] dest guard --- apps/main/src/api/xcm.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/apps/main/src/api/xcm.ts b/apps/main/src/api/xcm.ts index 47566743e..7e24135ca 100644 --- a/apps/main/src/api/xcm.ts +++ b/apps/main/src/api/xcm.ts @@ -167,16 +167,24 @@ export const xcmTransferQuery = ( bridgeTag, ], queryFn: async () => { - return TransferBuilder(wallet) + const builder = TransferBuilder(wallet) .withAsset(srcAsset) .withSource(srcChain) .withDestination(destChain) - .build({ - srcAddress, - dstAddress: destAddress, - dstAsset: destAsset, - tag: bridgeTag, - }) + + // Do not pass invalid/stale dest asset + const validDstAsset = builder.routes.some( + (r) => r.destination.asset.key === destAsset, + ) + ? destAsset + : undefined + + return builder.build({ + srcAddress, + dstAddress: destAddress, + dstAsset: validDstAsset, + tag: bridgeTag, + }) }, enabled: !!srcAddress && From 7d4d2903705c528ec73b0a21b7448425d0a88f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Von=C3=A1=C5=A1ek?= Date: Thu, 16 Apr 2026 11:56:16 +0200 Subject: [PATCH 20/26] Fix source fee not available after approval --- .../xcm/transfer/hooks/useSubmitXcmTransfer.ts | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts b/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts index 172f5d978..b1e7ed43e 100644 --- a/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts +++ b/apps/main/src/modules/xcm/transfer/hooks/useSubmitXcmTransfer.ts @@ -99,18 +99,19 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { srcAmount, ) + const sourceFee = await transfer.estimateFee(srcAmount) + const tx = srcChain.key === HYDRATION_CHAIN_KEY ? await papi.txFromCallData(Binary.fromHex(transferCall.data)) : await getExternalChainTx(srcChain, transferCall) const sourceFeeValue = (() => { - if (!source) return "" - if (source.fee.amount === 0n) + if (sourceFee.amount === 0n) return t("xcm:summary.feeEstimationNotAvailable") return t("common:currency", { - value: toDecimal(source.fee.amount, source.fee.decimals), - symbol: source.fee.originSymbol, + value: toDecimal(sourceFee.amount, sourceFee.decimals), + symbol: sourceFee.originSymbol, }) })() @@ -124,14 +125,14 @@ export const useSubmitXcmTransfer = (options: XcmTransferOptions = {}) => { success: t("tx.toast.success", i18nVars), }, fee: { - feeAmount: toDecimal(source.fee.amount, source.fee.decimals), - feeSymbol: source.fee.symbol, + feeAmount: toDecimal(sourceFee.amount, sourceFee.decimals), + feeSymbol: sourceFee.symbol, }, meta: { type: TransactionType.Xcm, srcChainKey: srcChain.key, srcChainFee: sourceFeeValue, - srcChainFeeSymbol: source.fee.symbol, + srcChainFeeSymbol: sourceFee.symbol, dstChainKey: destChain.key, dstChainFee: toDecimal( destination.fee.amount, From 33c2aeeea1d1c1a54f7203a7eb40fba8b6d41c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Von=C3=A1=C5=A1ek?= Date: Thu, 16 Apr 2026 12:40:09 +0200 Subject: [PATCH 21/26] Fix react errors --- .../ChainAssetSelect/AssetListItem.styled.ts | 6 +++++- .../ChainAssetSelect/AssetListItem.tsx | 16 ++++++++++++---- .../ChainAssetSelectButton.styled.ts | 4 +++- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/AssetListItem.styled.ts b/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/AssetListItem.styled.ts index 889fe28a3..16166594f 100644 --- a/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/AssetListItem.styled.ts +++ b/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/AssetListItem.styled.ts @@ -1,7 +1,11 @@ import { Flex } from "@galacticcouncil/ui/components" import { css, styled } from "@galacticcouncil/ui/utils" -export const SAssetListItem = styled(Flex)<{ isSelected: boolean }>( +export const SAssetListItem = styled(Flex, { + shouldForwardProp: (prop: string) => prop !== "isSelected", +})<{ + isSelected: boolean +}>( ({ theme, isSelected }) => css` justify-content: space-between; align-items: center; diff --git a/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/AssetListItem.tsx b/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/AssetListItem.tsx index 8cd51c522..4e56991a7 100644 --- a/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/AssetListItem.tsx +++ b/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/AssetListItem.tsx @@ -54,16 +54,23 @@ export const AssetListItem: React.FC = ({ const registryId = registryChain.getBalanceAssetId(asset) const registryAsset = getAsset(registryId.toString()) + const meta = registryAsset + ? { + symbol: registryAsset.symbol, + name: registryAsset.name, + } + : { + symbol: asset.originSymbol, + name: asset.originSymbol, + } + return ( {chain && } - + {route && isBridgeAssetRoute(route) && ( )} @@ -72,6 +79,7 @@ export const AssetListItem: React.FC = ({ 0n ? getToken("text.high") diff --git a/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelectButton.styled.ts b/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelectButton.styled.ts index 0d27e6ecc..c96ecf512 100644 --- a/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelectButton.styled.ts +++ b/apps/main/src/modules/xcm/transfer/components/ChainAssetSelect/ChainAssetSelectButton.styled.ts @@ -2,7 +2,9 @@ import { css } from "@emotion/react" import styled from "@emotion/styled" import { Button } from "@galacticcouncil/ui/components" -export const SButton = styled(Button)<{ +export const SButton = styled(Button, { + shouldForwardProp: (prop: string) => prop !== "hasSelection", +})<{ hasSelection: boolean disabled: boolean }>( From e9fbf5a1fd41529cbafd71ee3c8175dafa9dff5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Von=C3=A1=C5=A1ek?= Date: Fri, 17 Apr 2026 14:56:30 +0200 Subject: [PATCH 22/26] Fix duplicate journeys --- .../src/modules/xcm/history/XcJourneyCard.tsx | 8 ++++--- .../history/XcScanHistoryTable.columns.tsx | 11 +++++++--- .../main/src/modules/xcm/history/useXcScan.ts | 15 ++++++++----- .../modules/xcm/history/utils/optimistic.ts | 21 ++++++++++++++----- packages/utils/src/helpers/xcm.ts | 2 +- 5 files changed, 40 insertions(+), 17 deletions(-) diff --git a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx index e9e43a347..f29655398 100644 --- a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx +++ b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx @@ -15,8 +15,8 @@ import { import { getToken } from "@galacticcouncil/ui/utils" import { shortenAccountAddress, xcscan } from "@galacticcouncil/utils" import type { XcJourney } from "@galacticcouncil/xc-scan" +import Big from "big.js" import { useTranslation } from "react-i18next" -import { isNumber } from "remeda" import { ClaimButton } from "@/modules/xcm/history/components/ClaimButton" import { JourneyAssetLogo } from "@/modules/xcm/history/components/JourneyAssetLogo" @@ -49,6 +49,8 @@ export const XcJourneyCard: React.FC = (journey) => { const isNotPending = !pendingCorrelationIds.includes(journey.correlationId) const isClaimable = isNotPending && isJourneyClaimable(journey) + const usdValue = Big(totalUsd || transferAsset?.usd || 0) + return ( @@ -111,9 +113,9 @@ export const XcJourneyCard: React.FC = (journey) => { symbol: transferAsset.symbol, })} - {isNumber(totalUsd) && totalUsd > 0 && ( + {usdValue.gt(0) && ( - {t("currency", { value: totalUsd })} + {t("currency", { value: usdValue })} )} diff --git a/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx b/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx index 93d6c4158..54c5bcfb2 100644 --- a/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx +++ b/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx @@ -14,6 +14,7 @@ import { } from "@galacticcouncil/utils" import type { XcJourney } from "@galacticcouncil/xc-scan" import { createColumnHelper } from "@tanstack/react-table" +import Big from "big.js" import { useMemo } from "react" import { useTranslation } from "react-i18next" @@ -88,6 +89,8 @@ export const useXcScanHistoryColumns = () => { if (!transferAsset) return null + const usdValue = Big(row.original.totalUsd || transferAsset?.usd || 0) + return ( @@ -103,9 +106,11 @@ export const useXcScanHistoryColumns = () => { }) : t("number", { value: transferAsset.amount })} - - {t("currency", { value: row.original.totalUsd })} - + {usdValue.gt(0) && ( + + {t("currency", { value: usdValue })} + + )} ) diff --git a/apps/main/src/modules/xcm/history/useXcScan.ts b/apps/main/src/modules/xcm/history/useXcScan.ts index f68658bab..838d2fd23 100644 --- a/apps/main/src/modules/xcm/history/useXcScan.ts +++ b/apps/main/src/modules/xcm/history/useXcScan.ts @@ -3,7 +3,7 @@ import { useQuery, useQueryClient } from "@tanstack/react-query" import { useEffect, useState } from "react" import { getClaimableJourneys } from "@/modules/xcm/history/utils/claim" -import { isOptimisticJourney } from "@/modules/xcm/history/utils/optimistic" +import { isOptimisticJourneyForTxHash } from "@/modules/xcm/history/utils/optimistic" import { xcStore } from "./xcScanStore" @@ -57,10 +57,15 @@ export const useXcScanSubscription = (address: string) => { return [journey] } const prev = old.filter((item) => { - const isOptimistic = - isOptimisticJourney(item) && - item.originTxPrimary === journey.originTxPrimary - return !isOptimistic + const isOptimisticPrimary = isOptimisticJourneyForTxHash( + item, + journey.originTxPrimary ?? "", + ) + const isOptimisticSecondary = isOptimisticJourneyForTxHash( + item, + journey.originTxSecondary ?? "", + ) + return !isOptimisticPrimary && !isOptimisticSecondary }) return [journey, ...prev] }) diff --git a/apps/main/src/modules/xcm/history/utils/optimistic.ts b/apps/main/src/modules/xcm/history/utils/optimistic.ts index 7fbbb36fa..a6f92b38f 100644 --- a/apps/main/src/modules/xcm/history/utils/optimistic.ts +++ b/apps/main/src/modules/xcm/history/utils/optimistic.ts @@ -23,6 +23,16 @@ export function isOptimisticJourney(journey: XcJourney): boolean { return journey.correlationId.startsWith(OPTIMISTIC_JOURNEY_PREFIX) } +export function isOptimisticJourneyForTxHash( + journey: XcJourney, + txHash: string, +): boolean { + return ( + isOptimisticJourney(journey) && + (journey.originTxPrimary === txHash || journey.originTxSecondary === txHash) + ) +} + export function chainToUrn(chain: AnyChain): string { const ecosystem = chain.ecosystem if (!ecosystem) return "" @@ -90,8 +100,8 @@ export function insertOptimisticJourney( ) { const queryKey = createXcScanQueryKey(address) const current = queryClient.getQueryData(queryKey) ?? [] - const alreadyExists = current.some( - (j) => isOptimisticJourney(j) && j.originTxPrimary === txHash, + const alreadyExists = current.some((j) => + isOptimisticJourneyForTxHash(j, txHash), ) if (alreadyExists) return const optimisticJourney = convertXcmFormValuesToOptimisticJourney( @@ -100,6 +110,9 @@ export function insertOptimisticJourney( txHash, address, ) + console.log("OPTIMISTIC", optimisticJourney?.correlationId, { + journey: optimisticJourney, + }) if (!optimisticJourney) return queryClient.setQueryData(queryKey, (old) => [ optimisticJourney, @@ -114,8 +127,6 @@ export function removeOptimisticJourney( ) { const queryKey = createXcScanQueryKey(address) queryClient.setQueryData(queryKey, (old) => - (old ?? []).filter( - (j) => !(isOptimisticJourney(j) && j.originTxPrimary === txHash), - ), + (old ?? []).filter((j) => !isOptimisticJourneyForTxHash(j, txHash)), ) } diff --git a/packages/utils/src/helpers/xcm.ts b/packages/utils/src/helpers/xcm.ts index bc467225e..952f5ccd4 100644 --- a/packages/utils/src/helpers/xcm.ts +++ b/packages/utils/src/helpers/xcm.ts @@ -134,5 +134,5 @@ export function formatDestChainAddress( if (chain.key === HYDRATION_CHAIN_KEY && EvmAddr.isValid(address)) { return safeConvertH160toSS58(address) } - return address + return isAddressValidOnChain(address, chain) ? address : "" } From 7be61ad8d972bf65f15f60f1a0a020be37905891 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Von=C3=A1=C5=A1ek?= Date: Tue, 21 Apr 2026 16:57:51 +0200 Subject: [PATCH 23/26] bjscan --- apps/main/src/modules/xcm/XcmPage.tsx | 4 +- .../src/modules/xcm/history/XcJourneyCard.tsx | 33 +++- .../history/XcScanHistoryTable.columns.tsx | 2 +- .../xcm/history/XcScanJourneyListSkeleton.tsx | 8 +- apps/main/src/modules/xcm/history/index.ts | 1 + .../xcm/history/lib/BasejumpScanSseClient.ts | 65 +++++++ .../modules/xcm/history/useBasejumpScan.ts | 116 ++++++++++++ .../main/src/modules/xcm/history/useXcScan.ts | 31 +++- .../src/modules/xcm/history/utils/bjscan.ts | 165 ++++++++++++++++++ .../src/modules/xcm/history/utils/journey.ts | 21 +++ .../modules/xcm/history/utils/optimistic.ts | 11 +- .../modules/xcm/history/utils/protocols.ts | 2 +- apps/main/src/routes/__root.tsx | 6 +- 13 files changed, 447 insertions(+), 18 deletions(-) create mode 100644 apps/main/src/modules/xcm/history/lib/BasejumpScanSseClient.ts create mode 100644 apps/main/src/modules/xcm/history/useBasejumpScan.ts create mode 100644 apps/main/src/modules/xcm/history/utils/bjscan.ts diff --git a/apps/main/src/modules/xcm/XcmPage.tsx b/apps/main/src/modules/xcm/XcmPage.tsx index 114b4fae4..4cc20643e 100644 --- a/apps/main/src/modules/xcm/XcmPage.tsx +++ b/apps/main/src/modules/xcm/XcmPage.tsx @@ -12,9 +12,9 @@ export const XcmPage = () => { const address = account?.address ?? "" const claimable = useClaimableTransactions() - const { data: all, dataUpdatedAt } = useXcScan(address) + const { data: all, isLoading: isLoadingXcScan } = useXcScan(address) - const isLoading = !!account && dataUpdatedAt === 0 + const isLoading = !!account && isLoadingXcScan const isTwoColTemplate = !!account && (all.length > 0 || isLoading) return ( diff --git a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx index 2648dc877..baf3b2263 100644 --- a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx +++ b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx @@ -1,5 +1,6 @@ import { ArrowRight, + JetSki, QuestionCircleRegular, } from "@galacticcouncil/ui/assets/icons" import { @@ -13,7 +14,7 @@ import { Text, } from "@galacticcouncil/ui/components" import { getToken } from "@galacticcouncil/ui/utils" -import { xcscan } from "@galacticcouncil/utils" +import { etherscan, xcscan } from "@galacticcouncil/utils" import type { XcJourney } from "@galacticcouncil/xc-scan" import Big from "big.js" import { useTranslation } from "react-i18next" @@ -23,6 +24,7 @@ import { ClaimButton } from "@/modules/xcm/history/components/ClaimButton" import { JourneyAssetLogo } from "@/modules/xcm/history/components/JourneyAssetLogo" import { JourneyChainLogo } from "@/modules/xcm/history/components/JourneyChainLogo" import { JourneyDate } from "@/modules/xcm/history/components/JourneyDate" +import { JourneyProtocol } from "@/modules/xcm/history/components/JourneyProtocol" import { JourneyStatus } from "@/modules/xcm/history/components/JourneyStatus" import { usePendingClaimsStore } from "@/modules/xcm/history/hooks/usePendingClaimsStore" import { @@ -35,8 +37,16 @@ import { isOptimisticJourney } from "@/modules/xcm/history/utils/optimistic" import { toDecimal } from "@/utils/formatting" export const XcJourneyCard: React.FC = (journey) => { - const { origin, destination, sentAt, correlationId, status, totalUsd } = - journey + const { + origin, + destination, + sentAt, + correlationId, + status, + totalUsd, + originProtocol, + originTxPrimary, + } = journey const { t } = useTranslation(["common", "xcm"]) const { pendingCorrelationIds } = usePendingClaimsStore() @@ -45,7 +55,10 @@ export const XcJourneyCard: React.FC = (journey) => { const transferAsset = getTransferAsset(journey) const { from, to } = getFormattedAddresses(journey) - const link = xcscan.tx(correlationId) + const link = + originProtocol === "basejump" && originTxPrimary + ? etherscan.tx("base", originTxPrimary) + : xcscan.tx(correlationId) const isNotPending = !pendingCorrelationIds.includes(journey.correlationId) const isClaimable = isNotPending && isJourneyClaimable(journey) @@ -88,7 +101,17 @@ export const XcJourneyCard: React.FC = (journey) => { {sentAt && ( - + + {originProtocol === "basejump" && ( + + + + + )} { const durationMs = recvAt - sentAt - if (durationMs < 0) { + if (durationMs <= 0) { return null } diff --git a/apps/main/src/modules/xcm/history/XcScanJourneyListSkeleton.tsx b/apps/main/src/modules/xcm/history/XcScanJourneyListSkeleton.tsx index eb75d44c4..eed82d611 100644 --- a/apps/main/src/modules/xcm/history/XcScanJourneyListSkeleton.tsx +++ b/apps/main/src/modules/xcm/history/XcScanJourneyListSkeleton.tsx @@ -49,7 +49,13 @@ const JourneyCardSkeleton = () => { export const XcScanJourneyListSkeleton = () => { return ( - + {Array.from({ length: 4 }, (_, i) => ( ))} diff --git a/apps/main/src/modules/xcm/history/index.ts b/apps/main/src/modules/xcm/history/index.ts index 6f1bd6cf1..411cae4d6 100644 --- a/apps/main/src/modules/xcm/history/index.ts +++ b/apps/main/src/modules/xcm/history/index.ts @@ -1,3 +1,4 @@ +export { useBasejumpScanSubscription } from "./useBasejumpScan" export { useXcScanSubscription } from "./useXcScan" export { XcScanHistory } from "./XcScanHistory" export { xcStore } from "./xcScanStore" diff --git a/apps/main/src/modules/xcm/history/lib/BasejumpScanSseClient.ts b/apps/main/src/modules/xcm/history/lib/BasejumpScanSseClient.ts new file mode 100644 index 000000000..4ddcccb98 --- /dev/null +++ b/apps/main/src/modules/xcm/history/lib/BasejumpScanSseClient.ts @@ -0,0 +1,65 @@ +import { + createQueryString, + safeConvertAnyToH160, + safeParse, +} from "@galacticcouncil/utils" + +import { + type BasejumpScanItem, + basejumpSseEventSchema, +} from "@/modules/xcm/history/utils/bjscan" + +export const BJSCAN_API_ORIGIN = "https://bjscan-api.play.hydration.cloud" + +export type BasejumpScanSubscribeOptions = { + onCreate: (transfer: BasejumpScanItem) => void + onUpdate: (transfer: BasejumpScanItem) => void +} + +export class BasejumpScanSseClient { + private _baseUrl: string + private eventSource: EventSource | null = null + private onMessage: ((event: MessageEvent) => void) | null = null + + constructor(baseUrl: string) { + this._baseUrl = baseUrl + } + + subscribe(address: string, options: BasejumpScanSubscribeOptions): void { + this.unsubscribe() + + const h160 = safeConvertAnyToH160(address) + const url = `${this._baseUrl}/api/events${createQueryString({ address: h160 })}` + const eventSource = new EventSource(url) + this.eventSource = eventSource + + this.onMessage = (event: MessageEvent) => { + const data = safeParse(event.data) + const parsed = basejumpSseEventSchema.safeParse(data) + if (!parsed.success) return + + const { kind, transfer } = parsed.data + if (kind === "created") { + options.onCreate(transfer) + } + + if (kind === "updated") { + options.onUpdate(transfer) + } + } + + eventSource.addEventListener("created", this.onMessage) + eventSource.addEventListener("updated", this.onMessage) + } + + unsubscribe(): void { + if (!this.eventSource) return + if (this.onMessage) { + this.eventSource.removeEventListener("created", this.onMessage) + this.eventSource.removeEventListener("updated", this.onMessage) + } + this.eventSource.close() + this.eventSource = null + this.onMessage = null + } +} diff --git a/apps/main/src/modules/xcm/history/useBasejumpScan.ts b/apps/main/src/modules/xcm/history/useBasejumpScan.ts new file mode 100644 index 000000000..c50cf8d1b --- /dev/null +++ b/apps/main/src/modules/xcm/history/useBasejumpScan.ts @@ -0,0 +1,116 @@ +import { createQueryString, safeConvertAnyToH160 } from "@galacticcouncil/utils" +import type { XcJourney } from "@galacticcouncil/xc-scan" +import { useQuery, useQueryClient } from "@tanstack/react-query" +import { useEffect } from "react" +import { isNonNullish, sortBy } from "remeda" + +import { BasejumpScanSseClient } from "@/modules/xcm/history/lib/BasejumpScanSseClient" +import { + basejumpItemToXcJourney, + basejumpScanSchema, +} from "@/modules/xcm/history/utils/bjscan" +import { removeOptimisticJourney } from "@/modules/xcm/history/utils/optimistic" + +const BJSCAN_API_BASE_URL = "https://bjscan-api.play.hydration.cloud" +const BJSCAN_TRANSFERS_URL = `${BJSCAN_API_BASE_URL}/api/transfers` + +export const bjscan = new BasejumpScanSseClient(BJSCAN_API_BASE_URL) + +export const createBasejumpScanQueryKey = (address: string) => [ + "bjscan", + address, +] + +export const useBasejumpScan = (address: string) => { + return useQuery({ + queryKey: createBasejumpScanQueryKey(address), + queryFn: async () => { + const res = await fetch( + `${BJSCAN_TRANSFERS_URL}${createQueryString({ + address: safeConvertAnyToH160(address), + })}`, + ) + + if (!res.ok) { + throw new Error( + `BasejumpScan API error: ${res.status} ${res.statusText}`, + ) + } + + const data = await res.json() + const parsed = basejumpScanSchema.parse(data) + return parsed.items.map(basejumpItemToXcJourney).filter(isNonNullish) + }, + enabled: !!address, + staleTime: Infinity, + refetchOnWindowFocus: false, + retry: false, + }) +} + +const journeyDate = (j: XcJourney) => j.sentAt ?? j.createdAt ?? 0 + +function upsertBasejumpJourneyInCache( + prev: XcJourney[] | undefined, + journey: XcJourney, +): XcJourney[] { + const list = prev ?? [] + const filtered = list.filter( + (j) => + j.originTxPrimary !== journey.originTxPrimary && + j.correlationId !== journey.correlationId, + ) + return sortBy([...filtered, journey], [journeyDate, "desc"]) +} + +export const useBasejumpScanSubscription = (address: string) => { + const queryClient = useQueryClient() + + useEffect(() => { + if (!address) return + + bjscan.subscribe(address, { + onCreate(transfer) { + const journey = basejumpItemToXcJourney(transfer) + if (!journey) return + + const queryKey = createBasejumpScanQueryKey(address) + if (transfer.initiated?.txHash) { + removeOptimisticJourney( + queryClient, + address, + transfer.initiated.txHash, + ) + } + queryClient.setQueryData(queryKey, (old) => + upsertBasejumpJourneyInCache(old, journey), + ) + }, + onUpdate(transfer) { + const journey = basejumpItemToXcJourney(transfer) + if (!journey) return + + const queryKey = createBasejumpScanQueryKey(address) + queryClient.setQueryData(queryKey, (old) => { + const prev = old ?? [] + + const exists = prev.some( + (j) => j.correlationId === journey.correlationId, + ) + + if (!exists) { + return upsertBasejumpJourneyInCache(prev, journey) + } + + return prev.map((j) => + j.correlationId === journey.correlationId ? journey : j, + ) + }) + }, + }) + + return () => { + bjscan.unsubscribe() + } + }, [address, queryClient]) +} diff --git a/apps/main/src/modules/xcm/history/useXcScan.ts b/apps/main/src/modules/xcm/history/useXcScan.ts index 10b774724..192bacdfa 100644 --- a/apps/main/src/modules/xcm/history/useXcScan.ts +++ b/apps/main/src/modules/xcm/history/useXcScan.ts @@ -1,10 +1,12 @@ import type { XcJourney } from "@galacticcouncil/xc-scan" import { useQuery, useQueryClient } from "@tanstack/react-query" -import { useEffect, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { getClaimableJourneys } from "@/modules/xcm/history/utils/claim" +import { mergeJourneys } from "@/modules/xcm/history/utils/journey" import { isOptimisticJourneyForTxHash } from "@/modules/xcm/history/utils/optimistic" +import { useBasejumpScan } from "./useBasejumpScan" import { xcStore } from "./xcScanStore" export const createXcScanQueryKey = (address: string) => ["xcscan", address] @@ -16,7 +18,7 @@ type XcScanOptions = { export const useXcScan = (address: string, options: XcScanOptions = {}) => { const { claimableOnly } = options - return useQuery({ + const xcscan = useQuery({ queryKey: createXcScanQueryKey(address), enabled: !!address, staleTime: Infinity, @@ -26,6 +28,31 @@ export const useXcScan = (address: string, options: XcScanOptions = {}) => { select: claimableOnly ? getClaimableJourneys : undefined, queryFn: () => [], }) + + const bjscan = useBasejumpScan(address) + + const isLoadingXcScan = xcscan.dataUpdatedAt === 0 + const isLoading = claimableOnly + ? isLoadingXcScan + : isLoadingXcScan || bjscan.isLoading + + const data = useMemo(() => { + if (claimableOnly) return xcscan.data + if (bjscan.isSuccess && xcscan.isSuccess) + return mergeJourneys(bjscan.data, xcscan.data) + return xcscan.data + }, [ + bjscan.data, + bjscan.isSuccess, + claimableOnly, + xcscan.data, + xcscan.isSuccess, + ]) + + return { + isLoading, + data, + } } export const useXcScanSubscription = (address: string) => { diff --git a/apps/main/src/modules/xcm/history/utils/bjscan.ts b/apps/main/src/modules/xcm/history/utils/bjscan.ts new file mode 100644 index 000000000..5d7fc2260 --- /dev/null +++ b/apps/main/src/modules/xcm/history/utils/bjscan.ts @@ -0,0 +1,165 @@ +import { HYDRATION_CHAIN_KEY, stringEquals } from "@galacticcouncil/utils" +import { chainsMap } from "@galacticcouncil/xc-cfg" +import { AnyChain, Asset } from "@galacticcouncil/xc-core" +import type { XcJourney } from "@galacticcouncil/xc-scan" +import { isNumber } from "remeda" +import z from "zod" + +import { chainToUrn } from "@/modules/xcm/history/utils/optimistic" + +export const basejumpScanEventSchema = z.object({ + chain: z.string(), + txHash: z.string(), + logIndex: z.number(), + blockNumber: z.string(), + blockTimestamp: z.number(), +}) + +export const basejumpScanItemSchema = z.object({ + id: z.string(), + state: z.string(), + source_asset: z.string().nullable(), + source_chain: z.string().nullable(), + dest_asset: z.string().nullable(), + dest_chain: z.string().nullable(), + dest_chain_id: z.number().nullable(), + sender: z.string(), + recipient: z.string(), + gross_amount: z.string(), + fee: z.string(), + net_amount: z.string(), + transfer_sequence: z.string(), + message_sequence: z.string(), + pending_id: z.unknown().nullable(), + initiated: basejumpScanEventSchema.nullable(), + completed: basejumpScanEventSchema.nullable(), + fulfilled: basejumpScanEventSchema.nullable(), + queued: basejumpScanEventSchema.nullable(), + updated_at: z.string(), +}) + +export const basejumpScanSchema = z.object({ + items: z.array(basejumpScanItemSchema), + total: z.number(), +}) + +export const basejumpSseEventSchema = z.discriminatedUnion("kind", [ + z.object({ + kind: z.literal("created"), + transfer: basejumpScanItemSchema, + }), + z.object({ + kind: z.literal("updated"), + transfer: basejumpScanItemSchema, + }), +]) + +export type BasejumpScanEvent = z.infer +export type BasejumpScanItem = z.infer +export type BasejumpScan = z.infer +export type BasejumpSseEvent = z.infer + +export function resolveChain(chainKey: string | null): AnyChain | undefined { + if (!chainKey) return + return chainsMap.get(chainKey) +} + +export function resolveBasejumpAsset( + chainKey: string | null, + assetId: string | null, +): { asset: Asset; decimals: number; id: string } | undefined { + if (!chainKey || !assetId) return undefined + const chain = resolveChain(chainKey) + if (!chain) return undefined + + const asset = Array.from(chain.assetsData.values()).find( + (entry) => !!entry.id && stringEquals(entry.id.toString(), assetId), + ) + + if (!asset || !asset.id || !isNumber(asset.decimals)) return undefined + return { + asset: asset.asset, + decimals: asset.decimals, + id: asset.id.toString(), + } +} + +export function mapBasejumpState(state: string): string { + return state === "completed" ? "received" : "sent" +} + +export function parseBasejumpId(id: string): { + correlationId: string + id: number +} { + const tail = id.split("-").at(-1) + const parsed = Number(tail) + return { + correlationId: id, + id: Number.isNaN(parsed) ? 0 : parsed, + } +} + +export function parseTimestamp(item: BasejumpScanItem): number { + return ( + item?.initiated?.blockTimestamp || + item?.completed?.blockTimestamp || + Date.parse(item.updated_at) + ) +} + +export function basejumpItemToXcJourney( + item: BasejumpScanItem, +): XcJourney | undefined { + const sourceChain = resolveChain(item.source_chain) + const destChain = resolveChain(item.dest_chain || HYDRATION_CHAIN_KEY) + if (!sourceChain || !destChain) return undefined + + const resolvedAsset = resolveBasejumpAsset( + item.source_chain, + item.source_asset, + ) + if (!resolvedAsset) return undefined + + const originTxPrimary = item.initiated?.txHash + if (!originTxPrimary) return undefined + + const { id, correlationId } = parseBasejumpId(item.id) + const originUrn = chainToUrn(sourceChain) + const destinationUrn = chainToUrn(destChain) + const timestamp = parseTimestamp(item) + + return { + id, + correlationId, + status: mapBasejumpState(item.state), + type: "transfer", + originProtocol: "basejump", + destinationProtocol: "basejump", + origin: originUrn, + destination: destinationUrn, + from: item.sender, + fromFormatted: item.sender, + to: item.recipient, + toFormatted: item.recipient, + sentAt: timestamp, + createdAt: timestamp, + recvAt: item.completed ? timestamp : undefined, + stops: "", + instructions: "", + transactCalls: "", + originTxPrimary, + destinationTxPrimary: item.completed?.txHash, + totalUsd: 0, + assets: [ + { + asset: `${originUrn}|${resolvedAsset.id}`, + symbol: resolvedAsset.asset.originSymbol, + amount: item.gross_amount, + decimals: resolvedAsset.decimals, + role: "transfer", + sequence: 0, + }, + ], + } satisfies XcJourney +} diff --git a/apps/main/src/modules/xcm/history/utils/journey.ts b/apps/main/src/modules/xcm/history/utils/journey.ts index 2a359add6..2f45270ce 100644 --- a/apps/main/src/modules/xcm/history/utils/journey.ts +++ b/apps/main/src/modules/xcm/history/utils/journey.ts @@ -7,6 +7,7 @@ import { SpinnerIcon } from "@galacticcouncil/ui/components" import { ThemeToken } from "@galacticcouncil/ui/theme" import { isH160Address } from "@galacticcouncil/utils" import { XcJourney } from "@galacticcouncil/xc-scan" +import { isNonNullish, sortBy } from "remeda" export type TJourneyStatus = XcJourney["status"] @@ -94,3 +95,23 @@ export function getFormattedAddresses(journey: XcJourney) { return { from, to } } + +const journeyDate = (j: XcJourney) => j.sentAt ?? j.createdAt ?? 0 + +export function mergeJourneys( + existing: XcJourney[], + incoming: XcJourney[], +): XcJourney[] { + const seen = new Set( + existing.map((j) => j.originTxPrimary).filter(isNonNullish), + ) + const filtered = incoming.filter( + (j) => !j.originTxPrimary || !seen.has(j.originTxPrimary), + ) + + if (filtered.length === 0) { + return existing + } + + return sortBy([...existing, ...filtered], [journeyDate, "desc"]) +} diff --git a/apps/main/src/modules/xcm/history/utils/optimistic.ts b/apps/main/src/modules/xcm/history/utils/optimistic.ts index a6f92b38f..b4d45d3a9 100644 --- a/apps/main/src/modules/xcm/history/utils/optimistic.ts +++ b/apps/main/src/modules/xcm/history/utils/optimistic.ts @@ -11,6 +11,7 @@ import type { QueryClient } from "@tanstack/react-query" import { createXcScanQueryKey } from "@/modules/xcm/history/useXcScan" import type { XcmFormValues } from "@/modules/xcm/transfer/hooks/useXcmFormSchema" +import { XcmTag } from "@/states/transactions" import { scale } from "@/utils/formatting" const OPTIMISTIC_JOURNEY_PREFIX = "optimistic:" @@ -59,13 +60,16 @@ export function convertXcmFormValuesToOptimisticJourney( ? safeConvertSS58toH160(fromAddress) : fromAddress + const protocol = + values.bridgeProvider === XcmTag.Basejump ? "basejump" : "xcm" + return { id: 0, correlationId: getOptimisticJourneyId(txHash), status: "pending", type: "transfer", - originProtocol: "xcm", - destinationProtocol: "xcm", + originProtocol: protocol, + destinationProtocol: protocol, origin: originUrn, destination: destinationUrn, from, @@ -110,9 +114,6 @@ export function insertOptimisticJourney( txHash, address, ) - console.log("OPTIMISTIC", optimisticJourney?.correlationId, { - journey: optimisticJourney, - }) if (!optimisticJourney) return queryClient.setQueryData(queryKey, (old) => [ optimisticJourney, diff --git a/apps/main/src/modules/xcm/history/utils/protocols.ts b/apps/main/src/modules/xcm/history/utils/protocols.ts index 27027cf07..ab36b6736 100644 --- a/apps/main/src/modules/xcm/history/utils/protocols.ts +++ b/apps/main/src/modules/xcm/history/utils/protocols.ts @@ -3,7 +3,7 @@ import { ThemeToken } from "@galacticcouncil/ui/theme" const XC_SCAN_PROTOCOLS: Record = { basejump: { - label: "Basejump 🪂", + label: "Basejump", color: "colors.skyBlue.600", }, xcm: { diff --git a/apps/main/src/routes/__root.tsx b/apps/main/src/routes/__root.tsx index d5c3a5180..dd4ab031c 100644 --- a/apps/main/src/routes/__root.tsx +++ b/apps/main/src/routes/__root.tsx @@ -16,7 +16,10 @@ import { DataProviderSelect } from "@/components/DataProviderSelect/DataProvider import { LayoutSkeleton } from "@/modules/layout/components/LayoutSkeleton" import { useHasTopNavbar } from "@/modules/layout/hooks/useHasTopNavbar" import { MainLayout } from "@/modules/layout/MainLayout" -import { useXcScanSubscription } from "@/modules/xcm/history" +import { + useBasejumpScanSubscription, + useXcScanSubscription, +} from "@/modules/xcm/history" import { AssetsProvider } from "@/providers/assetsProvider" import { MultisigProvider } from "@/providers/MultisigProvider" import { RpcProvider, useRpcProvider } from "@/providers/rpcProvider" @@ -97,6 +100,7 @@ function ApiSubscriptions() { function AccountSubscriptions({ account }: { account: Account }) { useXcScanSubscription(account.address) + useBasejumpScanSubscription(account.address) return null } From 2ee7de618483f55026c5d09f5ce219d8eff1fac3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Von=C3=A1=C5=A1ek?= Date: Tue, 21 Apr 2026 17:50:50 +0200 Subject: [PATCH 24/26] Adjust claimable journey window --- .../src/modules/xcm/history/utils/claim.ts | 25 +++++++++++++------ 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/apps/main/src/modules/xcm/history/utils/claim.ts b/apps/main/src/modules/xcm/history/utils/claim.ts index a323b26f9..d0868a5ed 100644 --- a/apps/main/src/modules/xcm/history/utils/claim.ts +++ b/apps/main/src/modules/xcm/history/utils/claim.ts @@ -28,7 +28,13 @@ import { SuiCall, SuiClaim, } from "@galacticcouncil/xc-sdk" -import { minutesToMilliseconds } from "date-fns" +import { + addMilliseconds, + fromUnixTime, + hoursToMilliseconds, + isWithinInterval, + minutesToMilliseconds, +} from "date-fns" import { isString } from "remeda" import { @@ -40,13 +46,16 @@ import { XcJourneyWhStop, } from "@/modules/xcm/history/utils/journey" -const CLAIM_THRESHOLD = minutesToMilliseconds(5) +const CLAIM_MIN_AGE_MS = minutesToMilliseconds(5) // 5 minutes +const CLAIM_MAX_AGE_MS = hoursToMilliseconds(24) * 7 * 2 // 2 weeks + +function isWithinClaimWindow(emittedAtSeconds: number) { + const emittedAt = fromUnixTime(emittedAtSeconds) -function hasExceededClaimThreshold(emittedAt: number) { - const now = Date.now() - const emittedAtMs = emittedAt * 1000 - const deadline = emittedAtMs + CLAIM_THRESHOLD - return now >= deadline + return isWithinInterval(new Date(), { + start: addMilliseconds(emittedAt, CLAIM_MIN_AGE_MS), + end: addMilliseconds(emittedAt, CLAIM_MAX_AGE_MS), + }) } export function isJourneyClaimable(journey: XcJourney): boolean { @@ -59,7 +68,7 @@ export function isJourneyClaimable(journey: XcJourney): boolean { const asset = getTransferAsset(journey) if (!asset) return false - return hasExceededClaimThreshold(vaaHeader.timestamp) + return isWithinClaimWindow(vaaHeader.timestamp) } export function getClaimableJourneys(journeys: XcJourney[]) { From 7e02dc62fb1bb02b278dada6aeb82344ae6741cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Von=C3=A1=C5=A1ek?= Date: Tue, 21 Apr 2026 18:52:39 +0200 Subject: [PATCH 25/26] Basejump scan link --- .../src/modules/xcm/history/XcJourneyCard.tsx | 7 +++---- .../xcm/history/XcScanHistoryTable.columns.tsx | 12 +++++++----- .../xcm/history/lib/BasejumpScanSseClient.ts | 2 -- .../src/modules/xcm/history/useBasejumpScan.ts | 13 +++++++------ .../src/modules/xcm/history/utils/bjscan.ts | 17 ++++------------- packages/utils/src/helpers/basejumpscan.ts | 12 ++++++++++++ packages/utils/src/helpers/index.ts | 1 + 7 files changed, 34 insertions(+), 30 deletions(-) create mode 100644 packages/utils/src/helpers/basejumpscan.ts diff --git a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx index baf3b2263..bbf8115a3 100644 --- a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx +++ b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx @@ -14,7 +14,7 @@ import { Text, } from "@galacticcouncil/ui/components" import { getToken } from "@galacticcouncil/ui/utils" -import { etherscan, xcscan } from "@galacticcouncil/utils" +import { basejumpscan, xcscan } from "@galacticcouncil/utils" import type { XcJourney } from "@galacticcouncil/xc-scan" import Big from "big.js" import { useTranslation } from "react-i18next" @@ -45,7 +45,6 @@ export const XcJourneyCard: React.FC = (journey) => { status, totalUsd, originProtocol, - originTxPrimary, } = journey const { t } = useTranslation(["common", "xcm"]) const { pendingCorrelationIds } = usePendingClaimsStore() @@ -56,8 +55,8 @@ export const XcJourneyCard: React.FC = (journey) => { const { from, to } = getFormattedAddresses(journey) const link = - originProtocol === "basejump" && originTxPrimary - ? etherscan.tx("base", originTxPrimary) + originProtocol === "basejump" + ? basejumpscan.tx(correlationId) : xcscan.tx(correlationId) const isNotPending = !pendingCorrelationIds.includes(journey.correlationId) diff --git a/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx b/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx index ba1d28032..aecdbf5fb 100644 --- a/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx +++ b/apps/main/src/modules/xcm/history/XcScanHistoryTable.columns.tsx @@ -7,7 +7,7 @@ import { Text, } from "@galacticcouncil/ui/components" import { getToken } from "@galacticcouncil/ui/utils" -import { stringEquals, xcscan } from "@galacticcouncil/utils" +import { basejumpscan, stringEquals, xcscan } from "@galacticcouncil/utils" import type { XcJourney } from "@galacticcouncil/xc-scan" import { createColumnHelper } from "@tanstack/react-table" import Big from "big.js" @@ -203,11 +203,13 @@ export const useXcScanHistoryColumns = () => { const actionColumn = columnHelper.display({ id: XcScanHistoryTableColumnId.Action, cell: ({ row }) => { - const link = xcscan.tx(row.original.correlationId) + const { correlationId, originProtocol } = row.original + const link = + originProtocol === "basejump" + ? basejumpscan.tx(correlationId) + : xcscan.tx(correlationId) - const isNotPending = !pendingCorrelationIds.includes( - row.original.correlationId, - ) + const isNotPending = !pendingCorrelationIds.includes(correlationId) const isClaimable = isNotPending && isJourneyClaimable(row.original) return ( diff --git a/apps/main/src/modules/xcm/history/lib/BasejumpScanSseClient.ts b/apps/main/src/modules/xcm/history/lib/BasejumpScanSseClient.ts index 4ddcccb98..7ea24ce77 100644 --- a/apps/main/src/modules/xcm/history/lib/BasejumpScanSseClient.ts +++ b/apps/main/src/modules/xcm/history/lib/BasejumpScanSseClient.ts @@ -9,8 +9,6 @@ import { basejumpSseEventSchema, } from "@/modules/xcm/history/utils/bjscan" -export const BJSCAN_API_ORIGIN = "https://bjscan-api.play.hydration.cloud" - export type BasejumpScanSubscribeOptions = { onCreate: (transfer: BasejumpScanItem) => void onUpdate: (transfer: BasejumpScanItem) => void diff --git a/apps/main/src/modules/xcm/history/useBasejumpScan.ts b/apps/main/src/modules/xcm/history/useBasejumpScan.ts index c50cf8d1b..ebe613b83 100644 --- a/apps/main/src/modules/xcm/history/useBasejumpScan.ts +++ b/apps/main/src/modules/xcm/history/useBasejumpScan.ts @@ -1,4 +1,8 @@ -import { createQueryString, safeConvertAnyToH160 } from "@galacticcouncil/utils" +import { + basejumpscan, + createQueryString, + safeConvertAnyToH160, +} from "@galacticcouncil/utils" import type { XcJourney } from "@galacticcouncil/xc-scan" import { useQuery, useQueryClient } from "@tanstack/react-query" import { useEffect } from "react" @@ -11,10 +15,7 @@ import { } from "@/modules/xcm/history/utils/bjscan" import { removeOptimisticJourney } from "@/modules/xcm/history/utils/optimistic" -const BJSCAN_API_BASE_URL = "https://bjscan-api.play.hydration.cloud" -const BJSCAN_TRANSFERS_URL = `${BJSCAN_API_BASE_URL}/api/transfers` - -export const bjscan = new BasejumpScanSseClient(BJSCAN_API_BASE_URL) +export const bjscan = new BasejumpScanSseClient(basejumpscan.baseUrl) export const createBasejumpScanQueryKey = (address: string) => [ "bjscan", @@ -26,7 +27,7 @@ export const useBasejumpScan = (address: string) => { queryKey: createBasejumpScanQueryKey(address), queryFn: async () => { const res = await fetch( - `${BJSCAN_TRANSFERS_URL}${createQueryString({ + `${basejumpscan.transfers}${createQueryString({ address: safeConvertAnyToH160(address), })}`, ) diff --git a/apps/main/src/modules/xcm/history/utils/bjscan.ts b/apps/main/src/modules/xcm/history/utils/bjscan.ts index 5d7fc2260..df8ed44c8 100644 --- a/apps/main/src/modules/xcm/history/utils/bjscan.ts +++ b/apps/main/src/modules/xcm/history/utils/bjscan.ts @@ -100,14 +100,6 @@ export function parseBasejumpId(id: string): { } } -export function parseTimestamp(item: BasejumpScanItem): number { - return ( - item?.initiated?.blockTimestamp || - item?.completed?.blockTimestamp || - Date.parse(item.updated_at) - ) -} - export function basejumpItemToXcJourney( item: BasejumpScanItem, ): XcJourney | undefined { @@ -127,7 +119,6 @@ export function basejumpItemToXcJourney( const { id, correlationId } = parseBasejumpId(item.id) const originUrn = chainToUrn(sourceChain) const destinationUrn = chainToUrn(destChain) - const timestamp = parseTimestamp(item) return { id, @@ -142,9 +133,9 @@ export function basejumpItemToXcJourney( fromFormatted: item.sender, to: item.recipient, toFormatted: item.recipient, - sentAt: timestamp, - createdAt: timestamp, - recvAt: item.completed ? timestamp : undefined, + sentAt: item?.initiated?.blockTimestamp, + createdAt: item?.initiated?.blockTimestamp || Date.parse(item.updated_at), + recvAt: item?.completed?.blockTimestamp, stops: "", instructions: "", transactCalls: "", @@ -155,7 +146,7 @@ export function basejumpItemToXcJourney( { asset: `${originUrn}|${resolvedAsset.id}`, symbol: resolvedAsset.asset.originSymbol, - amount: item.gross_amount, + amount: item.net_amount, decimals: resolvedAsset.decimals, role: "transfer", sequence: 0, diff --git a/packages/utils/src/helpers/basejumpscan.ts b/packages/utils/src/helpers/basejumpscan.ts new file mode 100644 index 000000000..34f0370ed --- /dev/null +++ b/packages/utils/src/helpers/basejumpscan.ts @@ -0,0 +1,12 @@ +const BASEJUMPSCAN_URL = "https://bjscan-api.play.hydration.cloud" + +export const basejumpscan = { + baseUrl: BASEJUMPSCAN_URL, + transfers: `${BASEJUMPSCAN_URL}/api/transfers`, + link: (data: string | number): string => { + return `${BASEJUMPSCAN_URL}/${data}` + }, + tx: (id: string) => { + return basejumpscan.link(id) + }, +} diff --git a/packages/utils/src/helpers/index.ts b/packages/utils/src/helpers/index.ts index 5f6f1dbad..b1cbfbec2 100644 --- a/packages/utils/src/helpers/index.ts +++ b/packages/utils/src/helpers/index.ts @@ -1,5 +1,6 @@ export * from "./address" export * from "./array" +export * from "./basejumpscan" export * from "./big" export * from "./device" export * from "./evm" From 2ff60f161630c7f9c903127e8268c04343027cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Von=C3=A1=C5=A1ek?= Date: Tue, 21 Apr 2026 19:52:10 +0200 Subject: [PATCH 26/26] Ignore xcscan basejump entry --- .../src/modules/xcm/history/XcJourneyCard.tsx | 9 ++++++++- .../xcm/history/lib/BasejumpScanSseClient.ts | 2 +- .../modules/xcm/history/useBasejumpScan.ts | 4 ++-- .../main/src/modules/xcm/history/useXcScan.ts | 9 ++++++++- .../history/utils/{bjscan.ts => basejump.ts} | 0 .../modules/xcm/history/utils/optimistic.ts | 20 +++++++++++++++++++ 6 files changed, 39 insertions(+), 5 deletions(-) rename apps/main/src/modules/xcm/history/utils/{bjscan.ts => basejump.ts} (100%) diff --git a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx index bbf8115a3..d8f6adda5 100644 --- a/apps/main/src/modules/xcm/history/XcJourneyCard.tsx +++ b/apps/main/src/modules/xcm/history/XcJourneyCard.tsx @@ -100,7 +100,13 @@ export const XcJourneyCard: React.FC = (journey) => { {sentAt && ( - + {originProtocol === "basejump" && ( = (journey) => { )} void diff --git a/apps/main/src/modules/xcm/history/useBasejumpScan.ts b/apps/main/src/modules/xcm/history/useBasejumpScan.ts index ebe613b83..4c6cfb064 100644 --- a/apps/main/src/modules/xcm/history/useBasejumpScan.ts +++ b/apps/main/src/modules/xcm/history/useBasejumpScan.ts @@ -12,13 +12,13 @@ import { BasejumpScanSseClient } from "@/modules/xcm/history/lib/BasejumpScanSse import { basejumpItemToXcJourney, basejumpScanSchema, -} from "@/modules/xcm/history/utils/bjscan" +} from "@/modules/xcm/history/utils/basejump" import { removeOptimisticJourney } from "@/modules/xcm/history/utils/optimistic" export const bjscan = new BasejumpScanSseClient(basejumpscan.baseUrl) export const createBasejumpScanQueryKey = (address: string) => [ - "bjscan", + "basejumpscan", address, ] diff --git a/apps/main/src/modules/xcm/history/useXcScan.ts b/apps/main/src/modules/xcm/history/useXcScan.ts index 192bacdfa..e0e57f7f8 100644 --- a/apps/main/src/modules/xcm/history/useXcScan.ts +++ b/apps/main/src/modules/xcm/history/useXcScan.ts @@ -4,7 +4,10 @@ import { useEffect, useMemo, useState } from "react" import { getClaimableJourneys } from "@/modules/xcm/history/utils/claim" import { mergeJourneys } from "@/modules/xcm/history/utils/journey" -import { isOptimisticJourneyForTxHash } from "@/modules/xcm/history/utils/optimistic" +import { + isOptimisticJourneyForTxHash, + shouldIgnoreNewJourney, +} from "@/modules/xcm/history/utils/optimistic" import { useBasejumpScan } from "./useBasejumpScan" import { xcStore } from "./xcScanStore" @@ -84,6 +87,10 @@ export const useXcScanSubscription = (address: string) => { if (!old) { return [journey] } + + if (shouldIgnoreNewJourney(old, journey)) { + return old + } const prev = old.filter((item) => { const isOptimisticPrimary = isOptimisticJourneyForTxHash( item, diff --git a/apps/main/src/modules/xcm/history/utils/bjscan.ts b/apps/main/src/modules/xcm/history/utils/basejump.ts similarity index 100% rename from apps/main/src/modules/xcm/history/utils/bjscan.ts rename to apps/main/src/modules/xcm/history/utils/basejump.ts diff --git a/apps/main/src/modules/xcm/history/utils/optimistic.ts b/apps/main/src/modules/xcm/history/utils/optimistic.ts index b4d45d3a9..37d7c0aef 100644 --- a/apps/main/src/modules/xcm/history/utils/optimistic.ts +++ b/apps/main/src/modules/xcm/history/utils/optimistic.ts @@ -34,6 +34,26 @@ export function isOptimisticJourneyForTxHash( ) } +export function shouldIgnoreNewJourney( + previous: XcJourney[], + incoming: XcJourney, +): boolean { + return previous.some((journey) => { + const isOptimisticPrimary = isOptimisticJourneyForTxHash( + journey, + incoming.originTxPrimary ?? "", + ) + const isOptimisticSecondary = isOptimisticJourneyForTxHash( + journey, + incoming.originTxSecondary ?? "", + ) + return ( + journey.originProtocol === "basejump" && + (isOptimisticPrimary || isOptimisticSecondary) + ) + }) +} + export function chainToUrn(chain: AnyChain): string { const ecosystem = chain.ecosystem if (!ecosystem) return ""