2026-03-17 00:03:08 +00:00
|
|
|
// Pinia store for mesh networking state (Meshcore LoRa)
|
|
|
|
|
import { defineStore } from 'pinia'
|
|
|
|
|
import { ref, computed } from 'vue'
|
|
|
|
|
import { rpcClient } from '@/api/rpc-client'
|
|
|
|
|
|
|
|
|
|
export interface MeshStatus {
|
|
|
|
|
enabled: boolean
|
|
|
|
|
device_type: string
|
|
|
|
|
device_path: string | null
|
|
|
|
|
device_connected: boolean
|
|
|
|
|
firmware_version: string | null
|
|
|
|
|
self_node_id: number | null
|
|
|
|
|
self_advert_name: string | null
|
|
|
|
|
peer_count: number
|
|
|
|
|
channel_name: string
|
|
|
|
|
messages_sent: number
|
|
|
|
|
messages_received: number
|
|
|
|
|
detected_devices?: string[]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface MeshPeer {
|
|
|
|
|
contact_id: number
|
|
|
|
|
advert_name: string
|
|
|
|
|
did: string | null
|
|
|
|
|
pubkey_hex: string | null
|
|
|
|
|
rssi: number | null
|
|
|
|
|
snr: number | null
|
|
|
|
|
last_heard: string
|
|
|
|
|
hops: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface MeshChannel {
|
|
|
|
|
index: number
|
|
|
|
|
name: string
|
|
|
|
|
has_secret: boolean
|
|
|
|
|
}
|
|
|
|
|
|
feat: Phase 3 Week 7 — typed message UI, session badges, rich chat cards
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:34:37 +00:00
|
|
|
export type MeshMessageTypeLabel =
|
|
|
|
|
| 'text'
|
|
|
|
|
| 'alert'
|
|
|
|
|
| 'invoice'
|
|
|
|
|
| 'psbt_hash'
|
|
|
|
|
| 'coordinate'
|
|
|
|
|
| 'block_header'
|
2026-04-13 08:01:21 -04:00
|
|
|
| 'tx_relay'
|
|
|
|
|
| 'tx_relay_response'
|
|
|
|
|
| 'tx_confirmation'
|
|
|
|
|
| 'lightning_relay'
|
|
|
|
|
| 'lightning_relay_response'
|
2026-04-13 11:10:59 -04:00
|
|
|
| 'content_ref'
|
2026-04-13 13:19:30 -04:00
|
|
|
| 'reply'
|
|
|
|
|
| 'reaction'
|
2026-04-14 10:24:27 -04:00
|
|
|
| 'read_receipt'
|
|
|
|
|
| 'forward'
|
|
|
|
|
| 'edit'
|
|
|
|
|
| 'delete'
|
|
|
|
|
| 'presence'
|
|
|
|
|
| 'channel_invite'
|
|
|
|
|
| 'contact_card'
|
feat: Phase 3 Week 7 — typed message UI, session badges, rich chat cards
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:34:37 +00:00
|
|
|
|
2026-03-17 00:03:08 +00:00
|
|
|
export interface MeshMessage {
|
|
|
|
|
id: number
|
|
|
|
|
direction: 'sent' | 'received'
|
|
|
|
|
peer_contact_id: number
|
|
|
|
|
peer_name: string | null
|
|
|
|
|
plaintext: string
|
|
|
|
|
timestamp: string
|
|
|
|
|
delivered: boolean
|
|
|
|
|
encrypted: boolean
|
feat: Phase 3 Week 7 — typed message UI, session badges, rich chat cards
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:34:37 +00:00
|
|
|
message_type?: MeshMessageTypeLabel
|
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>
2026-03-17 15:51:56 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
typed_payload?: Record<string, any> | null
|
2026-04-13 13:19:30 -04:00
|
|
|
/// Cross-transport identity for this message — (sender_pubkey, sender_seq)
|
|
|
|
|
/// forms a stable MessageKey used by replies/reactions.
|
|
|
|
|
sender_pubkey?: string | null
|
|
|
|
|
sender_seq?: number | null
|
feat: Phase 3 Week 7 — typed message UI, session badges, rich chat cards
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:34:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface InvoiceData {
|
|
|
|
|
bolt11: string
|
|
|
|
|
amount_sats: number
|
|
|
|
|
memo: string | null
|
|
|
|
|
payment_hash?: string
|
|
|
|
|
paid?: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AlertData {
|
|
|
|
|
alert_type: 'emergency' | 'status' | 'dead_man' | 'block_header'
|
|
|
|
|
message: string
|
|
|
|
|
coordinate?: { lat: number; lng: number; label?: string }
|
|
|
|
|
signed?: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface CoordinateData {
|
|
|
|
|
lat: number
|
|
|
|
|
lng: number
|
|
|
|
|
label?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface SessionStatus {
|
|
|
|
|
has_session: boolean
|
|
|
|
|
forward_secrecy: boolean
|
|
|
|
|
message_count: number
|
|
|
|
|
ratchet_generation: number
|
|
|
|
|
peer_did: string | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface AlertStatus {
|
|
|
|
|
dead_man_enabled: boolean
|
|
|
|
|
dead_man_interval_secs: number
|
|
|
|
|
triggered: boolean
|
|
|
|
|
time_remaining_secs: number
|
|
|
|
|
has_gps: boolean
|
|
|
|
|
emergency_contacts: number
|
2026-03-17 00:03:08 +00:00
|
|
|
}
|
|
|
|
|
|
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>
2026-03-17 15:51:56 +00:00
|
|
|
export interface BlockHeader {
|
|
|
|
|
height: number
|
|
|
|
|
hash: string
|
|
|
|
|
prev_hash: string
|
|
|
|
|
timestamp: number
|
|
|
|
|
announced_by: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 16:12:01 +00:00
|
|
|
export interface NodePosition {
|
|
|
|
|
lat: number
|
|
|
|
|
lng: number
|
|
|
|
|
label?: string
|
|
|
|
|
timestamp: string
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 00:03:08 +00:00
|
|
|
export const useMeshStore = defineStore('mesh', () => {
|
|
|
|
|
const status = ref<MeshStatus | null>(null)
|
|
|
|
|
const peers = ref<MeshPeer[]>([])
|
|
|
|
|
const messages = ref<MeshMessage[]>([])
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
const error = ref<string | null>(null)
|
|
|
|
|
const sending = ref(false)
|
|
|
|
|
|
2026-03-21 01:11:05 +00:00
|
|
|
// Serialize send operations to prevent concurrent fetchMessages() races
|
|
|
|
|
let sendQueue: Promise<void> = Promise.resolve()
|
|
|
|
|
|
2026-03-19 16:12:01 +00:00
|
|
|
// Node position tracking for map view (contact_id -> position)
|
|
|
|
|
const nodePositions = ref<Map<number, NodePosition>>(new Map())
|
|
|
|
|
|
2026-03-17 00:03:08 +00:00
|
|
|
// Track unread message counts per peer (contact_id -> count)
|
|
|
|
|
const unreadCounts = ref<Record<number, number>>({})
|
|
|
|
|
// Currently viewing chat for this contact_id (clears unread)
|
|
|
|
|
const viewingChatId = ref<number | null>(null)
|
|
|
|
|
// Total unread count for nav badge
|
|
|
|
|
const totalUnread = computed(() =>
|
|
|
|
|
Object.values(unreadCounts.value).reduce((a, b) => a + b, 0)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
async function fetchStatus() {
|
|
|
|
|
try {
|
|
|
|
|
loading.value = true
|
|
|
|
|
error.value = null
|
|
|
|
|
const res = await rpcClient.call<MeshStatus>({ method: 'mesh.status' })
|
|
|
|
|
status.value = res
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to fetch mesh status'
|
|
|
|
|
} finally {
|
|
|
|
|
loading.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchPeers() {
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ peers: MeshPeer[]; count: number }>({
|
|
|
|
|
method: 'mesh.peers',
|
|
|
|
|
})
|
|
|
|
|
peers.value = res.peers
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to fetch mesh peers'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function fetchMessages(limit?: number) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ messages: MeshMessage[]; count: number }>({
|
|
|
|
|
method: 'mesh.messages',
|
|
|
|
|
params: limit ? { limit } : {},
|
|
|
|
|
})
|
|
|
|
|
// Detect new incoming messages and increment unread counts
|
|
|
|
|
const newMsgs = res.messages.filter(
|
|
|
|
|
m => m.direction === 'received' && !messages.value.some(existing => existing.id === m.id)
|
|
|
|
|
)
|
|
|
|
|
for (const msg of newMsgs) {
|
|
|
|
|
// Don't count as unread if we're currently viewing that chat
|
|
|
|
|
if (msg.peer_contact_id !== viewingChatId.value) {
|
|
|
|
|
unreadCounts.value[msg.peer_contact_id] = (unreadCounts.value[msg.peer_contact_id] || 0) + 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
messages.value = res.messages
|
2026-03-19 16:12:01 +00:00
|
|
|
// Extract node positions from coordinate messages
|
|
|
|
|
updateNodePositionsFromMessages(res.messages)
|
2026-03-17 00:03:08 +00:00
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to fetch mesh messages'
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 16:12:01 +00:00
|
|
|
// Convert microdegrees (from mesh protocol) to degrees for Leaflet
|
|
|
|
|
// Values > 90 for lat or > 180 for lng indicate microdegrees
|
|
|
|
|
function toDegreesIfMicro(lat: number, lng: number): { lat: number; lng: number } {
|
|
|
|
|
if (Math.abs(lat) > 90 || Math.abs(lng) > 180) {
|
|
|
|
|
return { lat: lat / 1000000, lng: lng / 1000000 }
|
|
|
|
|
}
|
|
|
|
|
return { lat, lng }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateNodePositionsFromMessages(msgs: MeshMessage[]) {
|
|
|
|
|
for (const msg of msgs) {
|
|
|
|
|
if (msg.message_type === 'coordinate' && msg.typed_payload) {
|
|
|
|
|
const payload = msg.typed_payload as CoordinateData
|
|
|
|
|
if (typeof payload.lat === 'number' && typeof payload.lng === 'number') {
|
|
|
|
|
const existing = nodePositions.value.get(msg.peer_contact_id)
|
|
|
|
|
if (!existing || msg.timestamp > existing.timestamp) {
|
|
|
|
|
const deg = toDegreesIfMicro(payload.lat, payload.lng)
|
|
|
|
|
nodePositions.value.set(msg.peer_contact_id, {
|
|
|
|
|
lat: deg.lat,
|
|
|
|
|
lng: deg.lng,
|
|
|
|
|
label: payload.label,
|
|
|
|
|
timestamp: msg.timestamp,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Also extract coordinates from alert messages that include location
|
|
|
|
|
if (msg.message_type === 'alert' && msg.typed_payload) {
|
|
|
|
|
const payload = msg.typed_payload as AlertData
|
|
|
|
|
if (payload.coordinate && typeof payload.coordinate.lat === 'number' && typeof payload.coordinate.lng === 'number') {
|
|
|
|
|
const existing = nodePositions.value.get(msg.peer_contact_id)
|
|
|
|
|
if (!existing || msg.timestamp > existing.timestamp) {
|
|
|
|
|
const deg = toDegreesIfMicro(payload.coordinate.lat, payload.coordinate.lng)
|
|
|
|
|
nodePositions.value.set(msg.peer_contact_id, {
|
|
|
|
|
lat: deg.lat,
|
|
|
|
|
lng: deg.lng,
|
|
|
|
|
label: payload.coordinate.label,
|
|
|
|
|
timestamp: msg.timestamp,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function getNodePositions(): Map<number, NodePosition> {
|
|
|
|
|
return nodePositions.value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Update self node position from deadman GPS data (contact_id = -1 for self)
|
|
|
|
|
function updateSelfPosition(lat: number, lng: number, label?: string) {
|
|
|
|
|
nodePositions.value.set(-1, {
|
|
|
|
|
lat,
|
|
|
|
|
lng,
|
|
|
|
|
label: label ?? 'This Node',
|
|
|
|
|
timestamp: new Date().toISOString(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 00:03:08 +00:00
|
|
|
function markChatRead(contactId: number) {
|
|
|
|
|
viewingChatId.value = contactId
|
|
|
|
|
delete unreadCounts.value[contactId]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearViewingChat() {
|
|
|
|
|
viewingChatId.value = null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sendMessage(contactId: number, message: string) {
|
2026-03-21 01:11:05 +00:00
|
|
|
const doSend = async () => {
|
|
|
|
|
try {
|
|
|
|
|
sending.value = true
|
|
|
|
|
error.value = null
|
|
|
|
|
const res = await rpcClient.call<{ sent: boolean; message_id: number; encrypted: boolean }>({
|
|
|
|
|
method: 'mesh.send',
|
|
|
|
|
params: { contact_id: contactId, message: message.trim() },
|
|
|
|
|
})
|
|
|
|
|
// Refresh messages after sending
|
|
|
|
|
if (res.sent) {
|
|
|
|
|
await fetchMessages()
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to send mesh message'
|
|
|
|
|
throw err
|
|
|
|
|
} finally {
|
|
|
|
|
sending.value = false
|
2026-03-17 00:03:08 +00:00
|
|
|
}
|
|
|
|
|
}
|
2026-03-21 01:11:05 +00:00
|
|
|
// Chain onto send queue to prevent concurrent fetchMessages() calls
|
|
|
|
|
const result = sendQueue.then(doSend, doSend)
|
|
|
|
|
sendQueue = result.then(() => {}, () => {})
|
|
|
|
|
return result
|
2026-03-17 00:03:08 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-12 12:11:00 -04:00
|
|
|
async function sendChannelMessage(channel: number, message: string) {
|
|
|
|
|
const doSend = async () => {
|
|
|
|
|
try {
|
|
|
|
|
sending.value = true
|
|
|
|
|
error.value = null
|
|
|
|
|
const res = await rpcClient.call<{ sent: boolean; message_id: number; channel: number }>({
|
|
|
|
|
method: 'mesh.send-channel',
|
|
|
|
|
params: { channel, message: message.trim() },
|
|
|
|
|
})
|
|
|
|
|
if (res.sent) {
|
|
|
|
|
await fetchMessages()
|
|
|
|
|
}
|
|
|
|
|
return res
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to send channel message'
|
|
|
|
|
throw err
|
|
|
|
|
} finally {
|
|
|
|
|
sending.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
const result = sendQueue.then(doSend, doSend)
|
|
|
|
|
sendQueue = result.then(() => {}, () => {})
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 00:03:08 +00:00
|
|
|
async function broadcastIdentity() {
|
|
|
|
|
try {
|
|
|
|
|
error.value = null
|
|
|
|
|
await rpcClient.call<{ broadcast: boolean }>({ method: 'mesh.broadcast' })
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to broadcast identity'
|
|
|
|
|
throw err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function configure(config: Partial<MeshStatus>) {
|
|
|
|
|
try {
|
|
|
|
|
error.value = null
|
|
|
|
|
await rpcClient.call<{ configured: boolean }>({
|
|
|
|
|
method: 'mesh.configure',
|
|
|
|
|
params: config,
|
|
|
|
|
})
|
|
|
|
|
await fetchStatus()
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to configure mesh'
|
|
|
|
|
throw err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat: Phase 3 Week 7 — typed message UI, session badges, rich chat cards
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:34:37 +00:00
|
|
|
async function sendInvoice(contactId: number, amountSats: number, memo?: string) {
|
|
|
|
|
try {
|
|
|
|
|
sending.value = true
|
|
|
|
|
error.value = null
|
|
|
|
|
return await rpcClient.call<{ sent: boolean; amount_sats: number; bolt11: string }>({
|
|
|
|
|
method: 'mesh.send-invoice',
|
|
|
|
|
params: { contact_id: contactId, amount_sats: amountSats, memo },
|
|
|
|
|
})
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to send invoice'
|
|
|
|
|
throw err
|
|
|
|
|
} finally {
|
|
|
|
|
sending.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sendCoordinate(contactId: number, lat: number, lng: number, label?: string) {
|
|
|
|
|
try {
|
|
|
|
|
sending.value = true
|
|
|
|
|
error.value = null
|
|
|
|
|
return await rpcClient.call<{ sent: boolean }>({
|
|
|
|
|
method: 'mesh.send-coordinate',
|
|
|
|
|
params: { contact_id: contactId, lat, lng, label },
|
|
|
|
|
})
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to send coordinate'
|
|
|
|
|
throw err
|
|
|
|
|
} finally {
|
|
|
|
|
sending.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sendAlert(message: string, alertType: string, broadcast = false, lat?: number, lng?: number) {
|
|
|
|
|
try {
|
|
|
|
|
error.value = null
|
|
|
|
|
return await rpcClient.call<{ sent: boolean; signed: boolean }>({
|
|
|
|
|
method: 'mesh.send-alert',
|
|
|
|
|
params: { message, alert_type: alertType, broadcast, lat, lng },
|
|
|
|
|
})
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to send alert'
|
|
|
|
|
throw err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 14:13:36 -04:00
|
|
|
async function sendContent(contactId: number, cid: string, caption?: string, peerOnion?: string) {
|
2026-04-13 11:10:59 -04:00
|
|
|
try {
|
|
|
|
|
sending.value = true
|
|
|
|
|
error.value = null
|
|
|
|
|
const res = await rpcClient.call<{ sent: boolean; message_id: number; cid: string; size: number }>({
|
|
|
|
|
method: 'mesh.send-content',
|
2026-04-13 14:13:36 -04:00
|
|
|
params: { contact_id: contactId, cid, caption, peer_onion: peerOnion },
|
2026-04-13 11:10:59 -04:00
|
|
|
})
|
|
|
|
|
if (res.sent) await fetchMessages()
|
|
|
|
|
return res
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to send content'
|
|
|
|
|
throw err
|
|
|
|
|
} finally {
|
|
|
|
|
sending.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 13:19:30 -04:00
|
|
|
async function sendReply(contactId: number, targetPubkey: string, targetSeq: number, text: string) {
|
|
|
|
|
sending.value = true
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ sent: boolean; message_id: number; sender_seq: number }>({
|
|
|
|
|
method: 'mesh.send-reply',
|
|
|
|
|
params: { contact_id: contactId, target_pubkey: targetPubkey, target_seq: targetSeq, text },
|
|
|
|
|
})
|
|
|
|
|
if (res.sent) await fetchMessages()
|
|
|
|
|
return res
|
|
|
|
|
} finally {
|
|
|
|
|
sending.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sendReaction(contactId: number, targetPubkey: string, targetSeq: number, emoji: string) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ sent: boolean; message_id: number; sender_seq: number }>({
|
|
|
|
|
method: 'mesh.send-reaction',
|
|
|
|
|
params: { contact_id: contactId, target_pubkey: targetPubkey, target_seq: targetSeq, emoji },
|
|
|
|
|
})
|
|
|
|
|
if (res.sent) await fetchMessages()
|
|
|
|
|
return res
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to send reaction'
|
|
|
|
|
throw err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 18:50:08 -04:00
|
|
|
async function getOutbox() {
|
|
|
|
|
try {
|
|
|
|
|
return await rpcClient.call<{ count: number; messages?: unknown[] }>({ method: 'mesh.outbox' })
|
|
|
|
|
} catch {
|
|
|
|
|
return { count: 0 }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function sendReadReceipt(contactId: number, targetPubkey: string, targetSeq: number) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ sent: boolean }>({
|
|
|
|
|
method: 'mesh.send-read-receipt',
|
|
|
|
|
params: { contact_id: contactId, target_pubkey: targetPubkey, target_seq: targetSeq },
|
|
|
|
|
})
|
|
|
|
|
return res
|
|
|
|
|
} catch {
|
|
|
|
|
// Read receipts are best-effort — never surface errors to the user.
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function forwardMessage(contactId: number, sourceMessageId: number) {
|
|
|
|
|
sending.value = true
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ sent: boolean; message_id: number }>({
|
|
|
|
|
method: 'mesh.forward-message',
|
|
|
|
|
params: { contact_id: contactId, source_message_id: sourceMessageId },
|
|
|
|
|
})
|
|
|
|
|
if (res.sent) await fetchMessages()
|
|
|
|
|
return res
|
|
|
|
|
} finally {
|
|
|
|
|
sending.value = false
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function editMessage(contactId: number, targetSeq: number, newText: string) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ sent: boolean; message_id: number }>({
|
|
|
|
|
method: 'mesh.edit-message',
|
|
|
|
|
params: { contact_id: contactId, target_seq: targetSeq, new_text: newText },
|
|
|
|
|
})
|
|
|
|
|
if (res.sent) await fetchMessages()
|
|
|
|
|
return res
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to edit message'
|
|
|
|
|
throw err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteMessage(contactId: number, targetSeq: number) {
|
|
|
|
|
try {
|
|
|
|
|
const res = await rpcClient.call<{ sent: boolean; message_id: number }>({
|
|
|
|
|
method: 'mesh.delete-message',
|
|
|
|
|
params: { contact_id: contactId, target_seq: targetSeq },
|
|
|
|
|
})
|
|
|
|
|
if (res.sent) await fetchMessages()
|
|
|
|
|
return res
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
error.value = err instanceof Error ? err.message : 'Failed to delete message'
|
|
|
|
|
throw err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-13 11:10:59 -04:00
|
|
|
async function fetchContent(params: {
|
|
|
|
|
cid: string
|
|
|
|
|
sender_onion: string
|
|
|
|
|
cap_token: string
|
|
|
|
|
cap_exp: number
|
|
|
|
|
mime?: string
|
|
|
|
|
filename?: string
|
|
|
|
|
}) {
|
|
|
|
|
return rpcClient.call<{ fetched: boolean; cached: boolean; cid: string; size?: number; mime?: string }>({
|
|
|
|
|
method: 'mesh.fetch-content',
|
|
|
|
|
params,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
feat: Phase 3 Week 7 — typed message UI, session badges, rich chat cards
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:34:37 +00:00
|
|
|
async function getSessionStatus(contactId: number): Promise<SessionStatus> {
|
|
|
|
|
return rpcClient.call<SessionStatus>({
|
|
|
|
|
method: 'mesh.session-status',
|
|
|
|
|
params: { contact_id: contactId },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function rotatePrekeys() {
|
|
|
|
|
return rpcClient.call<{ rotated: boolean; one_time_prekeys: number }>({
|
|
|
|
|
method: 'mesh.rotate-prekeys',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-03-17 15:51:56 +00:00
|
|
|
// ─── 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
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-03-17 23:56:37 +00:00
|
|
|
async function relayTransaction(txHex: string, mode: 'archy' | 'broadcast' = 'archy') {
|
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>
2026-03-17 15:51:56 +00:00
|
|
|
return rpcClient.call<{ request_id: number; queued: boolean; tx_hex_len: number }>({
|
|
|
|
|
method: 'mesh.relay-tx',
|
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>
2026-03-17 23:56:37 +00:00
|
|
|
params: { tx_hex: txHex, relay_mode: mode },
|
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>
2026-03-17 15:51:56 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
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>
2026-03-17 23:56:37 +00:00
|
|
|
async function relayStatus(requestId: number) {
|
|
|
|
|
return rpcClient.call<{
|
|
|
|
|
status: 'pending' | 'confirmed' | 'failed' | 'unknown'
|
|
|
|
|
request_id: number
|
|
|
|
|
txid?: string
|
|
|
|
|
error?: string
|
|
|
|
|
error_code?: string
|
|
|
|
|
completed_at?: string
|
|
|
|
|
}>({
|
|
|
|
|
method: 'mesh.relay-status',
|
|
|
|
|
params: { request_id: requestId },
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-17 00:03:08 +00:00
|
|
|
async function refreshAll() {
|
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>
2026-03-17 15:51:56 +00:00
|
|
|
await Promise.all([fetchStatus(), fetchPeers(), fetchMessages(), fetchDeadmanStatus(), fetchBlockHeaders()])
|
2026-03-17 00:03:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
status,
|
|
|
|
|
peers,
|
|
|
|
|
messages,
|
|
|
|
|
loading,
|
|
|
|
|
error,
|
|
|
|
|
sending,
|
|
|
|
|
unreadCounts,
|
|
|
|
|
totalUnread,
|
2026-03-19 16:12:01 +00:00
|
|
|
nodePositions,
|
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>
2026-03-17 15:51:56 +00:00
|
|
|
deadmanStatus,
|
|
|
|
|
blockHeaders,
|
|
|
|
|
latestBlockHeight,
|
2026-03-17 00:03:08 +00:00
|
|
|
fetchStatus,
|
|
|
|
|
fetchPeers,
|
|
|
|
|
fetchMessages,
|
|
|
|
|
sendMessage,
|
2026-04-12 12:11:00 -04:00
|
|
|
sendChannelMessage,
|
2026-03-17 00:03:08 +00:00
|
|
|
broadcastIdentity,
|
|
|
|
|
configure,
|
|
|
|
|
refreshAll,
|
|
|
|
|
markChatRead,
|
|
|
|
|
clearViewingChat,
|
feat: Phase 3 Week 7 — typed message UI, session badges, rich chat cards
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:34:37 +00:00
|
|
|
sendInvoice,
|
|
|
|
|
sendCoordinate,
|
|
|
|
|
sendAlert,
|
2026-04-13 11:10:59 -04:00
|
|
|
sendContent,
|
|
|
|
|
fetchContent,
|
2026-04-13 13:19:30 -04:00
|
|
|
sendReply,
|
|
|
|
|
sendReaction,
|
2026-04-13 18:50:08 -04:00
|
|
|
getOutbox,
|
|
|
|
|
sendReadReceipt,
|
|
|
|
|
forwardMessage,
|
|
|
|
|
editMessage,
|
|
|
|
|
deleteMessage,
|
feat: Phase 3 Week 7 — typed message UI, session badges, rich chat cards
Frontend store (mesh.ts):
- Add typed message interfaces: InvoiceData, AlertData, CoordinateData,
SessionStatus, AlertStatus, MeshMessageTypeLabel
- New actions: sendInvoice, sendCoordinate, sendAlert, getSessionStatus,
rotatePrekeys
Mesh.vue UI:
- Typed message rendering in chat bubbles:
- Invoice: orange card with sats amount, memo, bolt11 preview, paid badge
- Alert: red card (emergency/dead_man) or blue (status), signed badge,
GPS link to OpenStreetMap
- Coordinate: blue card with lat/lng, label, OSM map link
- Block header: purple inline with chain icon
- Session badge in chat header: green shield (Double Ratchet),
yellow (static encryption), gray (none)
- Session status fetched on peer selection via mesh.session-status RPC
Mock backend:
- Messages now include message_type and typed_payload fields
- Mix of text, invoice (paid + unpaid), alert (emergency + status),
coordinate, and block_header messages for testing
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 02:34:37 +00:00
|
|
|
getSessionStatus,
|
|
|
|
|
rotatePrekeys,
|
2026-03-19 16:12:01 +00:00
|
|
|
getNodePositions,
|
|
|
|
|
updateSelfPosition,
|
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>
2026-03-17 15:51:56 +00:00
|
|
|
fetchDeadmanStatus,
|
|
|
|
|
configureDeadman,
|
|
|
|
|
deadmanCheckin,
|
|
|
|
|
fetchBlockHeaders,
|
|
|
|
|
relayTransaction,
|
|
|
|
|
relayLightning,
|
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>
2026-03-17 23:56:37 +00:00
|
|
|
relayStatus,
|
2026-03-17 00:03:08 +00:00
|
|
|
}
|
|
|
|
|
})
|