feat(wallet): external tx-explorer fallback with consent + On-chain settings tab

Pruned nodes can't run the Mempool app, but tx links blindly opened it
anyway. Now: local app when running; otherwise an external explorer
(default tx1138.com) behind a one-time amber consent modal that spells
out what the other server's operator learns (tx of interest + IP) and
lets the user point at their own instance (placeholder mempool.guide).
Wallet Settings gains an On-chain tab (explorer URL + don't-warn toggle);
tabs renamed Cashu/Fedi so five fit in the row.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
archipelago
2026-07-22 17:31:48 -04:00
co-authored by Claude Fable 5
parent 5e7e928650
commit c0aef2f03e
6 changed files with 241 additions and 9 deletions
@@ -0,0 +1,71 @@
<template>
<BaseModal
:show="!!explorer.pendingTx.value"
title="Open on an external explorer?"
max-width="max-w-md"
@close="explorer.cancelPending()"
>
<!-- Same visual language as the uninstall keep-your-data warning: amber
caution card, plain words about exactly what is shared, explicit
choice never a silent hand-off to a third-party server. -->
<div class="p-3 rounded-lg border border-amber-400/25 bg-amber-500/10 text-amber-200/90 text-sm">
<p class="font-medium mb-1"> This node doesn't run its own Mempool explorer</p>
<p class="text-amber-200/75 text-xs leading-relaxed">
(Pruned Bitcoin nodes can't index the full chain.) Your transaction will open on
<span class="font-medium">another node's mempool</span> — that server's operator can see
which transaction you looked up, along with your IP address. Use a server you trust,
or your own mempool instance if you have one elsewhere.
</p>
</div>
<div class="mt-4">
<label class="block text-sm text-white/80 mb-1">Explorer to use</label>
<input
v-model="url"
:placeholder="EXPLORER_PLACEHOLDER"
spellcheck="false"
class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm font-mono focus:outline-none focus:border-orange-400/60"
/>
<p class="text-[11px] text-white/40 mt-1">
Defaults to tx1138.com. Any Mempool-compatible instance works you can change this
any time in Settings System.
</p>
</div>
<label class="flex items-center gap-2 mt-4 text-sm text-white/70 cursor-pointer select-none">
<input type="checkbox" v-model="dontAskAgain" class="accent-orange-400" />
Remember my choice and don't ask again
</label>
<div class="flex gap-2 mt-6">
<button class="flex-1 glass-button px-4 py-2 rounded-lg text-sm" @click="explorer.cancelPending()">
Cancel
</button>
<button
class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium"
@click="explorer.confirmPending(url, dontAskAgain)"
>
Open Explorer
</button>
</div>
</BaseModal>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
import BaseModal from '@/components/BaseModal.vue'
import { useTxExplorer, EXPLORER_PLACEHOLDER } from '@/composables/useTxExplorer'
const explorer = useTxExplorer()
const url = ref(explorer.prefs.value.url)
const dontAskAgain = ref(false)
// Re-seed the input each time the modal opens (prefs may have changed in
// Settings since the last time).
watch(explorer.pendingTx, (tx) => {
if (tx) {
url.value = explorer.prefs.value.url
dontAskAgain.value = false
}
})
</script>
@@ -100,7 +100,7 @@
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import BaseModal from '@/components/BaseModal.vue'
import { useAppLauncherStore } from '@/stores/appLauncher'
import { useTxExplorer } from '@/composables/useTxExplorer'
interface WalletTransaction {
tx_hash: string
@@ -167,10 +167,12 @@ function kindLabel(tx: WalletTransaction): string {
return ''
}
const txExplorer = useTxExplorer()
function openInMempool(tx: WalletTransaction) {
if (!isOnchain(tx)) return
// Overlay the explorer above the current page — never navigate away.
useAppLauncherStore().openSession('mempool', { path: `/tx/${tx.tx_hash}` })
// Local Mempool app when running (overlaid above this modal); external
// explorer with consent otherwise (pruned nodes can't run Mempool).
txExplorer.openTx(tx.tx_hash)
}
function formatTxTime(timestamp: number): string {
@@ -149,6 +149,41 @@
</p>
</div>
<!-- ===================== On-chain ===================== -->
<div v-show="activeTab === 'onchain'">
<p class="text-white/60 text-sm mb-4">
Where "view transaction" links open. When the Mempool app is installed and running
on this node, it is always used — private, no third parties. Without it (pruned
Bitcoin nodes can't run Mempool), links open on the explorer below.
</p>
<label class="block text-sm text-white/80 mb-1">Transaction explorer</label>
<input
v-model="explorerUrlInput"
:placeholder="EXPLORER_PLACEHOLDER"
spellcheck="false"
class="w-full rounded-lg bg-white/[0.06] border border-white/10 text-white px-3 py-2 text-sm font-mono focus:outline-none focus:border-orange-400/60"
@change="saveExplorer"
/>
<p class="text-[11px] text-white/40 mt-1">
Any Mempool-compatible instance works. Default: tx1138.com.
</p>
<div class="mt-3 p-3 rounded-lg border border-amber-400/25 bg-amber-500/10 text-amber-200/80 text-xs leading-relaxed">
Opening a transaction on an external explorer tells that server's operator which
transaction you're interested in, plus your IP address. Use a server you trust.
</div>
<label class="flex items-center gap-2 mt-4 text-sm text-white/70 cursor-pointer select-none">
<input type="checkbox" v-model="explorerAcknowledged" class="accent-orange-400" @change="saveExplorer" />
Don't warn me each time before opening the external explorer
</label>
<div class="flex gap-3 mt-6">
<button @click="close" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">{{ t('common.close') }}</button>
</div>
</div>
<!-- ===================== Ark ===================== -->
<div v-show="activeTab === 'ark'">
<div class="flex items-start gap-2 mb-4">
@@ -253,24 +288,38 @@ import { useI18n } from 'vue-i18n'
import { rpcClient } from '@/api/rpc-client'
import BaseModal from '@/components/BaseModal.vue'
import LightningChannelsPanel from '@/components/LightningChannelsPanel.vue'
import { useTxExplorer, EXPLORER_PLACEHOLDER } from '@/composables/useTxExplorer'
const { t } = useI18n()
const props = defineProps<{ show: boolean }>()
const emit = defineEmits<{ close: []; changed: [] }>()
// Short labels on purpose — five tabs share one row ("Fedi", not
// "Fedimint Federations") so On-chain fits.
const tabs = [
{ key: 'channels' as const, label: 'Channels' },
{ key: 'cashu' as const, label: 'Cashu Mints' },
{ key: 'fedimint' as const, label: 'Fedimint Federations' },
{ key: 'cashu' as const, label: 'Cashu' },
{ key: 'fedimint' as const, label: 'Fedi' },
{ key: 'ark' as const, label: 'Ark' },
{ key: 'onchain' as const, label: 'On-chain' },
]
const activeTab = ref<'channels' | 'cashu' | 'fedimint' | 'ark'>('channels')
const activeTab = ref<'channels' | 'cashu' | 'fedimint' | 'ark' | 'onchain'>('channels')
// Backed by wallet.fedimint-list / -join / -leave (fedimint-clientd HTTP bridge).
// Join degrades gracefully with a clear error if the Fedimint client app isn't installed.
const fedimintBackendReady = true
// ---- On-chain: external transaction explorer ----
const txExplorer = useTxExplorer()
const explorerUrlInput = ref(txExplorer.prefs.value.url)
const explorerAcknowledged = ref(txExplorer.prefs.value.acknowledged)
function saveExplorer() {
txExplorer.setExplorer(explorerUrlInput.value, explorerAcknowledged.value)
// Reflect normalization (empty input falls back to the default).
explorerUrlInput.value = txExplorer.prefs.value.url
}
// ---- Cashu mints ----
const mints = ref<string[]>([])
const newMint = ref('')