Phase 5 mesh networking: - E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes relay encrypted blobs transparently via Meshcore native routing - Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic looks like sensor data on the wire, 0xAA marker, configurable per-node - Pre-flight Bitcoin Core health check on relay node — specific error codes (bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails - mesh.relay-status RPC endpoint — frontend polls for relay result every 3s - On-Chain / Lightning tabs in Off-Grid Bitcoin panel - Archy Peers vs Mesh Broadcast relay mode selector - Mesh view fills viewport (no page scroll), internal panel scrolling - Version bump to 1.2.0-alpha Also includes: deploy hardening, container fixes, IndeedHub updates, boot screen, dashboard improvements, MASTER_PLAN task tracking Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
139 lines
5.5 KiB
Vue
139 lines
5.5 KiB
Vue
<template>
|
||
<Teleport to="body">
|
||
<div v-if="show" class="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-md" @click.self="close" @keydown.escape="close">
|
||
<div class="glass-card p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto" role="dialog" aria-modal="true">
|
||
<h2 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="amount" type="number" min="1" placeholder="1000" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-white/30" />
|
||
</div>
|
||
|
||
<div v-if="effectiveMethod !== 'ecash'" class="mb-3">
|
||
<label class="text-white/60 text-sm block mb-1">
|
||
{{ effectiveMethod === 'lightning' ? 'Lightning Invoice (BOLT11)' : 'Bitcoin Address' }}
|
||
</label>
|
||
<textarea v-model="dest" rows="2" :placeholder="effectiveMethod === 'lightning' ? 'lnbc...' : 'bc1...'" class="w-full bg-white/5 border border-white/10 rounded-lg px-3 py-2 text-white text-sm font-mono focus:outline-none focus:border-white/30"></textarea>
|
||
</div>
|
||
|
||
<div v-if="ecashToken && effectiveMethod === '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">{{ ecashToken }}</p>
|
||
<button @click="copyText(ecashToken)" class="mt-2 text-xs text-orange-400 hover:text-orange-300">Copy</button>
|
||
</div>
|
||
|
||
<div v-if="resultTxid" class="mb-3 p-2 bg-green-500/10 border border-green-500/20 rounded-lg">
|
||
<p class="text-green-400 text-xs">Sent! TX: {{ resultTxid }}</p>
|
||
</div>
|
||
<div v-if="resultHash" class="mb-3 p-2 bg-green-500/10 border border-green-500/20 rounded-lg">
|
||
<p class="text-green-400 text-xs">Paid! Hash: {{ resultHash }}</p>
|
||
</div>
|
||
|
||
<div v-if="error" class="mb-3 text-xs text-red-400">{{ error }}</div>
|
||
|
||
<div class="flex gap-3">
|
||
<button @click="close" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm">{{ t('common.close') }}</button>
|
||
<button @click="send" :disabled="processing || !amount" class="flex-1 glass-button px-4 py-2 rounded-lg text-sm font-medium bg-orange-500/20 border-orange-500/30 disabled:opacity-50">
|
||
{{ processing ? 'Sending...' : 'Send' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</Teleport>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { ref, computed } from 'vue'
|
||
import { useI18n } from 'vue-i18n'
|
||
import { rpcClient } from '@/api/rpc-client'
|
||
|
||
const { t } = useI18n()
|
||
|
||
const props = defineProps<{ show: boolean }>()
|
||
const emit = defineEmits<{ close: []; sent: [] }>()
|
||
|
||
const sendMethod = ref<'auto' | 'lightning' | 'onchain' | 'ecash'>('auto')
|
||
const amount = ref<number>(0)
|
||
const dest = ref('')
|
||
const processing = ref(false)
|
||
const error = ref('')
|
||
const resultTxid = ref('')
|
||
const resultHash = ref('')
|
||
const ecashToken = ref('')
|
||
|
||
const effectiveMethod = computed(() => {
|
||
if (sendMethod.value !== 'auto') return sendMethod.value
|
||
const amt = amount.value || 0
|
||
if (amt <= 0) return 'lightning'
|
||
if (amt < 1000) return 'ecash'
|
||
if (amt > 500000) return 'onchain'
|
||
return 'lightning'
|
||
})
|
||
|
||
function close() {
|
||
error.value = ''
|
||
resultTxid.value = ''
|
||
resultHash.value = ''
|
||
ecashToken.value = ''
|
||
emit('close')
|
||
}
|
||
|
||
function copyText(text: string) {
|
||
navigator.clipboard.writeText(text).catch(() => {})
|
||
}
|
||
|
||
async function send() {
|
||
if (!amount.value || processing.value) return
|
||
processing.value = true
|
||
error.value = ''
|
||
ecashToken.value = ''
|
||
resultTxid.value = ''
|
||
resultHash.value = ''
|
||
|
||
const method = effectiveMethod.value
|
||
try {
|
||
if (method === 'ecash') {
|
||
const res = await rpcClient.call<{ token: string }>({
|
||
method: 'wallet.ecash-send',
|
||
params: { amount_sats: amount.value },
|
||
})
|
||
ecashToken.value = res.token
|
||
} else if (method === 'lightning') {
|
||
if (!dest.value.trim()) { error.value = t('web5.pasteInvoice'); return }
|
||
const res = await rpcClient.call<{ payment_hash: string }>({
|
||
method: 'lnd.payinvoice',
|
||
params: { payment_request: dest.value.trim() },
|
||
})
|
||
resultHash.value = res.payment_hash
|
||
} else {
|
||
if (!dest.value.trim()) { error.value = t('web5.enterBitcoinAddress'); return }
|
||
const res = await rpcClient.call<{ txid: string }>({
|
||
method: 'lnd.sendcoins',
|
||
params: { addr: dest.value.trim(), amount: amount.value },
|
||
})
|
||
resultTxid.value = res.txid
|
||
}
|
||
emit('sent')
|
||
} catch (err: unknown) {
|
||
error.value = err instanceof Error ? err.message : t('web5.sendFailed')
|
||
} finally {
|
||
processing.value = false
|
||
}
|
||
}
|
||
</script>
|