From 3aebbcbbb86aae2ca27d19e7a08924a72853927d Mon Sep 17 00:00:00 2001 From: archipelago Date: Thu, 16 Jul 2026 21:49:09 -0400 Subject: [PATCH] =?UTF-8?q?feat(setup):=20Zeus=20lightning=20journey=20?= =?UTF-8?q?=E2=80=94=20fund-wallet=20step,=20IBD=20timer=20+=20finish-setu?= =?UTF-8?q?p=20toast,=20channel=20suggestions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every Lightning setup (Open a Shop, Accept Payments, Run a Lightning Node) gains a guided path to a working channel: - New "Fund Your Bitcoin Wallet" step, gated on the blockchain finishing its initial sync: while syncing it shows a live progress bar with a counting-down time-remaining estimate; once synced it shows the on-chain balance and a "Fund Wallet" button that opens the receive modal with a fresh address, QR, and the Zeus channel limits (min 150,000 / max 1,500,000 sats) noted. - When the sync finishes while a Lightning setup is mid-flight, a toast pops with a "Finish setup" link straight back to the wizard (toasts now support action links). If several setups are in flight, one is chosen — the shared steps complete the lightning part of any of them. - Channel steps are Zeus-branded (logo + copy) and land on the Lightning Channels screen, which now carries an "Open a channel with Zeus" card that prefills the open-channel modal with the Olympus peer URI, 150k sats, and private-channel checked. "Get Zeus" links to zeusln.com. - Setup completion now actually requires walking the manual steps (fund, open channel, configure) — previously any step whose app was installed was silently auto-ticked and running apps marked the whole goal done. - Completion CTAs now go to the app you just set up ("Go to my shop (BTCPay)" etc.) instead of the generic services list; iframe apps open in the on-top app overlay. - On-chain send modal gains a "Send all funds" sweep toggle, backed by LND's send_all on the backend (amount no longer required when sweeping). - Demo/mock: bitcoin.getinfo now returns the block_height/sync_progress contract the UI reads and simulates a ~90s IBD ramp per visitor so the timer, toast, and fund flow can all be demoed live; channel list data fixed (status/channel_point/liquidity totals — the panel previously crashed on the missing status field); sendcoins supports send_all. Co-Authored-By: Claude Fable 5 --- core/archipelago/src/api/rpc/lnd/wallet.rs | 57 ++++-- neode-ui/mock-backend.js | 42 +++-- .../public/assets/img/app-icons/zeus.webp | Bin 0 -> 5196 bytes .../src/components/LightningChannelsPanel.vue | 72 +++++++- .../src/components/ReceiveBitcoinModal.vue | 19 +- neode-ui/src/components/SendBitcoinModal.vue | 54 +++++- neode-ui/src/components/ToastStack.vue | 16 +- neode-ui/src/composables/useBitcoinSync.ts | 109 ++++++++++++ .../src/composables/useIbdFinishWatcher.ts | 90 ++++++++++ neode-ui/src/composables/useToast.ts | 13 +- neode-ui/src/data/goals.ts | 50 +++++- neode-ui/src/stores/goals.ts | 11 +- neode-ui/src/types/goals.ts | 10 +- neode-ui/src/views/Dashboard.vue | 4 + neode-ui/src/views/GoalDetail.vue | 165 +++++++++++++++++- neode-ui/src/views/goals/goalStepActions.ts | 9 +- 16 files changed, 658 insertions(+), 63 deletions(-) create mode 100644 neode-ui/public/assets/img/app-icons/zeus.webp create mode 100644 neode-ui/src/composables/useBitcoinSync.ts create mode 100644 neode-ui/src/composables/useIbdFinishWatcher.ts 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 0000000000000000000000000000000000000000..5ea10aff9d4c1ee1e468f0a7408c1d13cae11e90 GIT binary patch literal 5196 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 + +
+
+ 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 → +
+
+
+