feat: Lightning channel backup, Web5 mobile tab active, file path fix
Task 14: Lightning Channel Backup - New lnd.export-channel-backup RPC — exports SCB (Static Channel Backup) - Settings UI: "Lightning Channel Backup" section with export + copy - Returns base64 backup data, channel count, timestamp Web5 mobile tab active state - Fixed combined tab matching for Web5: includes /web5, /federation, /mesh routes - Previously only matched /cloud and /server (wrong branch) Content file path fix - Allow forward slashes in filenames for subdirectories (Music/song.mp3) - Still block .., \, null bytes, hidden files, absolute paths Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1f732d8d08
commit
c5417640a2
@ -938,6 +938,53 @@ impl RpcHandler {
|
||||
"grpc_port": 10009,
|
||||
}))
|
||||
}
|
||||
|
||||
/// lnd.export-channel-backup — Export all channel static backups (SCB).
|
||||
/// Returns base64-encoded multi-channel backup that can restore channels on a new node.
|
||||
pub(super) async fn handle_lnd_export_channel_backup(&self) -> Result<serde_json::Value> {
|
||||
let macaroon_path = "/var/lib/archipelago/lnd/data/chain/bitcoin/mainnet/admin.macaroon";
|
||||
let macaroon_bytes = tokio::fs::read(macaroon_path)
|
||||
.await
|
||||
.context("Failed to read LND admin macaroon")?;
|
||||
let macaroon_hex = hex::encode(&macaroon_bytes);
|
||||
|
||||
let cert_path = "/var/lib/archipelago/lnd/tls.cert";
|
||||
let client = reqwest::Client::builder()
|
||||
.danger_accept_invalid_certs(true)
|
||||
.timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.context("Failed to build HTTP client")?;
|
||||
|
||||
let resp = client
|
||||
.get("https://127.0.0.1:8080/v1/channels/backup")
|
||||
.header("Grpc-Metadata-macaroon", &macaroon_hex)
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to reach LND REST API")?;
|
||||
|
||||
if !resp.status().is_success() {
|
||||
anyhow::bail!("LND returned {}", resp.status());
|
||||
}
|
||||
|
||||
let data: serde_json::Value = resp.json().await.context("Invalid JSON from LND")?;
|
||||
|
||||
// Extract the multi_chan_backup bytes
|
||||
let backup_b64 = data
|
||||
.get("multi_chan_backup")
|
||||
.and_then(|m| m.get("multi_chan_backup"))
|
||||
.and_then(|b| b.as_str())
|
||||
.unwrap_or("");
|
||||
|
||||
Ok(serde_json::json!({
|
||||
"backup": backup_b64,
|
||||
"channel_count": data.get("multi_chan_backup")
|
||||
.and_then(|m| m.get("chan_points"))
|
||||
.and_then(|c| c.as_array())
|
||||
.map(|a| a.len())
|
||||
.unwrap_or(0),
|
||||
"timestamp": chrono::Utc::now().to_rfc3339(),
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
// Channel types
|
||||
|
||||
@ -514,6 +514,7 @@ impl RpcHandler {
|
||||
"lnd.create-raw-tx" => self.handle_lnd_create_raw_tx(params).await,
|
||||
"lnd.gettransactions" => self.handle_lnd_gettransactions().await,
|
||||
"lnd.connect-info" => self.handle_lnd_connect_info().await,
|
||||
"lnd.export-channel-backup" => self.handle_lnd_export_channel_backup().await,
|
||||
|
||||
// Multi-identity management
|
||||
"identity.list" => self.handle_identity_list(params).await,
|
||||
|
||||
@ -304,7 +304,9 @@
|
||||
'nav-tab-active': item.isCombined
|
||||
? (item.path === '/dashboard/apps'
|
||||
? (route.path.includes('/apps') || route.path.includes('/marketplace') || route.path.includes('/discover') || route.path.includes('/app-session'))
|
||||
: (route.path.includes('/cloud') || route.path.includes('/server')))
|
||||
: item.path === '/dashboard/web5'
|
||||
? (route.path.includes('/web5') || route.path.includes('/federation') || route.path.includes('/mesh'))
|
||||
: (route.path.includes('/cloud') || route.path.includes('/server')))
|
||||
: undefined
|
||||
}"
|
||||
:exact-active-class="item.isCombined ? undefined : 'nav-tab-active'"
|
||||
|
||||
@ -955,6 +955,26 @@
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<!-- Lightning Channel Backup -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
<h2 class="text-xl font-semibold text-white/96 mb-1">Lightning Channel Backup</h2>
|
||||
<p class="text-sm text-white/60 mb-3">Export your channel state so you can restore channels on a new node. Does not include on-chain wallet seed.</p>
|
||||
<div class="flex gap-3">
|
||||
<button @click="exportChannelBackup" :disabled="exportingChannelBackup" class="glass-button px-4 py-2 rounded-lg text-sm flex items-center gap-2">
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
{{ exportingChannelBackup ? 'Exporting...' : 'Export Channel Backup' }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="channelBackupData" class="mt-3 bg-black/30 rounded-lg p-3">
|
||||
<p class="text-xs text-white/40 mb-1">{{ channelBackupChannels }} channel{{ channelBackupChannels !== 1 ? 's' : '' }} backed up at {{ channelBackupTime }}</p>
|
||||
<textarea readonly :value="channelBackupData" rows="3" class="w-full bg-black/20 text-xs font-mono text-white/60 rounded p-2 resize-none border border-white/10"></textarea>
|
||||
<button @click="copyChannelBackup" class="mt-2 glass-button px-3 py-1.5 rounded text-xs">{{ channelBackupCopied ? 'Copied!' : 'Copy Backup Data' }}</button>
|
||||
</div>
|
||||
<p v-if="channelBackupError" class="mt-2 text-xs text-red-400">{{ channelBackupError }}</p>
|
||||
</div>
|
||||
|
||||
<!-- Network Diagnostics Link -->
|
||||
<div class="glass-card px-6 py-6 mb-6">
|
||||
<div class="flex items-center justify-between">
|
||||
@ -1439,6 +1459,40 @@ interface BackupEntry {
|
||||
}
|
||||
const backupList = ref<BackupEntry[]>([])
|
||||
const loadingBackups = ref(false)
|
||||
|
||||
// Lightning channel backup
|
||||
const exportingChannelBackup = ref(false)
|
||||
const channelBackupData = ref('')
|
||||
const channelBackupChannels = ref(0)
|
||||
const channelBackupTime = ref('')
|
||||
const channelBackupError = ref('')
|
||||
const channelBackupCopied = ref(false)
|
||||
|
||||
async function exportChannelBackup() {
|
||||
exportingChannelBackup.value = true
|
||||
channelBackupError.value = ''
|
||||
try {
|
||||
const res = await rpcClient.call<{ backup: string; channel_count: number; timestamp: string }>({
|
||||
method: 'lnd.export-channel-backup',
|
||||
timeout: 15000,
|
||||
})
|
||||
channelBackupData.value = res.backup
|
||||
channelBackupChannels.value = res.channel_count
|
||||
channelBackupTime.value = new Date(res.timestamp).toLocaleString()
|
||||
} catch (err: unknown) {
|
||||
channelBackupError.value = err instanceof Error ? err.message : 'Failed to export'
|
||||
} finally {
|
||||
exportingChannelBackup.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function copyChannelBackup() {
|
||||
if (channelBackupData.value) {
|
||||
navigator.clipboard.writeText(channelBackupData.value).catch(() => {})
|
||||
channelBackupCopied.value = true
|
||||
setTimeout(() => { channelBackupCopied.value = false }, 2000)
|
||||
}
|
||||
}
|
||||
const showCreateBackupModal = ref(false)
|
||||
const backupPassphrase = ref('')
|
||||
const backupDescription = ref('')
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user