diff --git a/src/App.vue b/src/App.vue index 084c1df..5203345 100644 --- a/src/App.vue +++ b/src/App.vue @@ -119,6 +119,100 @@ watch( } ) +// A deep-linked node/chainId/networkId must never apply silently — the URL is +// an untrusted source, so surface exactly what's being requested and only +// apply it once the user explicitly approves (see confirmDeepLink/cancelDeepLink). +interface PendingDeepLink { + node: string | null + chainId: number | null + networkId: number | null +} +const pendingDeepLink = ref(null) +const showDeepLinkConfirm = ref(false) +const isConnectingDeepLink = ref(false) +const deepLinkError = ref(null) + +watch( + () => route.query, + (query) => { + const { node: dlNode, chainId: dlChainId, networkId: dlNetworkId } = query + const node = typeof dlNode === 'string' && /^wss?:\/\/.+/.test(dlNode) ? dlNode : null + const chainId = typeof dlChainId === 'string' ? parseInt(dlChainId, 10) : NaN + const networkId = typeof dlNetworkId === 'string' ? parseInt(dlNetworkId, 10) : NaN + + if ( + node || + (Number.isInteger(chainId) && chainId > 0) || + (Number.isInteger(networkId) && networkId > 0) + ) { + pendingDeepLink.value = { + node, + chainId: Number.isInteger(chainId) && chainId > 0 ? chainId : null, + networkId: Number.isInteger(networkId) && networkId > 0 ? networkId : null, + } + deepLinkError.value = null + showDeepLinkConfirm.value = true + + const cleanQuery = { ...query } + delete cleanQuery.node + delete cleanQuery.chainId + delete cleanQuery.networkId + router.replace({ path: route.path, query: cleanQuery }) + } + }, + { immediate: true } +) + +async function confirmDeepLink() { + const request = pendingDeepLink.value + if (!request || isConnectingDeepLink.value) return + + isConnectingDeepLink.value = true + deepLinkError.value = null + const previousChainId = network.chainId.value + const previousNetworkId = network.networkId.value + try { + if (request.chainId !== null || request.networkId !== null) { + await network.updateNetworkConfig( + request.chainId ?? network.chainId.value, + request.networkId ?? network.networkId.value + ) + } + if (request.node) { + await network.changeNode(request.node) + } + // Only close and clear the request once the node/chain is verified reachable + // and actually applied — closing beforehand would show success and then + // silently revert moments later if the check fails. + showDeepLinkConfirm.value = false + pendingDeepLink.value = null + toast.show('Network updated.', 'success') + } catch (error) { + // changeNode reverts to the previous node itself on failure, but + // updateNetworkConfig has no reachability check and already persisted — + // roll the chain/network id back too, so a failed node switch never leaves + // the wallet on its old (working) node paired with a new, mismatched chain id. + if (request.chainId !== null || request.networkId !== null) { + try { + await network.updateNetworkConfig(previousChainId, previousNetworkId) + } catch { + // best-effort — the original error below is what the user needs to see + } + } + deepLinkError.value = + error instanceof Error ? error.message : 'Failed to apply the requested network.' + } finally { + isConnectingDeepLink.value = false + } +} + +function cancelDeepLink() { + if (isConnectingDeepLink.value) return + showDeepLinkConfirm.value = false + pendingDeepLink.value = null + deepLinkError.value = null +} + // The router guard redirects a locked user off a protected route to "/" with // ?unlock=. Open the unlock dialog for that target, then clear the query. watch( @@ -442,6 +536,43 @@ async function handleWalletAdded(address: string) { + + + + + + Connect to a different network? + + A link is asking this wallet to connect to the following network. Only continue if you + trust where this link came from. + + +
+
+ Node: {{ pendingDeepLink.node }} +
+
+ Chain ID: + {{ pendingDeepLink?.chainId ?? network.chainId.value }} +
+
+ Network ID: + {{ pendingDeepLink?.networkId ?? network.networkId.value }} +
+
+

+ {{ deepLinkError }} +

+ + + + +
+
diff --git a/src/main.ts b/src/main.ts index 442c5f9..224713b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -26,4 +26,10 @@ app.config.errorHandler = (err, _instance, info) => { } app.use(router) -app.mount('#app') + +// Wait for the initial navigation to resolve before mounting, so App.vue's +// onMounted sees the URL's query params (e.g. deep-linked node/chainId) via +// route.query instead of racing router's async initial resolution. +router.isReady().then(() => { + app.mount('#app') +})