diff --git a/neode-ui/src/components/AppLauncherOverlay.vue b/neode-ui/src/components/AppLauncherOverlay.vue index 4bae8a02..cb6bb640 100644 --- a/neode-ui/src/components/AppLauncherOverlay.vue +++ b/neode-ui/src/components/AppLauncherOverlay.vue @@ -529,7 +529,7 @@ async function approvePayment() { } else if (method === 'lightning') { // Both arms below need a Lightning node — paying an invoice and minting // one. With none installed, raise the install modal instead of failing. - if (!lightning.requireLightningNode()) return + if (!(await lightning.requireLightningReady('send'))) return if (pay.invoice) { // Tracked to a real terminal state — slow routing is not a failure. const res = await rpcClient.payLightningInvoice({ payment_request: pay.invoice }) diff --git a/neode-ui/src/components/LightningRequiredModal.vue b/neode-ui/src/components/LightningRequiredModal.vue index f41728a5..831bac89 100644 --- a/neode-ui/src/components/LightningRequiredModal.vue +++ b/neode-ui/src/components/LightningRequiredModal.vue @@ -11,9 +11,19 @@ @close="onClose" >

- Your Lightning node is running, but it has no funds or inbound liquidity - yet — so it can't send or be paid. The Lightning setup walks you through - funding it and opening a channel. + Your Lightning node is running, but it has no payment channel yet. + + + Open one with Zeus Olympus from the + channels screen — it's prefilled there, and needs 150,000–1,500,000 + on-chain sats.

Lightning payments need a Lightning node that's actually running. Yours is @@ -81,7 +91,7 @@ started. You can close this and carry on; the install keeps running.

-
+
@@ -90,11 +100,16 @@ class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium" @click="openApps" >Open My Apps - +
@@ -140,7 +155,7 @@ const nodes: NodeChoice[] = [ const router = useRouter() const modalTitle = computed(() => { - if (lightningStatusIs('no-funds')) return 'Lightning not funded yet' + if (lightningStatusIs('no-funds')) return 'You need a Lightning channel' if (lightningStatusIs('stopped')) return 'Lightning node not running' return 'Lightning node required' }) @@ -177,6 +192,16 @@ function lightningStatusIs(s: string) { /** The Lightning goal already owns funding + channel-opening, so reuse it * rather than duplicating that flow inside this modal. */ function openLightningSetup() { + lightning.close() + // The channels screen already has the prefilled Zeus/Olympus open-channel + // flow, so send the user straight to the thing that solves it rather than + // to the wizard that would only point here anyway. + router.push('/dashboard/apps/lnd/channels') +} + +/** The guided walkthrough, for someone who wants the whole path explained + * rather than to be dropped straight into the open-channel form. */ +function openSetupGuide() { lightning.close() router.push('/dashboard/goals/run-lightning-node') } diff --git a/neode-ui/src/components/ReceiveBitcoinModal.vue b/neode-ui/src/components/ReceiveBitcoinModal.vue index 4b77e29e..085133e1 100644 --- a/neode-ui/src/components/ReceiveBitcoinModal.vue +++ b/neode-ui/src/components/ReceiveBitcoinModal.vue @@ -156,7 +156,7 @@ async function receive() { // No Lightning implementation installed is not an error — it is a // missing prerequisite. Raise the install modal instead of letting // lnd.createinvoice fail with connection-refused (FED-08 follow-up). - if (!lightning.requireLightningNode()) return + if (!(await lightning.requireLightningReady('receive'))) return if (!invoiceAmount.value) { error.value = t('receiveBitcoin.enterAnAmount'); return } const res = await rpcClient.call<{ payment_request: string }>({ method: 'lnd.createinvoice', diff --git a/neode-ui/src/components/SendBitcoinModal.vue b/neode-ui/src/components/SendBitcoinModal.vue index 84c5ff32..52b0720d 100644 --- a/neode-ui/src/components/SendBitcoinModal.vue +++ b/neode-ui/src/components/SendBitcoinModal.vue @@ -513,14 +513,14 @@ async function loadConfirmBalance() { } } -function review() { +async function review() { error.value = '' const method = effectiveMethod.value const d = dest.value.trim() if (method === 'lightning') { // Gate BEFORE the confirm step, not at submit: walking a user through // review-and-confirm only to fail on a missing node is the defect. - if (!lightning.requireLightningNode()) return + if (!(await lightning.requireLightningReady('send'))) return if (!d) { error.value = t('web5.pasteInvoice'); return } invoiceAmountSats.value = parseBolt11AmountSats(d) } else { diff --git a/neode-ui/src/composables/useLightningRequired.ts b/neode-ui/src/composables/useLightningRequired.ts index cca8e95e..4a378525 100644 --- a/neode-ui/src/composables/useLightningRequired.ts +++ b/neode-ui/src/composables/useLightningRequired.ts @@ -17,6 +17,7 @@ // lnd container at all. import { ref } from 'vue' import { useAppStore } from '@/stores/app' +import { rpcClient } from '@/api/rpc-client' import { PackageState } from '@/types/api' /** Package ids that provide a Lightning node. @@ -37,6 +38,8 @@ export type LightningStatus = 'absent' | 'stopped' | 'running' | 'no-funds' // global modal mounted in App.vue. const show = ref(false) const status = ref('absent') +/** Which direction raised the funding modal, so the copy can be specific. */ +const fundingDirection = ref<'send' | 'receive'>('receive') export function useLightningRequired() { const appStore = useAppStore() @@ -113,13 +116,44 @@ export function useLightningRequired() { return true } + /** + * The full readiness gate: node installed AND running AND with liquidity in + * the direction being attempted. + * + * Node state alone is not enough — LND happily mints an invoice with zero + * channels, so a state-only gate hands the user an invoice nobody can pay + * (and a send that can only fail). `receive` needs inbound liquidity, + * `send` needs outbound. + * + * Fails OPEN on an RPC error: if we cannot read the channel list we let the + * attempt proceed rather than block a working wallet on a transient blip. + */ + async function requireLightningReady(direction: 'send' | 'receive'): Promise { + if (!requireLightningNode()) return false + try { + const res = await rpcClient.call<{ total_inbound?: number; total_outbound?: number }>({ + method: 'lnd.listchannels', + timeout: 15000, + }) + const liquidity = direction === 'receive' ? res?.total_inbound ?? 0 : res?.total_outbound ?? 0 + if (liquidity > 0) return true + fundingDirection.value = direction + openLightningFunding() + return false + } catch { + return true + } + } + return { show, + fundingDirection, status, lightningStatus, hasLightningNode, requireLightningNode, openLightningFunding, + requireLightningReady, handleLightningFailure, close, } diff --git a/neode-ui/src/views/web5/Web5SendReceiveModals.vue b/neode-ui/src/views/web5/Web5SendReceiveModals.vue index a9ac9899..6f7ceace 100644 --- a/neode-ui/src/views/web5/Web5SendReceiveModals.vue +++ b/neode-ui/src/views/web5/Web5SendReceiveModals.vue @@ -471,7 +471,7 @@ async function unifiedReceive() { if (receiveMethod.value === 'lightning') { // Missing prerequisite, not an error — raise the install modal rather // than letting lnd.createinvoice fail with connection-refused. - if (!lightning.requireLightningNode()) return + if (!(await lightning.requireLightningReady('receive'))) return if (!receiveInvoiceAmount.value || receiveInvoiceAmount.value < 1) { unifiedReceiveError.value = t('web5.enterAmount') return