Browser companion to the Dead Man's Vault mobile app, live at
https://dmvapp.palatinearc.com. React + Vite + @solana/wallet-adapter; users sign
with a standard browser wallet (Phantom / Solflare / Backpack). The web app never holds a
private key — both the owner and the heir sign every transaction in their own wallet
extension.
It is a two-tab app:
- Inheritances — heir claim portal. Connect a wallet, auto-discover every vault where
you're a beneficiary (via the notify-server
GET /inheritances?wallet=endpoint) or import one by the owner's address, and claim a matured vault. Execution is already permissionless on-chain, so this is just a web crank driven by the heir's wallet — the heir pays fees. - My Vault — owner console. View status (Active / Frozen / Distributed, countdown to
the switch, beneficiaries + shares, SOL + token/NFT balances with names & logos),
deposit/withdraw SOL, deposit tokens + NFTs from your wallet (behind a
checkDepositableguard) and withdraw them, edit beneficiaries, set/edit specific bequests (SOL / SPL / NFT), and revoke or close the vault. All owner mutations are gated to pre-grace + mutable, exactly as on-chain.
What stays on the phone: heartbeats and vault creation. On-chain record_heartbeat is
signed by the device-held agent key, which the web app never has — so liveness proof and
initial setup remain in the mobile app.
The on-chain logic is not forked. Via a Vite alias (@app → ../dead-mans-vault/app/src)
this app imports the exact same TypeScript the mobile app ships — a single source of truth,
no duplicated transaction builders:
ClaimService.runClaim— the resumable, idempotent heir claim loopVaultTransactionService— every permissionless-crank + owner-mutation transaction builderutils/rpcConfig,utils/constants(PROGRAM_ID, etc.)
The wallet seam that was Mobile Wallet Adapter on the phone is @solana/wallet-adapter-react
here — it returns the same web3.js v1 Transaction + signTransaction callback the reused
services already expect.
Making RN source run in a browser needs a few seams:
react-native→src/shims/react-native.ts— a 3-linePlatformstub (constants.tsuses it only for a font family).process.env.EXPO_PUBLIC_*→define— the Expo build-time env vars replaced at compile time withVITE_*values (all have|| defaultfallbacks).resolve.dedupe— the reused files live in the siblingapp/, so bare imports would otherwise pull a second copy of every shared dep fromapp/node_modules. Dedupe forces one copy of@solana/web3.js,@coral-xyz/anchor,@solana/spl-token,bs58,buffer,bn.js,react,react-domfrom this project.- Manual
Buffer/globalpolyfill (src/polyfills.ts, imported first inmain.tsx) — notvite-plugin-node-polyfills, whose dev-mode Buffer injection hits a TDZ "cannot access before initialization" circular-init bug with web3.js's pre-bundled deps. @types/reactpinned to18.3.12(with anoverridesblock) to keep one React type version across the shared graph.
- Default RPC is the notify-server proxy:
https://notify.palatinearc.com/rpc?cluster=devnet. The Helius key is injected server-side (never shipped in the bundle). HTTP is rate-limited to 150 requests / 10s per client; WebSocket confirmations are proxied by Caddy → Helius. The?cluster=devnethint keeps network labels / explorer links correct (the proxy URL itself carries no cluster). - Bring-your-own RPC: users can set their own endpoint in Settings → Network (persists
in
localStorage, "restart to apply"). - Fail-closed network gate (
src/BootGate.tsx): before the app mounts,verifyNetwork()(reused from@app) checks the RPC's on-chain genesis hash againstVITE_EXPECTED_CLUSTER(defaultdevnet). VERIFIED → app mounts; MISMATCH → hard block (wrong-cluster RPC can't connect); UNKNOWN (RPC unreachable) → read-only with retry/continue (owner + heir writes are gated off). A mainnet web deploy MUST setVITE_EXPECTED_CLUSTER=mainnet-beta, and the RPC/proxy must answergetGenesisHashwith the mainnet hash. The genesis check is the authority — the?cluster=devnetURL hint is now only for labels/explorer links. - Token / NFT names + logos come from Helius DAS
getAssetsByOwnerthrough the same proxy. ⚠ DAS through this proxy needs named-object params (params: { ownerAddress }), not the array-wrapped form.
cp .env.example .env # defaults to the notify-server RPC proxy
npm install
npm run dev # http://localhost:5173
npm run build # production build → dist/
npm run typecheck # type-check web src only (not the reused RN source)
buildrunsvite buildonly (notscover the reused RN source, which needs RN types).typecheckis scoped tosrc/viatsconfig.web.json.
Static build served by Caddy from /var/www/dmvapp:
bash deploy.sh # runs `vite build`, then mirrors dist/ → /var/www/dmvapp- Phase 1 (heir claim) — done.
- Phase 2 (owner console) — done.
Remaining:
- A pure-web liveness model (prove liveness without the phone) — today heartbeats still require the device-held agent key.
rotate_agentstays phone-only by design (device migration is a mobile flow).