diff --git a/mobile_app/context/LxmfContext.tsx b/mobile_app/context/LxmfContext.tsx index a880151..f1673c1 100644 --- a/mobile_app/context/LxmfContext.tsx +++ b/mobile_app/context/LxmfContext.tsx @@ -431,6 +431,7 @@ interface LxmfCtxValue { pairNusRNode: (mac: string) => boolean; beaconRpc: (destHashHex: string, method: string, params?: unknown) => Promise; beaconBroadcastRpc: (method: string, params?: unknown, timeoutMs?: number) => Promise<{ resultJson: string; beaconHash: string }>; + beaconRpcWait: (destHashHex: string, method: string, params?: unknown, timeoutMs?: number) => Promise<{ resultJson: string; isError: boolean }>; blePeerCount: number; updateDisplayName: (name: string) => Promise; isBeacon: boolean; @@ -877,6 +878,7 @@ export function LxmfProvider({ children }: { readonly children: React.ReactNode pairNusRNode: lxmf.pairNusRNode, beaconRpc: lxmf.beaconRpc, beaconBroadcastRpc: lxmf.beaconBroadcastRpc, + beaconRpcWait: lxmf.beaconRpcWait, blePeerCount, updateDisplayName: async (name: string) => { const trimmed = name.trim(); @@ -909,7 +911,7 @@ export function LxmfProvider({ children }: { readonly children: React.ReactNode lxmf.events, lxmf.error, lxmf.start, lxmf.stop, lxmf.broadcast, lxmf.getStatus, lxmf.getBeacons, lxmf.setLogLevel, lxmf.bleUnpairedRNodeCount, - lxmf.getNusUnpairedRNodes, lxmf.pairNusRNode, lxmf.beaconRpc, lxmf.beaconBroadcastRpc]); + lxmf.getNusUnpairedRNodes, lxmf.pairNusRNode, lxmf.beaconRpc, lxmf.beaconBroadcastRpc, lxmf.beaconRpcWait]); return ( diff --git a/mobile_app/context/NetworkModeContext.tsx b/mobile_app/context/NetworkModeContext.tsx index bc844b4..4f72a6a 100644 --- a/mobile_app/context/NetworkModeContext.tsx +++ b/mobile_app/context/NetworkModeContext.tsx @@ -61,7 +61,7 @@ export interface NetworkState { const NetworkModeContext = createContext(undefined); export function NetworkModeProvider({ children }: { readonly children: ReactNode }) { - const { beacons, beaconBroadcastRpc, status, peers } = useLxmfContext(); + const { beacons, beaconRpcWait, status, peers } = useLxmfContext(); const [internet, setInternet] = useState(true); // Subscribe to OS-level connectivity — no polling, no HTTP spam. @@ -107,9 +107,9 @@ export function NetworkModeProvider({ children }: { readonly children: ReactNode const adapter = useMemo(() => { if (mode === "online") return new DirectRpcAdapter(solanaConnection); - if (mode === "mesh") return new MeshRpcAdapter(meshHash, beaconBroadcastRpc); + if (mode === "mesh") return new MeshRpcAdapter(meshHash, beaconRpcWait); return new IsolatedRpcAdapter(); - }, [mode, meshHash, beaconBroadcastRpc]); + }, [mode, meshHash, beaconRpcWait]); const value = useMemo( () => ({ mode, adapter, relayHash: adapter.relayHash }), diff --git a/mobile_app/src/infrastructure/network/MeshRpcAdapter.ts b/mobile_app/src/infrastructure/network/MeshRpcAdapter.ts index 257c973..828063d 100644 --- a/mobile_app/src/infrastructure/network/MeshRpcAdapter.ts +++ b/mobile_app/src/infrastructure/network/MeshRpcAdapter.ts @@ -20,26 +20,37 @@ import type { IRpcAdapter, ParsedTokenAccountsByOwner } from './types'; const MESH_RPC_TIMEOUT_MS = 30_000; -type BeaconBroadcastRpcFn = ( +type BeaconRpcWaitFn = ( + destHashHex: string, method: string, params?: unknown, timeoutMs?: number, -) => Promise<{ resultJson: string; beaconHash: string }>; +) => Promise<{ resultJson: string; isError: boolean }>; export class MeshRpcAdapter implements IRpcAdapter { readonly mode = 'mesh' as const; readonly relayHash: string; - private readonly beaconBroadcastRpc: BeaconBroadcastRpcFn; + private readonly beaconRpcWait: BeaconRpcWaitFn; - constructor(relayBeaconHash: string, beaconBroadcastRpc: BeaconBroadcastRpcFn) { + constructor(relayBeaconHash: string, beaconRpcWait: BeaconRpcWaitFn) { this.relayHash = relayBeaconHash; - this.beaconBroadcastRpc = beaconBroadcastRpc; + this.beaconRpcWait = beaconRpcWait; } private async rpc(method: string, params: unknown[]): Promise { - const { resultJson } = await this.beaconBroadcastRpc(method, params, MESH_RPC_TIMEOUT_MS); - return JSON.parse(resultJson) as T; + const { resultJson, isError } = await this.beaconRpcWait( + this.relayHash, + method, + params, + MESH_RPC_TIMEOUT_MS, + ); + const parsed = JSON.parse(resultJson) as unknown; + if (isError) { + const err = parsed as { code?: number; message?: string }; + throw new Error(err.message ?? `RPC error (${err.code ?? -1})`); + } + return parsed as T; } async getBalance(pubkey: PublicKey): Promise { diff --git a/mobile_app/src/services/sendTransaction.ts b/mobile_app/src/services/sendTransaction.ts index ab335b8..8513231 100644 --- a/mobile_app/src/services/sendTransaction.ts +++ b/mobile_app/src/services/sendTransaction.ts @@ -303,7 +303,6 @@ async function buildSplTransferTransaction({ mintAddress, decimals, programId, - rpcAdapter, }: { rpcAdapter: IRpcAdapter; fromPubkey: PublicKey; @@ -312,7 +311,6 @@ async function buildSplTransferTransaction({ mintAddress: string; decimals: number; programId?: string; - rpcAdapter: IRpcAdapter; }): Promise { // Bottom-line guard against any caller (including direct deep-links to // /send/review with a tampered programId param) trying to build an SPL @@ -476,7 +474,6 @@ export async function estimateSplTransferFeeLamports({ mintAddress, decimals, programId, - rpcAdapter, }); const { blockhash } = await withTimeout( rpcAdapter.getLatestBlockhash(), @@ -546,7 +543,6 @@ export async function sendSplTransfer({ mintAddress, decimals, programId, - rpcAdapter, }); return signAndSubmitTransaction({ walletAdapter, rpcAdapter, tx, expectedPubkey: fromPubkey }); }