feat: Phase 4 — off-grid Bitcoin relay, block headers, dead man's switch

- Typed message dispatch in listener (BlockHeader, TxRelay, LightningRelay, Alert, TxConfirmation)
- Base64 encoding for binary payloads over LoRa (fixes NUL byte truncation)
- Compact block header announcements (88 bytes, fits 160-byte LoRa limit)
- Block header announcer: internet nodes auto-announce new blocks to Archy peers
- TX relay: mesh-only nodes can broadcast transactions via internet-connected peers
- Confirmation tracking: relay node monitors 1/3, 2/3, 3/3 confirmations, sends updates back
- Dead man's switch background task with configurable interval and signed alert broadcast
- 6 new RPC endpoints: relay-tx, block-headers, relay-lightning, deadman-status/configure/checkin
- lnd.create-raw-tx: create signed TX without broadcasting (for mesh relay)
- Web5 wallet: offline detection + "Send via mesh?" prompt with auto relay + confirmation polling
- Mesh.vue: Off-Grid Bitcoin tab, Dead Man tab, Send Bitcoin/Lightning buttons
- TX/Lightning relay sends only to Archy peers (not broadcast to all devices)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Dorian
2026-03-17 15:51:56 +00:00
co-authored by Claude Opus 4.6
parent eeb3c77e12
commit 70f1348c15
13 changed files with 2091 additions and 126 deletions
+83 -2
View File
@@ -53,7 +53,8 @@ export interface MeshMessage {
delivered: boolean
encrypted: boolean
message_type?: MeshMessageTypeLabel
typed_payload?: InvoiceData | AlertData | CoordinateData | null
// eslint-disable-next-line @typescript-eslint/no-explicit-any
typed_payload?: Record<string, any> | null
}
export interface InvoiceData {
@@ -94,6 +95,14 @@ export interface AlertStatus {
emergency_contacts: number
}
export interface BlockHeader {
height: number
hash: string
prev_hash: string
timestamp: number
announced_by: string
}
export const useMeshStore = defineStore('mesh', () => {
const status = ref<MeshStatus | null>(null)
const peers = ref<MeshPeer[]>([])
@@ -269,8 +278,71 @@ export const useMeshStore = defineStore('mesh', () => {
})
}
// ─── Phase 4: Off-Grid Bitcoin Operations ────────────────────────────
const deadmanStatus = ref<AlertStatus | null>(null)
const blockHeaders = ref<BlockHeader[]>([])
const latestBlockHeight = ref(0)
async function fetchDeadmanStatus() {
try {
deadmanStatus.value = await rpcClient.call<AlertStatus>({ method: 'mesh.deadman-status' })
} catch {
// Dead man switch not available
}
}
async function configureDeadman(config: {
enabled?: boolean
interval_secs?: number
lat?: number
lng?: number
label?: string
contacts?: string[]
custom_message?: string
auto_gps?: boolean
}) {
return rpcClient.call<AlertStatus>({
method: 'mesh.deadman-configure',
params: config,
})
}
async function deadmanCheckin() {
return rpcClient.call<{ checked_in: boolean; time_remaining_secs: number }>({
method: 'mesh.deadman-checkin',
})
}
async function fetchBlockHeaders(count = 10) {
try {
const res = await rpcClient.call<{ headers: BlockHeader[]; latest_height: number; count: number }>({
method: 'mesh.block-headers',
params: { count },
})
blockHeaders.value = res.headers
latestBlockHeight.value = res.latest_height
} catch {
// Block headers not available
}
}
async function relayTransaction(txHex: string) {
return rpcClient.call<{ request_id: number; queued: boolean; tx_hex_len: number }>({
method: 'mesh.relay-tx',
params: { tx_hex: txHex },
})
}
async function relayLightning(bolt11: string, amountSats: number) {
return rpcClient.call<{ request_id: number; queued: boolean; amount_sats: number }>({
method: 'mesh.relay-lightning',
params: { bolt11, amount_sats: amountSats },
})
}
async function refreshAll() {
await Promise.all([fetchStatus(), fetchPeers(), fetchMessages()])
await Promise.all([fetchStatus(), fetchPeers(), fetchMessages(), fetchDeadmanStatus(), fetchBlockHeaders()])
}
return {
@@ -282,6 +354,9 @@ export const useMeshStore = defineStore('mesh', () => {
sending,
unreadCounts,
totalUnread,
deadmanStatus,
blockHeaders,
latestBlockHeight,
fetchStatus,
fetchPeers,
fetchMessages,
@@ -296,5 +371,11 @@ export const useMeshStore = defineStore('mesh', () => {
sendAlert,
getSessionStatus,
rotatePrekeys,
fetchDeadmanStatus,
configureDeadman,
deadmanCheckin,
fetchBlockHeaders,
relayTransaction,
relayLightning,
}
})