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
parent d1ac098edb
commit f273816405
48 changed files with 3432 additions and 438 deletions

View File

@@ -35,6 +35,8 @@ const relayingLn = ref(false)
const relayResult = ref('')
const meshSendAddr = ref('')
const meshSendAmount = ref('')
const relayMode = ref<'archy' | 'broadcast'>('archy')
const sendTab = ref<'onchain' | 'lightning'>('onchain')
const deadmanConfiguring = ref(false)
const deadmanInterval = ref('21600')
const deadmanEnabled = ref(false)
@@ -72,11 +74,15 @@ async function handleMeshSendBitcoin() {
params: { addr: meshSendAddr.value.trim(), amount_sats: parseInt(meshSendAmount.value) },
})
// Step 2: Relay via mesh
relayResult.value = 'Sending via mesh radio...'
const relayRes = await mesh.relayTransaction(rawRes.raw_tx_hex)
relayResult.value = `Sent via mesh! Request #${relayRes.request_id} — waiting for broadcast confirmation from peers`
relayResult.value = relayMode.value === 'broadcast'
? 'Broadcasting via mesh network...'
: 'Sending to Archy peers (encrypted)...'
const relayRes = await mesh.relayTransaction(rawRes.raw_tx_hex, relayMode.value)
relayResult.value = `Sent via mesh! Request #${relayRes.request_id} — waiting for relay peer to broadcast...`
meshSendAddr.value = ''
meshSendAmount.value = ''
// Step 3: Poll for relay result (every 3s for 90s)
pollRelayStatus(relayRes.request_id)
} catch (err: unknown) {
relayResult.value = err instanceof Error ? err.message : 'Send failed'
} finally {
@@ -84,6 +90,30 @@ async function handleMeshSendBitcoin() {
}
}
function pollRelayStatus(requestId: number) {
let attempts = 0
const maxAttempts = 30
const interval = setInterval(async () => {
attempts++
try {
const res = await mesh.relayStatus(requestId)
if (res.status === 'confirmed' && res.txid) {
relayResult.value = `TX broadcast! txid: ${res.txid.slice(0, 8)}...${res.txid.slice(-8)}`
clearInterval(interval)
} else if (res.status === 'failed') {
const code = res.error_code ? ` [${res.error_code}]` : ''
relayResult.value = `Relay failed${code}: ${res.error || 'unknown error'}`
clearInterval(interval)
} else if (attempts >= maxAttempts) {
relayResult.value += ' (timed out waiting for confirmation)'
clearInterval(interval)
}
} catch {
if (attempts >= maxAttempts) clearInterval(interval)
}
}, 3000)
}
async function handleRelayTx() {
if (!txHexInput.value.trim()) return
relayingTx.value = true
@@ -221,6 +251,7 @@ function openChat(peer: MeshPeer) {
activeChatChannel.value = null
sendError.value = ''
messageText.value = ''
activeTab.value = 'chat'
mesh.markChatRead(peer.contact_id)
nextTick(() => scrollChatToBottom())
}
@@ -230,6 +261,7 @@ function openChannelChat(channel: { index: number; name: string }) {
activeChatPeer.value = null
sendError.value = ''
messageText.value = ''
activeTab.value = 'chat'
nextTick(() => scrollChatToBottom())
}
@@ -468,6 +500,11 @@ function truncatePubkey(hex: string | null): string {
<h3 class="mesh-panel-title">Off-Grid Bitcoin</h3>
<p class="mesh-panel-sub">Relay transactions and receive block headers via mesh radio</p>
<!-- Relay status notification -->
<div v-if="relayResult" class="mesh-relay-result" :class="relayResult.includes('failed') || relayResult.includes('Failed') ? 'error' : 'success'">
{{ relayResult }}
</div>
<!-- Block Headers -->
<div class="mesh-bitcoin-section">
<div class="mesh-bitcoin-section-header">
@@ -483,46 +520,61 @@ function truncatePubkey(hex: string | null): string {
</div>
</div>
<!-- Send Bitcoin (creates TX + auto-relays) -->
<div class="mesh-bitcoin-section">
<div class="mesh-bitcoin-section-header">
<span class="mesh-bitcoin-label">Send Bitcoin (Off-Grid)</span>
</div>
<!-- On-Chain / Lightning tabs -->
<div class="mesh-send-tabs">
<button class="mesh-send-tab" :class="{ active: sendTab === 'onchain' }" @click="sendTab = 'onchain'">On-Chain</button>
<button class="mesh-send-tab" :class="{ active: sendTab === 'lightning' }" @click="sendTab = 'lightning'">Lightning</button>
</div>
<!-- On-Chain tab -->
<div v-if="sendTab === 'onchain'" class="mesh-bitcoin-section">
<p class="mesh-bitcoin-hint">Creates a signed transaction locally and relays via mesh peers</p>
<input v-model="meshSendAddr" class="mesh-bitcoin-input" placeholder="Bitcoin address (bc1...)" />
<input v-model="meshSendAmount" class="mesh-bitcoin-input mesh-bitcoin-input-sm" type="number" placeholder="Amount (sats)" min="546" />
<div class="mesh-relay-mode">
<label class="mesh-relay-mode-option" :class="{ active: relayMode === 'archy' }">
<input type="radio" v-model="relayMode" value="archy" />
<span>Archy Peers <small>(E2E encrypted, direct)</small></span>
</label>
<label class="mesh-relay-mode-option" :class="{ active: relayMode === 'broadcast' }">
<input type="radio" v-model="relayMode" value="broadcast" />
<span>Mesh Broadcast <small>(multi-hop, wider reach)</small></span>
</label>
</div>
<button class="glass-button" :disabled="!meshSendAddr.trim() || !meshSendAmount || relayingTx" @click="handleMeshSendBitcoin">
{{ relayingTx ? 'Sending...' : 'Send Bitcoin via Mesh' }}
{{ relayingTx ? 'Sending...' : 'Send via Mesh' }}
</button>
<details class="mesh-bitcoin-advanced">
<summary class="mesh-bitcoin-label">Raw TX Relay</summary>
<div style="margin-top: 8px;">
<textarea v-model="txHexInput" class="mesh-bitcoin-input" placeholder="Paste raw transaction hex..." rows="3" />
<button class="glass-button" :disabled="!txHexInput.trim() || relayingTx" @click="handleRelayTx">
{{ relayingTx ? 'Relaying...' : 'Relay Raw TX' }}
</button>
</div>
</details>
</div>
<!-- Send Lightning (auto-relays invoice) -->
<div class="mesh-bitcoin-section">
<div class="mesh-bitcoin-section-header">
<span class="mesh-bitcoin-label">Pay Lightning Invoice (Off-Grid)</span>
</div>
<!-- Lightning tab -->
<div v-if="sendTab === 'lightning'" class="mesh-bitcoin-section">
<p class="mesh-bitcoin-hint">Relays a Lightning invoice to an internet-connected peer for payment</p>
<input v-model="bolt11Input" class="mesh-bitcoin-input" placeholder="lnbc... (bolt11 invoice)" />
<input v-model="bolt11AmountInput" class="mesh-bitcoin-input mesh-bitcoin-input-sm" type="number" placeholder="Amount (sats)" />
<div class="mesh-relay-mode">
<label class="mesh-relay-mode-option" :class="{ active: relayMode === 'archy' }">
<input type="radio" v-model="relayMode" value="archy" />
<span>Archy Peers <small>(E2E encrypted, direct)</small></span>
</label>
<label class="mesh-relay-mode-option" :class="{ active: relayMode === 'broadcast' }">
<input type="radio" v-model="relayMode" value="broadcast" />
<span>Mesh Broadcast <small>(multi-hop, wider reach)</small></span>
</label>
</div>
<button class="glass-button" :disabled="!bolt11Input.trim() || !bolt11AmountInput || relayingLn" @click="handleRelayLightning">
{{ relayingLn ? 'Relaying...' : 'Pay via Mesh' }}
</button>
</div>
<!-- Advanced: Raw TX Relay -->
<details class="mesh-bitcoin-advanced">
<summary class="mesh-bitcoin-label">Advanced: Raw TX Relay</summary>
<div class="mesh-bitcoin-section" style="margin-top: 8px;">
<textarea v-model="txHexInput" class="mesh-bitcoin-input" placeholder="Paste raw transaction hex..." rows="3" />
<button class="glass-button" :disabled="!txHexInput.trim() || relayingTx" @click="handleRelayTx">
{{ relayingTx ? 'Relaying...' : 'Relay Raw TX' }}
</button>
</div>
</details>
<div v-if="relayResult" class="mesh-relay-result" :class="relayResult.includes('failed') ? 'error' : 'success'">
{{ relayResult }}
</div>
</div>
<!-- Dead Man's Switch Panel -->
@@ -790,7 +842,7 @@ function truncatePubkey(hex: string | null): string {
flex-direction: column;
gap: 12px;
min-height: 0;
overflow: hidden;
overflow-y: auto;
}
.mesh-right {
@@ -800,6 +852,7 @@ function truncatePubkey(hex: string | null): string {
display: flex;
flex-direction: column;
gap: 12px;
overflow: hidden;
}
/* ─── Status card ─── */
@@ -1493,6 +1546,59 @@ function truncatePubkey(hex: string | null): string {
.mesh-bitcoin-input::placeholder { color: rgba(255,255,255,0.3); }
.mesh-bitcoin-input:focus { outline: none; border-color: rgba(251,146,60,0.4); }
.mesh-bitcoin-input-sm { max-width: 200px; }
.mesh-relay-mode {
display: flex;
gap: 8px;
margin: 8px 0;
}
.mesh-relay-mode-option {
flex: 1;
display: flex;
align-items: center;
gap: 6px;
padding: 8px 10px;
border-radius: 8px;
border: 1px solid rgba(255,255,255,0.1);
background: rgba(0,0,0,0.2);
cursor: pointer;
font-size: 0.78rem;
color: rgba(255,255,255,0.6);
transition: all 0.2s ease;
}
.mesh-relay-mode-option:hover { border-color: rgba(255,255,255,0.2); }
.mesh-relay-mode-option.active {
border-color: rgba(251,146,60,0.4);
background: rgba(251,146,60,0.08);
color: rgba(255,255,255,0.9);
}
.mesh-relay-mode-option input[type="radio"] { display: none; }
.mesh-relay-mode-icon { font-size: 1rem; }
.mesh-relay-mode-option small { display: block; font-size: 0.65rem; color: rgba(255,255,255,0.4); }
.mesh-send-tabs {
display: flex;
gap: 0;
margin-bottom: 12px;
border-radius: 8px;
overflow: hidden;
border: 1px solid rgba(255,255,255,0.1);
}
.mesh-send-tab {
flex: 1;
padding: 8px 16px;
background: rgba(0,0,0,0.2);
color: rgba(255,255,255,0.5);
border: none;
cursor: pointer;
font-size: 0.82rem;
font-weight: 600;
transition: all 0.2s ease;
}
.mesh-send-tab:first-child { border-right: 1px solid rgba(255,255,255,0.1); }
.mesh-send-tab:hover { color: rgba(255,255,255,0.7); background: rgba(255,255,255,0.03); }
.mesh-send-tab.active {
color: #fb923c;
background: rgba(251,146,60,0.08);
}
.mesh-block-list { display: flex; flex-direction: column; gap: 4px; }
.mesh-block-row {
display: flex;