From 7fa4936d39e8c6ace41af4e625eb15ed559be8f3 Mon Sep 17 00:00:00 2001 From: digitalSloth Date: Sun, 13 Sep 2026 11:09:30 +0100 Subject: [PATCH] feat: apply node/chain deep-link params with user confirmation A URL can carry node/chainId/networkId query params so a link can point the wallet at a specific network (e.g. a testnet). The wallet boots normally on its current network, then shows a confirmation dialog naming the exact node and chain/network IDs a link is requesting nothing is applied until the user explicitly approves. On confirm, the dialog stays open with a disabled "Connecting..." state while the change is verified via the existing changeNode/ updateNetworkConfig path (the same one the network settings UI uses), which checks reachability before persisting. On failure the dialog shows the error inline with a retry option, and the chain/network id is rolled back to its previous value so a failed switch never leaves the wallet on its old node paired with a mismatched chain id. Declining or cancelling leaves the current network untouched. --- src/App.vue | 131 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.ts | 8 +++- 2 files changed, 138 insertions(+), 1 deletion(-) 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') +})