- F25: Split Web5.vue (3940 lines) into 14 files under views/web5/ - F26: Split Mesh.vue (2106→840 lines) extracting Bitcoin and Deadman panels - F27: Dashboard.vue assessed — layout shell, no split needed - F28: Split Settings.vue (1792 lines) into AccountSection + SystemSection Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
520 lines
22 KiB
Vue
520 lines
22 KiB
Vue
<template>
|
|
<!-- Unified Send Modal -->
|
|
<Teleport to="body">
|
|
<div v-if="showUnifiedSendModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="closeUnifiedSendModal" @keydown.escape="closeUnifiedSendModal">
|
|
<div class="glass-card p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="send-bitcoin-title">
|
|
<h2 id="send-bitcoin-title" class="text-lg font-bold text-white mb-4">{{ t('web5.sendBitcoinTitle') }}</h2>
|
|
|
|
<!-- Method tabs -->
|
|
<div class="flex gap-1 mb-4 p-1 bg-white/5 rounded-lg">
|
|
<button
|
|
v-for="m in (['auto', 'lightning', 'onchain', 'ecash'] as const)"
|
|
:key="m"
|
|
@click="sendMethod = m"
|
|
class="flex-1 px-2 py-1.5 rounded text-xs font-medium capitalize transition-colors"
|
|
:class="sendMethod === m ? 'bg-white/15 text-white' : 'text-white/50 hover:text-white/80'"
|
|
>{{ m === 'onchain' ? 'On-chain' : m }}</button>
|
|
</div>
|
|
|
|
<div v-if="sendMethod === 'auto'" class="mb-3 p-2 bg-white/5 rounded-lg">
|
|
<p class="text-xs text-white/50">Auto-selects method based on amount: ecash < 1k sats, Lightning 1k-500k, on-chain > 500k</p>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="text-white/60 text-sm block mb-1">Amount (sats)</label>
|
|
<input v-model.number="unifiedSendAmount" type="number" min="1" placeholder="1000" class="w-full input-glass" />
|
|
</div>
|
|
|
|
<div v-if="effectiveSendMethod !== 'ecash'" class="mb-3">
|
|
<label class="text-white/60 text-sm block mb-1">
|
|
{{ effectiveSendMethod === 'lightning' ? 'Lightning Invoice (BOLT11)' : 'Bitcoin Address' }}
|
|
</label>
|
|
<textarea v-model="unifiedSendDest" rows="2" :placeholder="effectiveSendMethod === 'lightning' ? 'lnbc...' : 'bc1...'" class="w-full input-glass font-mono"></textarea>
|
|
</div>
|
|
|
|
<div v-if="ecashSendToken && effectiveSendMethod === 'ecash'" class="mb-3 p-2 bg-white/5 rounded-lg">
|
|
<p class="text-white/50 text-xs mb-1">Token (share with recipient):</p>
|
|
<p class="text-xs font-mono text-white/80 break-all">{{ ecashSendToken }}</p>
|
|
<button @click="copyEcashToken(ecashSendToken)" class="mt-2 text-xs text-orange-400 hover:text-orange-300">Copy</button>
|
|
</div>
|
|
|
|
<div v-if="effectiveSendMethod === 'onchain'" class="mb-3 flex items-center gap-3 p-3 bg-white/5 rounded-lg">
|
|
<label class="relative inline-flex items-center cursor-pointer">
|
|
<input type="checkbox" v-model="useHardwareWallet" class="sr-only peer" />
|
|
<div class="w-9 h-5 bg-white/10 peer-focus:outline-none rounded-full peer peer-checked:bg-orange-500/40 transition-colors after:content-[''] after:absolute after:top-0.5 after:left-[2px] after:bg-white after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:after:translate-x-full"></div>
|
|
</label>
|
|
<div>
|
|
<p class="text-sm text-white">{{ t('web5.signWithHwWallet') }}</p>
|
|
<p class="text-xs text-white/40">{{ t('web5.createsPsbt') }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="psbtStep === 'created' && psbtData" class="mb-3 space-y-2">
|
|
<div class="p-3 bg-white/5 rounded-lg">
|
|
<p class="text-xs text-white/50 mb-1">Unsigned PSBT (copy or download):</p>
|
|
<textarea readonly :value="psbtData" rows="3" class="w-full bg-black/20 border border-white/10 rounded px-2 py-1 text-xs font-mono text-white/80 focus:outline-none"></textarea>
|
|
<div class="flex gap-2 mt-2">
|
|
<button @click="copyPsbt" class="text-xs text-orange-400 hover:text-orange-300">Copy PSBT</button>
|
|
<button @click="downloadPsbt" class="text-xs text-orange-400 hover:text-orange-300">Download .psbt</button>
|
|
</div>
|
|
</div>
|
|
<div class="p-3 bg-white/5 rounded-lg">
|
|
<p class="text-xs text-white/50 mb-1">Paste signed PSBT or upload file:</p>
|
|
<textarea v-model="signedPsbtInput" rows="3" placeholder="Paste signed PSBT base64 here..." class="w-full bg-black/20 border border-white/10 rounded px-2 py-1 text-xs font-mono text-white/80 focus:outline-none focus:border-white/30"></textarea>
|
|
<div class="flex gap-2 mt-2">
|
|
<label class="text-xs text-orange-400 hover:text-orange-300 cursor-pointer">
|
|
Upload .psbt
|
|
<input type="file" accept=".psbt,.txt" class="hidden" @change="handlePsbtFileUpload" />
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="showMeshRelayPrompt" class="mb-3 alert-warning">
|
|
<div class="flex items-center gap-2 mb-2">
|
|
<span class="text-lg">📡</span>
|
|
<p class="text-orange-300 text-sm font-medium">You are offline</p>
|
|
</div>
|
|
<p class="text-white/70 text-xs mb-3">Send this transaction via mesh radio? It will be relayed by the nearest internet-connected node and you'll receive confirmation updates.</p>
|
|
<div class="flex gap-2">
|
|
<button @click="dismissMeshRelayPrompt" class="flex-1 glass-button px-3 py-2 rounded-lg text-xs">Cancel</button>
|
|
<button @click="handleMeshRelaySend" class="flex-1 glass-button glass-button-warning px-3 py-2 rounded-lg text-xs font-medium">Send via Mesh</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="meshRelayActive" class="mb-3 alert-warning">
|
|
<div class="flex items-center gap-2 mb-1">
|
|
<svg class="animate-spin h-3 w-3 text-orange-400" fill="none" viewBox="0 0 24 24">
|
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
|
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
|
</svg>
|
|
<p class="text-orange-300 text-xs font-medium">Mesh Relay</p>
|
|
</div>
|
|
<p class="text-white/60 text-xs">{{ meshRelayStatus }}</p>
|
|
</div>
|
|
|
|
<div v-if="sendResultTxid" class="mb-3 alert-success"><p class="text-xs">Sent! TX: {{ sendResultTxid }}</p></div>
|
|
<div v-if="sendResultHash" class="mb-3 alert-success"><p class="text-xs">Paid! Hash: {{ sendResultHash }}</p></div>
|
|
<div v-if="unifiedSendError" class="mb-3 text-xs text-red-400">{{ unifiedSendError }}</div>
|
|
|
|
<div class="flex gap-3">
|
|
<button @click="closeUnifiedSendModal" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">{{ t('common.close') }}</button>
|
|
<button v-if="psbtStep === 'created'" @click="finalizePsbt" :disabled="unifiedSendProcessing || !signedPsbtInput.trim()" class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50">
|
|
{{ unifiedSendProcessing ? 'Broadcasting...' : 'Broadcast' }}
|
|
</button>
|
|
<button v-else @click="unifiedSend" :disabled="unifiedSendProcessing || !unifiedSendAmount" class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50">
|
|
{{ unifiedSendProcessing ? 'Sending...' : (useHardwareWallet && effectiveSendMethod === 'onchain' ? 'Create PSBT' : 'Send') }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
|
|
<!-- Unified Receive Modal -->
|
|
<Teleport to="body">
|
|
<div v-if="showUnifiedReceiveModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="closeUnifiedReceiveModal" @keydown.escape="closeUnifiedReceiveModal">
|
|
<div class="glass-card p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto" role="dialog" aria-modal="true" aria-labelledby="receive-bitcoin-title">
|
|
<h2 id="receive-bitcoin-title" class="text-lg font-bold text-white mb-4">{{ t('web5.receiveBitcoinTitle') }}</h2>
|
|
|
|
<div class="flex gap-1 mb-4 p-1 bg-white/5 rounded-lg">
|
|
<button
|
|
v-for="m in (['onchain', 'lightning', 'ecash'] as const)"
|
|
:key="m"
|
|
@click="receiveMethod = m"
|
|
class="flex-1 px-2 py-1.5 rounded text-xs font-medium capitalize transition-colors"
|
|
:class="receiveMethod === m ? 'bg-white/15 text-white' : 'text-white/50 hover:text-white/80'"
|
|
>{{ m === 'onchain' ? 'On-chain' : m }}</button>
|
|
</div>
|
|
|
|
<div v-if="receiveMethod === 'lightning'">
|
|
<div class="mb-3">
|
|
<label class="text-white/60 text-sm block mb-1">Amount (sats)</label>
|
|
<input v-model.number="receiveInvoiceAmount" type="number" min="1" placeholder="1000" class="w-full input-glass" />
|
|
</div>
|
|
<div class="mb-3">
|
|
<label class="text-white/60 text-sm block mb-1">Memo (optional)</label>
|
|
<input v-model="receiveInvoiceMemo" type="text" placeholder="Payment for..." class="w-full input-glass" />
|
|
</div>
|
|
<div v-if="receiveInvoiceResult" class="mb-3 p-2 bg-white/5 rounded-lg">
|
|
<p class="text-white/50 text-xs mb-1">Invoice (share with sender):</p>
|
|
<p class="text-xs font-mono text-white/80 break-all">{{ receiveInvoiceResult }}</p>
|
|
<button @click="copyToClipboard(receiveInvoiceResult, 'Invoice copied')" class="mt-2 text-xs text-orange-400 hover:text-orange-300">Copy</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="receiveMethod === 'onchain'">
|
|
<div v-if="receiveOnchainAddress" class="mb-3 p-3 bg-white/5 rounded-lg text-center">
|
|
<canvas ref="onchainQrCanvas" class="mx-auto mb-3 rounded-lg" style="image-rendering: pixelated;"></canvas>
|
|
<p class="text-white/50 text-xs mb-2">Your Bitcoin address:</p>
|
|
<p class="text-sm font-mono text-white/90 break-all">{{ receiveOnchainAddress }}</p>
|
|
<button @click="copyToClipboard(receiveOnchainAddress, 'Address copied')" class="mt-2 text-xs text-orange-400 hover:text-orange-300">Copy</button>
|
|
</div>
|
|
<div v-else class="mb-3 text-center">
|
|
<p class="text-white/50 text-sm mb-2">{{ t('web5.generateFreshAddress') }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="receiveMethod === 'ecash'">
|
|
<div class="mb-3">
|
|
<label class="text-white/60 text-sm block mb-1">Paste ecash token</label>
|
|
<textarea v-model="ecashReceiveToken" rows="3" placeholder="cashuSend_..." class="w-full input-glass"></textarea>
|
|
</div>
|
|
<div v-if="ecashReceiveResult" class="mb-3 text-xs text-green-400">{{ ecashReceiveResult }}</div>
|
|
</div>
|
|
|
|
<div v-if="unifiedReceiveError" class="mb-3 text-xs text-red-400">{{ unifiedReceiveError }}</div>
|
|
|
|
<div class="flex gap-3">
|
|
<button @click="closeUnifiedReceiveModal" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">{{ t('common.close') }}</button>
|
|
<button @click="unifiedReceive" :disabled="unifiedReceiveProcessing" class="flex-1 glass-button glass-button-success px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50">
|
|
{{ unifiedReceiveProcessing ? 'Processing...' : receiveMethod === 'onchain' ? 'Generate Address' : receiveMethod === 'lightning' ? 'Create Invoice' : 'Receive' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Teleport>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed, nextTick } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { rpcClient } from '@/api/rpc-client'
|
|
import { useTransportStore } from '@/stores/transport'
|
|
import { useMeshStore } from '@/stores/mesh'
|
|
import { safeClipboardWrite } from './utils'
|
|
|
|
const { t } = useI18n()
|
|
const transportStore = useTransportStore()
|
|
const meshStore = useMeshStore()
|
|
|
|
const emit = defineEmits<{
|
|
toast: [text: string]
|
|
balancesChanged: []
|
|
}>()
|
|
|
|
// Send state
|
|
const showUnifiedSendModal = ref(false)
|
|
const sendMethod = ref<'auto' | 'lightning' | 'onchain' | 'ecash'>('auto')
|
|
const unifiedSendAmount = ref<number>(0)
|
|
const unifiedSendDest = ref('')
|
|
const unifiedSendProcessing = ref(false)
|
|
const unifiedSendError = ref('')
|
|
const sendResultTxid = ref('')
|
|
const sendResultHash = ref('')
|
|
const useHardwareWallet = ref(false)
|
|
const meshRelayActive = ref(false)
|
|
const meshRelayStatus = ref('')
|
|
const meshRelayRequestId = ref(0)
|
|
const showMeshRelayPrompt = ref(false)
|
|
const psbtData = ref('')
|
|
const psbtStep = ref<'idle' | 'created' | 'finalizing'>('idle')
|
|
const signedPsbtInput = ref('')
|
|
const ecashSendToken = ref('')
|
|
|
|
// Receive state
|
|
const showUnifiedReceiveModal = ref(false)
|
|
const receiveMethod = ref<'lightning' | 'onchain' | 'ecash'>('onchain')
|
|
const receiveInvoiceAmount = ref<number>(0)
|
|
const receiveInvoiceMemo = ref('')
|
|
const receiveInvoiceResult = ref('')
|
|
const receiveOnchainAddress = ref('')
|
|
const onchainQrCanvas = ref<HTMLCanvasElement | null>(null)
|
|
const unifiedReceiveProcessing = ref(false)
|
|
const unifiedReceiveError = ref('')
|
|
const ecashReceiveToken = ref('')
|
|
const ecashReceiveResult = ref('')
|
|
|
|
const effectiveSendMethod = computed(() => {
|
|
if (sendMethod.value !== 'auto') return sendMethod.value
|
|
const amt = unifiedSendAmount.value || 0
|
|
if (amt <= 0) return 'lightning'
|
|
if (amt < 1000) return 'ecash'
|
|
if (amt > 500000) return 'onchain'
|
|
return 'lightning'
|
|
})
|
|
|
|
function openSend() { showUnifiedSendModal.value = true }
|
|
function openReceive() { showUnifiedReceiveModal.value = true }
|
|
|
|
function closeUnifiedSendModal() {
|
|
showUnifiedSendModal.value = false
|
|
ecashSendToken.value = ''
|
|
unifiedSendError.value = ''
|
|
sendResultTxid.value = ''
|
|
sendResultHash.value = ''
|
|
psbtData.value = ''
|
|
psbtStep.value = 'idle'
|
|
signedPsbtInput.value = ''
|
|
}
|
|
|
|
function closeUnifiedReceiveModal() {
|
|
showUnifiedReceiveModal.value = false
|
|
receiveInvoiceResult.value = ''
|
|
receiveOnchainAddress.value = ''
|
|
ecashReceiveToken.value = ''
|
|
ecashReceiveResult.value = ''
|
|
unifiedReceiveError.value = ''
|
|
}
|
|
|
|
function copyEcashToken(token: string) {
|
|
safeClipboardWrite(token)
|
|
emit('toast', t('web5.ecashTokenCopied'))
|
|
}
|
|
|
|
function copyToClipboard(text: string, msg: string) {
|
|
safeClipboardWrite(text)
|
|
emit('toast', msg)
|
|
}
|
|
|
|
async function unifiedSend() {
|
|
if (!unifiedSendAmount.value || unifiedSendProcessing.value) return
|
|
unifiedSendProcessing.value = true
|
|
unifiedSendError.value = ''
|
|
ecashSendToken.value = ''
|
|
sendResultTxid.value = ''
|
|
sendResultHash.value = ''
|
|
meshRelayActive.value = false
|
|
meshRelayStatus.value = ''
|
|
|
|
const method = effectiveSendMethod.value
|
|
try {
|
|
if (method === 'ecash') {
|
|
const res = await rpcClient.call<{ token: string }>({
|
|
method: 'wallet.ecash-send',
|
|
params: { amount_sats: unifiedSendAmount.value },
|
|
})
|
|
ecashSendToken.value = res.token
|
|
} else if (method === 'lightning') {
|
|
if (!unifiedSendDest.value.trim()) {
|
|
unifiedSendError.value = t('web5.pasteInvoice')
|
|
return
|
|
}
|
|
const res = await rpcClient.call<{ payment_hash: string; amount_sats: number }>({
|
|
method: 'lnd.payinvoice',
|
|
params: { payment_request: unifiedSendDest.value.trim() },
|
|
})
|
|
sendResultHash.value = res.payment_hash
|
|
} else {
|
|
if (!unifiedSendDest.value.trim()) {
|
|
unifiedSendError.value = t('web5.enterBitcoinAddress')
|
|
return
|
|
}
|
|
if (useHardwareWallet.value) {
|
|
const res = await rpcClient.createPsbt({
|
|
outputs: [{ address: unifiedSendDest.value.trim(), amount_sats: unifiedSendAmount.value }],
|
|
})
|
|
psbtData.value = res.psbt_base64
|
|
psbtStep.value = 'created'
|
|
signedPsbtInput.value = ''
|
|
unifiedSendProcessing.value = false
|
|
return
|
|
}
|
|
await transportStore.fetchStatus()
|
|
if (transportStore.meshOnly) {
|
|
showMeshRelayPrompt.value = true
|
|
unifiedSendProcessing.value = false
|
|
return
|
|
}
|
|
try {
|
|
const res = await rpcClient.call<{ txid: string }>({
|
|
method: 'lnd.sendcoins',
|
|
params: { addr: unifiedSendDest.value.trim(), amount: unifiedSendAmount.value },
|
|
})
|
|
sendResultTxid.value = res.txid
|
|
} catch (sendErr: unknown) {
|
|
const errMsg = sendErr instanceof Error ? sendErr.message : ''
|
|
if (errMsg.includes('connection') || errMsg.includes('timeout') || errMsg.includes('unavailable')) {
|
|
showMeshRelayPrompt.value = true
|
|
unifiedSendProcessing.value = false
|
|
return
|
|
}
|
|
throw sendErr
|
|
}
|
|
}
|
|
emit('balancesChanged')
|
|
} catch (err: unknown) {
|
|
unifiedSendError.value = err instanceof Error ? err.message : t('web5.sendFailed')
|
|
} finally {
|
|
unifiedSendProcessing.value = false
|
|
}
|
|
}
|
|
|
|
async function handleMeshRelaySend() {
|
|
showMeshRelayPrompt.value = false
|
|
unifiedSendProcessing.value = true
|
|
meshRelayActive.value = true
|
|
meshRelayStatus.value = 'Creating signed transaction...'
|
|
unifiedSendError.value = ''
|
|
try {
|
|
meshRelayStatus.value = 'Signing transaction locally...'
|
|
const rawRes = await rpcClient.call<{ raw_tx_hex: string; amount_sats: number }>({
|
|
method: 'lnd.create-raw-tx',
|
|
params: { addr: unifiedSendDest.value.trim(), amount_sats: unifiedSendAmount.value },
|
|
})
|
|
meshRelayStatus.value = 'Sending via mesh radio to connected peers...'
|
|
const relayRes = await meshStore.relayTransaction(rawRes.raw_tx_hex)
|
|
meshRelayRequestId.value = relayRes.request_id
|
|
meshRelayStatus.value = 'Transaction sent via mesh -- waiting for broadcast confirmation...'
|
|
startMeshRelayPolling(relayRes.request_id)
|
|
} catch (err: unknown) {
|
|
meshRelayActive.value = false
|
|
meshRelayStatus.value = ''
|
|
unifiedSendError.value = err instanceof Error ? err.message : 'Mesh relay failed'
|
|
} finally {
|
|
unifiedSendProcessing.value = false
|
|
}
|
|
}
|
|
|
|
function dismissMeshRelayPrompt() {
|
|
showMeshRelayPrompt.value = false
|
|
}
|
|
|
|
let meshRelayPollTimer: ReturnType<typeof setInterval> | null = null
|
|
|
|
function startMeshRelayPolling(_requestId: number) {
|
|
if (meshRelayPollTimer) clearInterval(meshRelayPollTimer)
|
|
meshRelayPollTimer = setInterval(async () => {
|
|
await meshStore.fetchMessages()
|
|
const msgs = meshStore.messages
|
|
for (const msg of msgs) {
|
|
if (msg.direction !== 'received') continue
|
|
const text = msg.plaintext
|
|
if (text.includes(`[tx_relay_response]`) && text.includes('txid:')) {
|
|
const match = text.match(/txid:\s*(\w+)/)
|
|
if (match && match[1]) {
|
|
sendResultTxid.value = match[1]
|
|
meshRelayStatus.value = `Broadcast confirmed! txid: ${match[1].slice(0, 16)}... -- waiting for confirmations`
|
|
}
|
|
}
|
|
if (text.includes('[tx_confirmation]')) {
|
|
const confMatch = text.match(/(\d)\/3 confirmations/)
|
|
if (confMatch && confMatch[1]) {
|
|
const confs = parseInt(confMatch[1])
|
|
meshRelayStatus.value = `${confs}/3 confirmations${confs >= 3 ? ' -- Transaction confirmed!' : '...'}`
|
|
if (confs >= 3) {
|
|
meshRelayActive.value = false
|
|
if (meshRelayPollTimer) {
|
|
clearInterval(meshRelayPollTimer)
|
|
meshRelayPollTimer = null
|
|
}
|
|
emit('balancesChanged')
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}, 5000)
|
|
setTimeout(() => {
|
|
if (meshRelayPollTimer) {
|
|
clearInterval(meshRelayPollTimer)
|
|
meshRelayPollTimer = null
|
|
}
|
|
}, 3 * 60 * 60 * 1000)
|
|
}
|
|
|
|
async function finalizePsbt() {
|
|
if (!signedPsbtInput.value.trim() || unifiedSendProcessing.value) return
|
|
unifiedSendProcessing.value = true
|
|
unifiedSendError.value = ''
|
|
try {
|
|
await rpcClient.finalizePsbt(signedPsbtInput.value.trim())
|
|
psbtStep.value = 'idle'
|
|
psbtData.value = ''
|
|
signedPsbtInput.value = ''
|
|
sendResultTxid.value = t('web5.broadcastViaHwWallet')
|
|
emit('balancesChanged')
|
|
} catch (err: unknown) {
|
|
unifiedSendError.value = err instanceof Error ? err.message : t('web5.broadcastFailed')
|
|
} finally {
|
|
unifiedSendProcessing.value = false
|
|
}
|
|
}
|
|
|
|
function copyPsbt() {
|
|
if (!psbtData.value) return
|
|
safeClipboardWrite(psbtData.value)
|
|
unifiedSendError.value = t('web5.psbtCopied')
|
|
}
|
|
|
|
function downloadPsbt() {
|
|
if (!psbtData.value) return
|
|
const blob = new Blob([psbtData.value], { type: 'text/plain' })
|
|
const url = URL.createObjectURL(blob)
|
|
const a = document.createElement('a')
|
|
a.href = url
|
|
a.download = 'transaction.psbt'
|
|
a.click()
|
|
URL.revokeObjectURL(url)
|
|
}
|
|
|
|
function handlePsbtFileUpload(event: Event) {
|
|
const input = event.target as HTMLInputElement
|
|
const file = input.files?.[0]
|
|
if (!file) return
|
|
const reader = new FileReader()
|
|
reader.onload = (e) => {
|
|
signedPsbtInput.value = (e.target?.result as string) || ''
|
|
}
|
|
reader.readAsText(file)
|
|
input.value = ''
|
|
}
|
|
|
|
async function unifiedReceive() {
|
|
if (unifiedReceiveProcessing.value) return
|
|
unifiedReceiveProcessing.value = true
|
|
unifiedReceiveError.value = ''
|
|
try {
|
|
if (receiveMethod.value === 'lightning') {
|
|
if (!receiveInvoiceAmount.value || receiveInvoiceAmount.value < 1) {
|
|
unifiedReceiveError.value = t('web5.enterAmount')
|
|
return
|
|
}
|
|
const res = await rpcClient.call<{ payment_request: string }>({
|
|
method: 'lnd.createinvoice',
|
|
params: { amount_sats: receiveInvoiceAmount.value, memo: receiveInvoiceMemo.value },
|
|
})
|
|
receiveInvoiceResult.value = res.payment_request
|
|
} else if (receiveMethod.value === 'onchain') {
|
|
const res = await rpcClient.call<{ address: string }>({ method: 'lnd.newaddress' })
|
|
receiveOnchainAddress.value = res.address
|
|
nextTick(() => renderQrCode(res.address, onchainQrCanvas.value))
|
|
} else {
|
|
if (!ecashReceiveToken.value.trim()) {
|
|
unifiedReceiveError.value = t('web5.pasteEcashToken')
|
|
return
|
|
}
|
|
const res = await rpcClient.call<{ received_sats: number }>({
|
|
method: 'wallet.ecash-receive',
|
|
params: { token: ecashReceiveToken.value.trim() },
|
|
})
|
|
ecashReceiveResult.value = `Received ${res.received_sats} sats!`
|
|
ecashReceiveToken.value = ''
|
|
emit('balancesChanged')
|
|
}
|
|
} catch (err: unknown) {
|
|
unifiedReceiveError.value = err instanceof Error ? err.message : t('web5.receiveFailed')
|
|
} finally {
|
|
unifiedReceiveProcessing.value = false
|
|
}
|
|
}
|
|
|
|
async function renderQrCode(data: string, canvas: HTMLCanvasElement | null) {
|
|
if (!canvas || !data) return
|
|
try {
|
|
const QRCode = await import('qrcode')
|
|
await QRCode.toCanvas(canvas, `bitcoin:${data}`, {
|
|
width: 200,
|
|
margin: 2,
|
|
color: { dark: '#000000', light: '#ffffff' },
|
|
})
|
|
} catch { /* QR rendering failed silently */ }
|
|
}
|
|
|
|
defineExpose({
|
|
openSend,
|
|
openReceive,
|
|
meshRelayActive,
|
|
meshRelayStatus,
|
|
sendResultTxid,
|
|
})
|
|
</script>
|