diff --git a/neode-ui/public/assets/img/app-icons/core-lightning.svg b/neode-ui/public/assets/img/app-icons/core-lightning.svg
new file mode 100644
index 00000000..94e8b1e2
--- /dev/null
+++ b/neode-ui/public/assets/img/app-icons/core-lightning.svg
@@ -0,0 +1,56 @@
+
diff --git a/neode-ui/src/components/LightningRequiredModal.vue b/neode-ui/src/components/LightningRequiredModal.vue
index 3d9d9ab8..f41728a5 100644
--- a/neode-ui/src/components/LightningRequiredModal.vue
+++ b/neode-ui/src/components/LightningRequiredModal.vue
@@ -5,12 +5,17 @@
same reasoning as ExternalExplorerModal. -->
-
+
+ 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.
+
+
Lightning payments need a Lightning node that's actually running. Yours is
installed but isn't running right now — start it from My Apps and try
again.
@@ -21,13 +26,27 @@
it'll be installed for you.
-
+
-
+
+
+
+
+
+
{{ node.name }}
@@ -41,7 +60,7 @@
@@ -71,12 +90,17 @@
class="flex-1 glass-button glass-button-warning px-4 py-2 rounded-lg text-sm font-medium"
@click="openApps"
>Open My Apps
+ Set up Lightning
+
+
diff --git a/neode-ui/src/components/ReceiveBitcoinModal.vue b/neode-ui/src/components/ReceiveBitcoinModal.vue
index 81edfd8d..4b77e29e 100644
--- a/neode-ui/src/components/ReceiveBitcoinModal.vue
+++ b/neode-ui/src/components/ReceiveBitcoinModal.vue
@@ -191,6 +191,9 @@ async function receive() {
emit('received')
}
} catch (err: unknown) {
+ // A running node with no inbound liquidity is a funding problem, not a
+ // failure — reuse the Lightning modal in its funding mode instead.
+ if (receiveMethod.value === 'lightning' && lightning.handleLightningFailure(err)) return
error.value = receiveMethod.value === 'onchain'
? explainReceiveAddressFailure(err)
: err instanceof Error ? err.message : 'Failed'
diff --git a/neode-ui/src/components/SendBitcoinModal.vue b/neode-ui/src/components/SendBitcoinModal.vue
index 9a01cb0c..84c5ff32 100644
--- a/neode-ui/src/components/SendBitcoinModal.vue
+++ b/neode-ui/src/components/SendBitcoinModal.vue
@@ -665,6 +665,8 @@ async function send() {
// Success pane (or the token pane for ecash mints) takes over the modal.
confirming.value = false
} catch (err: unknown) {
+ // Running node with nothing to pay with -> funding modal, not a raw string.
+ if (effectiveMethod.value === 'lightning' && lightning.handleLightningFailure(err)) return
error.value = err instanceof Error ? err.message : t('web5.sendFailed')
} finally {
processing.value = false
diff --git a/neode-ui/src/composables/useLightningRequired.ts b/neode-ui/src/composables/useLightningRequired.ts
index 2f1ac434..cca8e95e 100644
--- a/neode-ui/src/composables/useLightningRequired.ts
+++ b/neode-ui/src/composables/useLightningRequired.ts
@@ -28,8 +28,10 @@ export const LIGHTNING_NODE_APP_IDS = ['lnd'] as const
/** `absent` — nothing installed, offer to install one.
* `stopped` — installed but not running, point the user at My Apps.
+ * `no-funds` — running, but there is nothing to pay with / no inbound
+ * liquidity to be paid into; send the user to the Lightning setup goal.
* `running` — good to go. */
-export type LightningStatus = 'absent' | 'stopped' | 'running'
+export type LightningStatus = 'absent' | 'stopped' | 'running' | 'no-funds'
// Module-scope: one source of truth shared by every caller and the single
// global modal mounted in App.vue.
@@ -73,5 +75,52 @@ export function useLightningRequired() {
show.value = false
}
- return { show, status, lightningStatus, hasLightningNode, requireLightningNode, close }
+ /**
+ * Raise the same modal in its funding mode: the node is installed and
+ * running, but has no usable balance/liquidity yet. Reuses this modal
+ * rather than inventing a second one, and routes to the Lightning setup
+ * goal where funding and channel-opening already live.
+ */
+ function openLightningFunding() {
+ status.value = 'no-funds'
+ show.value = true
+ }
+
+ /**
+ * Map a failed Lightning attempt onto the funding modal when the node is
+ * running but has nothing to pay with / no inbound liquidity. Returns true
+ * when it handled the error, so the caller can skip showing a raw string.
+ *
+ * Matched on the message because LND surfaces these as plain text: there is
+ * no distinct error code for "no channels" vs "no route" vs "insufficient
+ * balance", and all three mean the same thing to the user — fund me.
+ */
+ function handleLightningFailure(err: unknown): boolean {
+ if (lightningStatus() !== 'running') return false
+ const msg = (err instanceof Error ? err.message : String(err ?? '')).toLowerCase()
+ const fundingRelated = [
+ 'no route',
+ 'no routes',
+ 'insufficient',
+ 'no channel',
+ 'not enough',
+ 'balance',
+ 'unable to find a path',
+ 'no path',
+ ].some((needle) => msg.includes(needle))
+ if (!fundingRelated) return false
+ openLightningFunding()
+ return true
+ }
+
+ return {
+ show,
+ status,
+ lightningStatus,
+ hasLightningNode,
+ requireLightningNode,
+ openLightningFunding,
+ handleLightningFailure,
+ close,
+ }
}