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.
+
+ Receiving needs inbound liquidity — a
+ channel with funds on the far side — otherwise any invoice you create
+ is unpayable.
+
+
+ Sending needs outbound liquidity — a
+ funded channel to route through.
+
+ 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.
@@ -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