feat: v1.2.0-alpha — E2E encrypted mesh relay, steganography, relay status polling

Phase 5 mesh networking:
- E2E encrypted TX relay (X25519 + ChaCha20-Poly1305) — non-Archy nodes
  relay encrypted blobs transparently via Meshcore native routing
- Steganographic encoding modes (WeatherStation, SensorNetwork) — traffic
  looks like sensor data on the wire, 0xAA marker, configurable per-node
- Pre-flight Bitcoin Core health check on relay node — specific error codes
  (bitcoin_unreachable, bitcoin_syncing, tx_rejected) instead of generic fails
- mesh.relay-status RPC endpoint — frontend polls for relay result every 3s
- On-Chain / Lightning tabs in Off-Grid Bitcoin panel
- Archy Peers vs Mesh Broadcast relay mode selector
- Mesh view fills viewport (no page scroll), internal panel scrolling
- Version bump to 1.2.0-alpha

Also includes: deploy hardening, container fixes, IndeedHub updates,
boot screen, dashboard improvements, MASTER_PLAN task tracking

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-17 23:56:37 +00:00
co-authored by Claude Opus 4.6
parent d1ac098edb
commit f273816405
48 changed files with 3432 additions and 438 deletions
@@ -93,6 +93,8 @@ pub struct RelayTracker {
tx_requests: RwLock<HashMap<u64, PendingRelay>>,
/// Pending Lightning relay requests.
lightning_requests: RwLock<HashMap<u64, PendingRelay>>,
/// Completed relay results (kept for 5 minutes for frontend polling).
completed_results: RwLock<Vec<RelayResult>>,
}
#[derive(Debug, Clone)]
@@ -101,11 +103,22 @@ struct PendingRelay {
created_at: String,
}
/// Result of a completed relay attempt, stored for frontend polling.
#[derive(Debug, Clone, serde::Serialize)]
pub struct RelayResult {
pub request_id: u64,
pub txid: Option<String>,
pub error: Option<String>,
pub error_code: Option<String>,
pub completed_at: String,
}
impl RelayTracker {
pub fn new() -> Self {
Self {
tx_requests: RwLock::new(HashMap::new()),
lightning_requests: RwLock::new(HashMap::new()),
completed_results: RwLock::new(Vec::new()),
}
}
@@ -155,6 +168,31 @@ impl RelayTracker {
let ln = self.lightning_requests.read().await.len();
(tx, ln)
}
/// Store a completed relay result for frontend polling.
pub async fn store_result(&self, result: RelayResult) {
let mut results = self.completed_results.write().await;
// Evict results older than 5 minutes
let cutoff = chrono::Utc::now() - chrono::Duration::minutes(5);
let cutoff_str = cutoff.to_rfc3339();
results.retain(|r| r.completed_at > cutoff_str);
results.push(result);
}
/// Get relay result by request_id (returns None if not yet completed or expired).
pub async fn get_result(&self, request_id: u64) -> Option<RelayResult> {
self.completed_results
.read()
.await
.iter()
.find(|r| r.request_id == request_id)
.cloned()
}
/// Check if a TX relay request is still pending.
pub async fn is_pending(&self, request_id: u64) -> bool {
self.tx_requests.read().await.contains_key(&request_id)
}
}
impl Default for RelayTracker {
@@ -220,11 +258,13 @@ pub fn build_tx_relay_response(
request_id: u64,
txid: Option<&str>,
error: Option<&str>,
error_code: Option<&str>,
) -> Result<Vec<u8>> {
let payload = message_types::encode_payload(&TxRelayResponsePayload {
request_id,
txid: txid.map(|s| s.to_string()),
error: error.map(|s| s.to_string()),
error_code: error_code.map(|s| s.to_string()),
})?;
let envelope = TypedEnvelope::new(MeshMessageType::TxRelayResponse, payload);
envelope.to_wire()