diff --git a/core/archipelago/src/api/rpc/lnd/wallet.rs b/core/archipelago/src/api/rpc/lnd/wallet.rs index 71650834..286f84b5 100644 --- a/core/archipelago/src/api/rpc/lnd/wallet.rs +++ b/core/archipelago/src/api/rpc/lnd/wallet.rs @@ -95,33 +95,54 @@ impl RpcHandler { .get("addr") .and_then(|v| v.as_str()) .ok_or_else(|| anyhow::anyhow!("Missing 'addr' parameter"))?; - let amount = params - .get("amount") - .and_then(|v| v.as_i64()) - .ok_or_else(|| anyhow::anyhow!("Missing 'amount' parameter (sats)"))?; - - if amount < 546 { - return Err(anyhow::anyhow!( - "Amount must be at least 546 sats (dust limit)" - )); - } - if amount > 21_000_000 * 100_000_000 { - return Err(anyhow::anyhow!("Amount exceeds maximum Bitcoin supply")); - } + // send_all sweeps the entire confirmed on-chain balance (LND computes + // the amount after fees); amount is required otherwise. + let send_all = params + .get("send_all") + .and_then(|v| v.as_bool()) + .unwrap_or(false); + let amount = if send_all { + None + } else { + let amount = params + .get("amount") + .and_then(|v| v.as_i64()) + .ok_or_else(|| anyhow::anyhow!("Missing 'amount' parameter (sats)"))?; + if amount < 546 { + return Err(anyhow::anyhow!( + "Amount must be at least 546 sats (dust limit)" + )); + } + if amount > 21_000_000 * 100_000_000 { + return Err(anyhow::anyhow!("Amount exceeds maximum Bitcoin supply")); + } + Some(amount) + }; // Validate Bitcoin address format (basic: length and allowed chars) if addr.len() < 14 || addr.len() > 90 || !addr.chars().all(|c| c.is_ascii_alphanumeric()) { return Err(anyhow::anyhow!("Invalid Bitcoin address format")); } - info!(addr = addr, amount = amount, "Sending on-chain Bitcoin"); + info!( + addr = addr, + amount = amount, + send_all = send_all, + "Sending on-chain Bitcoin" + ); let (client, macaroon_hex) = self.lnd_client().await?; - let send_body = serde_json::json!({ - "addr": addr, - "amount": amount.to_string(), - }); + let send_body = match amount { + Some(amount) => serde_json::json!({ + "addr": addr, + "amount": amount.to_string(), + }), + None => serde_json::json!({ + "addr": addr, + "send_all": true, + }), + }; let resp = client .post(format!("{LND_REST_BASE_URL}/v1/transactions")) diff --git a/neode-ui/mock-backend.js b/neode-ui/mock-backend.js index 36b476c0..75eaf731 100755 --- a/neode-ui/mock-backend.js +++ b/neode-ui/mock-backend.js @@ -3394,14 +3394,19 @@ app.post('/rpc/v1', (req, res) => { } case 'lnd.listchannels': { + // Shape matches the real backend: status + channel_point are required + // by the channels panel; totals feed the liquidity summary tiles. + const channels = [ + { chan_id: '840921088114688', remote_pubkey: '031b301307574bbe9b9ac7b79cbe1700e31e544513eae0b5d7497483083f99e581', capacity: 1500000, local_balance: 950000, remote_balance: 550000, active: true, status: 'active', channel_point: randomHex(32) + ':0', peer_alias: 'Olympus by ZEUS' }, + { chan_id: '840921088114689', remote_pubkey: '03abcdef12345678901234567890123456789012345678901234567890abcdef12', capacity: 2000000, local_balance: 1200000, remote_balance: 800000, active: true, status: 'active', channel_point: randomHex(32) + ':1', peer_alias: 'WalletOfSatoshi' }, + { chan_id: '840921088114690', remote_pubkey: '02fedcba98765432109876543210987654321098765432109876543210fedcba98', capacity: 10000000, local_balance: 4500000, remote_balance: 5500000, active: true, status: 'active', channel_point: randomHex(32) + ':0', peer_alias: 'Voltage' }, + { chan_id: '840921088114691', remote_pubkey: '03456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123', capacity: 3000000, local_balance: 100000, remote_balance: 2900000, active: false, status: 'inactive', channel_point: randomHex(32) + ':0', peer_alias: 'Kraken' }, + ] return res.json({ result: { - channels: [ - { chan_id: '840921088114688', remote_pubkey: '02778f4a', capacity: 5000000, local_balance: 2450000, remote_balance: 2550000, active: true, peer_alias: 'ACINQ Signet' }, - { chan_id: '840921088114689', remote_pubkey: '03abcdef', capacity: 2000000, local_balance: 1200000, remote_balance: 800000, active: true, peer_alias: 'WalletOfSatoshi' }, - { chan_id: '840921088114690', remote_pubkey: '02fedcba', capacity: 10000000, local_balance: 4500000, remote_balance: 5500000, active: true, peer_alias: 'Voltage' }, - { chan_id: '840921088114691', remote_pubkey: '03456789', capacity: 3000000, local_balance: 100000, remote_balance: 2900000, active: true, peer_alias: 'Kraken' }, - ], + channels, + total_outbound: channels.reduce((s, c) => s + c.local_balance, 0), + total_inbound: channels.reduce((s, c) => s + c.remote_balance, 0), }, }) } @@ -3445,7 +3450,10 @@ app.post('/rpc/v1', (req, res) => { } case 'lnd.sendcoins': { - const amt = params?.amount || params?.amt || 50000 + // send_all sweeps the entire on-chain balance (minus a mock fee) + const amt = params?.send_all + ? Math.max(0, walletState.onchain_sats - 250) + : (params?.amount || params?.amt || 50000) walletState.onchain_sats = Math.max(0, walletState.onchain_sats - amt) const txid = randomHex(32) walletState.transactions.unshift({ @@ -3607,15 +3615,29 @@ app.post('/rpc/v1', (req, res) => { } case 'bitcoin.getinfo': { + // Demo IBD simulation: the first call of a session arms a ~90s ramp + // from 98.2% → 100% so the setup wizard can demo the live sync timer + // and the "finish setup" toast that fires when IBD completes. + // (The real backend returns { block_height, sync_progress } — a 0–1 + // fraction — which is what the frontend reads; the bitcoin-core-style + // fields are kept for any legacy consumers.) + if (!walletState.ibd_started_at) walletState.ibd_started_at = Date.now() + const IBD_RAMP_MS = 90_000 + const elapsed = Date.now() - walletState.ibd_started_at + const syncProgress = Math.min(1, 0.982 + 0.018 * (elapsed / IBD_RAMP_MS)) + const tipHeight = 892451 + const height = Math.round(tipHeight * syncProgress) return res.json({ result: { chain: 'signet', - blocks: 892451, - headers: 892451, + block_height: height, + sync_progress: syncProgress, + blocks: height, + headers: tipHeight, bestblockhash: 'a1b2c3d4e5f6' + '0'.repeat(58), difficulty: 0.001126515290698186, mediantime: Math.floor(Date.now() / 1000) - 300, - verificationprogress: 1.0, + verificationprogress: syncProgress, chainwork: '000000000000000000000000000000000000000000000000000000000001a2b3', size_on_disk: 210_000_000, pruned: false, diff --git a/neode-ui/public/assets/img/app-icons/zeus.webp b/neode-ui/public/assets/img/app-icons/zeus.webp new file mode 100644 index 00000000..5ea10aff Binary files /dev/null and b/neode-ui/public/assets/img/app-icons/zeus.webp differ diff --git a/neode-ui/src/components/LightningChannelsPanel.vue b/neode-ui/src/components/LightningChannelsPanel.vue index 93129af7..92f0435a 100644 --- a/neode-ui/src/components/LightningChannelsPanel.vue +++ b/neode-ui/src/components/LightningChannelsPanel.vue @@ -16,6 +16,39 @@ + +
+
+ Zeus +
+

Open a channel with Zeus

+

+ Pair your node with the Zeus mobile wallet — open a channel to their Olympus node and + start sending and receiving Lightning payments from your phone. + Minimum 150,000 · maximum 1,500,000 sats. +

+
+
+ + Get Zeus → +
+
+
+