Security (33 pentest findings addressed): - CRITICAL: backend binds 127.0.0.1, path traversal in tor.rs/dwn fixed - HIGH: federation requires signatures, XSS login redirect, RBAC viewer restricted - HIGH: tar slip prevention, S3 SSRF validation, backup ID validation - MEDIUM: remember-me random secret, TOTP session rotation, password re-auth - LOW: CSP unsafe-inline removed, CORS dev-only, onion/webhook validation Container reliability: - Memory limits on all 37 containers (OOM prevention) - Exited vs stopped state distinction with health-aware status badges - Crash recovery coordination (no more restart cascade) - User-stopped tracking survives reboots - Tiered boot recovery (databases → core → services → apps) UI: - Wallet TransactionsModal, health-aware app status badges - Restart button on containers, exited/crashed red state - Mesh view overhaul, glass button updates, BaseModal/ToggleSwitch - Apps sticky header removed, dev faucet, mutable mock wallet Infrastructure: - LND REST port 8080 exposed over Tor (LND Connect fix) - Nginx cookie_session fix, deploy script Tor config updated - Dev environment: podman auto-start, boot mode simulation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
134 lines
5.2 KiB
Vue
134 lines
5.2 KiB
Vue
<template>
|
|
<BaseModal :show="show" :title="t('web5.sendBitcoinTitle')" max-width="max-w-2xl" content-class="max-h-[90vh] overflow-y-auto" @close="close">
|
|
<!-- 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' ? t('sendBitcoin.onChain') : m === 'lightning' ? t('sendBitcoin.lightning') : m === 'ecash' ? t('sendBitcoin.ecash') : t('sendBitcoin.auto') }}</button>
|
|
</div>
|
|
|
|
<div v-if="sendMethod === 'auto'" class="mb-3 p-2 bg-white/5 rounded-lg">
|
|
<p class="text-xs text-white/50">{{ t('sendBitcoin.autoMethodDesc') }}</p>
|
|
</div>
|
|
|
|
<div class="mb-3">
|
|
<label class="text-white/60 text-sm block mb-1">{{ t('sendBitcoin.amountSats') }}</label>
|
|
<input v-model.number="amount" type="number" min="1" placeholder="1000" class="w-full input-glass" />
|
|
</div>
|
|
|
|
<div v-if="effectiveMethod !== 'ecash'" class="mb-3">
|
|
<label class="text-white/60 text-sm block mb-1">
|
|
{{ effectiveMethod === 'lightning' ? t('sendBitcoin.lightningInvoice') : t('sendBitcoin.bitcoinAddress') }}
|
|
</label>
|
|
<textarea v-model="dest" rows="2" :placeholder="effectiveMethod === 'lightning' ? 'lnbc...' : 'bc1...'" class="w-full input-glass font-mono"></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">{{ t('sendBitcoin.tokenShareLabel') }}</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">{{ t('common.copy') }}</button>
|
|
</div>
|
|
|
|
<div v-if="resultTxid" class="mb-3 alert-success">
|
|
<p class="text-xs">{{ t('sendBitcoin.sentTx', { txid: resultTxid }) }}</p>
|
|
</div>
|
|
<div v-if="resultHash" class="mb-3 alert-success">
|
|
<p class="text-xs">{{ t('sendBitcoin.paidHash', { hash: resultHash }) }}</p>
|
|
</div>
|
|
|
|
<div v-if="error" class="mb-3 alert-error">{{ 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 glass-button-warning px-4 py-2 rounded-lg text-sm font-medium disabled:opacity-50">
|
|
{{ processing ? t('common.sending') : t('common.send') }}
|
|
</button>
|
|
</div>
|
|
</BaseModal>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { rpcClient } from '@/api/rpc-client'
|
|
import BaseModal from '@/components/BaseModal.vue'
|
|
|
|
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>
|