Skip to content

OmLanke/MeshWallet

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

27 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

MeshWallet

TypeScript React Native Expo Ethereum ethers.js WalletConnect SQLite NativeWind Zustand

A self-custodial Ethereum wallet for iOS and Android that sends transactions over a BLE mesh network, enabling offline-first transfers without internet access.


Table of Contents


What is MeshWallet?

MeshWallet is a self-custodial Ethereum mobile wallet that routes signed transactions through a BLE (Bluetooth Low Energy) device mesh when no internet is available. Any phone running the app acts as a relay node; when a node gains connectivity it flushes buffered transactions to an Ethereum RPC endpoint (Sepolia testnet) and polls for confirmation.

The wallet generates secp256k1 key pairs natively, stores private keys in the OS secure enclave (via expo-secure-store), and signs transactions locally using @noble/curves. WalletConnect v2 integration allows dApp pairing. The design assumes an adversarial environment: every packet is AES-256-GCM encrypted, carries a TTL, and is deduplicated before relay.


Architecture Overview

graph TB
    subgraph Device["Mobile Device (iOS / Android)"]
        UI["Expo Router UI\n(app/ screens)"]
        WS["walletStore\nZustand"]
        MS["meshStore\nZustand"]
        KM["KeyManager\nexpo-secure-store + secp256k1"]
        SG["Signer\n@noble/curves"]
        EN["Encryptor\nAES-256-GCM"]
        DB[(SQLite\nexpo-sqlite)]
        GW["GatewaySync\nBackgroundFetch"]
    end

    subgraph BLE["BLE Mesh Layer"]
        BM["BLEManager\n@offline-protocol/mesh-sdk"]
        MR["MeshRouter\nTTL · dedup · relay buffer"]
        PS["PairingService\nneighbor discovery"]
        PC["PacketCodec\nJSON encode/decode"]
    end

    subgraph External["External"]
        RPC["Ethereum RPC\nSepolia (multi-endpoint)"]
        ES["Etherscan API\nSepolia"]
        WC["WalletConnect v2\nCloud relay"]
    end

    UI --> WS
    UI --> MS
    UI --> KM
    UI --> SG
    UI --> EN
    SG --> DB
    GW --> DB
    GW --> RPC
    GW --> ES
    MR --> DB
    MR --> BM
    BM --> PC
    MR --> GW
    PS --> BM
    UI --> PS
    UI --> WC
    WS --> UI
    MS --> UI
Loading

Transaction Flow

sequenceDiagram
    participant User
    participant UI as Expo Router UI
    participant Signer
    participant Encryptor
    participant DB as SQLite
    participant MeshRouter
    participant BLEMesh as BLE Mesh (peers)
    participant GatewaySync
    participant RPC as Ethereum RPC (Sepolia)

    User->>UI: Enter recipient + amount, tap Send
    UI->>Signer: signTransaction(unsignedTx)
    Signer->>DB: insertTransaction(status=QUEUED_LOCAL)
    UI->>Encryptor: encryptForSender(signedTx, privKey)
    UI->>MeshRouter: enqueue MeshPacket (TTL=12)
    MeshRouter->>DB: insertRelayPacket + status→IN_MESH
    MeshRouter->>BLEMesh: writePacketToDevice (per peer)
    BLEMesh-->>MeshRouter: neighbor relays packet onwards

    alt Device has internet
        MeshRouter->>GatewaySync: flushPendingToGateway()
        GatewaySync->>DB: read relay_buffer (forwarded=0)
        GatewaySync->>Encryptor: decryptFromSender(packet)
        GatewaySync->>RPC: ethers.Wallet.sendTransaction (EIP-1559)
        RPC-->>GatewaySync: txHash
        GatewaySync->>DB: status→BROADCAST + tx_hash
        loop Poll every 5 s (max 10 min)
            GatewaySync->>RPC: getTransactionReceipt(txHash)
            RPC-->>GatewaySync: receipt
            GatewaySync->>DB: status→CONFIRMED or FAILED
        end
    else No internet (pure mesh relay)
        BLEMesh-->>BLEMesh: packet hops until a node with internet flushes it
    end
Loading

Failure Compensation

Failure Point Effect Recovery
All RPC endpoints unreachable broadcastTransactionDirect throws; forwarded stays 0 Next flushPendingToGateway call retries automatically
Wrong chain ID from RPC Error thrown, RPC skipped Falls through to next RPC in fallback list
Receipt poll timeout (10 min) Status stays BROADCAST reconcileBroadcastTransactions() can be called manually
Decryption failure Packet marked forwarded=1 and skipped No retry; packet is effectively dropped
Relay buffer full (RELAY_BUFFER_MAX=50) Incoming packet dropped Sender will re-inject after TTL reset
TTL reaches 0 Packet dropped at MeshRouter.handleIncomingPacket Sender must rebroadcast

Tech Stack

Application

Module Language Runtime Framework Storage Role
Mobile App TypeScript Hermes (RN 0.81) Expo 54 / React Native expo-sqlite (SQLite) UI, wallet, mesh coordination
BLE Layer TypeScript Hermes @offline-protocol/mesh-sdk in-memory relay buffer BLE central+peripheral, mesh routing
Crypto TypeScript Hermes @noble/curves, @noble/ciphers expo-secure-store Key gen, signing, AES-256-GCM
Gateway TypeScript Hermes ethers.js v6 SQLite relay_buffer RPC broadcast, confirmation polling
State TypeScript Hermes Zustand v5 in-memory Wallet + mesh reactive state

Infrastructure

Component Technology Purpose
On-device DB expo-sqlite (SQLite) Transactions, relay buffer, ledger
Secure key storage expo-secure-store Private key persistence in OS keychain
BLE transport @offline-protocol/mesh-sdk Central/peripheral management, mesh routing
Ethereum RPC Sepolia public endpoints (drpc, publicnode, 1rpc, rpc.sepolia.org) Transaction broadcast + receipt polling
WalletConnect @walletconnect/core v2 + modal-react-native dApp pairing via QR / deep link
Background sync expo-background-fetch + expo-task-manager Gateway flush while app is backgrounded
Build / OTA EAS Build + EAS Submit Cloud builds for iOS and Android

Project Structure

MeshWallet/
├── app/                        # Expo Router file-based screens
│   ├── (tabs)/                 # Bottom-tab group
│   │   ├── wallet.tsx          # Main balance + recent txs
│   │   ├── history.tsx         # Full transaction history
│   │   ├── network.tsx         # BLE mesh peer view + relay events
│   │   └── profile.tsx         # Address, QR code, settings
│   ├── send/                   # Multi-step send flow (stack)
│   │   ├── index.tsx           # Amount + recipient entry
│   │   ├── scan.tsx            # QR camera scanner
│   │   ├── pairing.tsx         # BLE peer picker
│   │   ├── confirm.tsx         # Review + sign
│   │   └── success.tsx         # Broadcast confirmation
│   ├── transaction/[id].tsx    # Transaction detail
│   ├── onboarding.tsx          # First-run key generation
│   ├── ledger.tsx              # On-chain balance reconciliation
│   ├── receive.tsx             # QR receive address
│   └── _layout.tsx             # Root navigator + WalletContext init
├── src/
│   ├── ble/
│   │   ├── BLEManager.ts       # Wraps @offline-protocol/mesh-sdk singleton
│   │   ├── MeshRouter.ts       # TTL, dedup, relay buffer, gateway trigger
│   │   ├── PacketCodec.ts      # MeshPacket type + JSON encode/decode
│   │   └── PairingService.ts   # Neighbor discovery for send pairing
│   ├── crypto/
│   │   ├── KeyManager.ts       # secp256k1 keygen, SecureStore persistence
│   │   ├── Signer.ts           # keccak256 + secp256k1 tx signing/verify
│   │   └── Encryptor.ts        # AES-256-GCM encrypt/decrypt per packet
│   ├── db/
│   │   ├── schema.ts           # SQLite table definitions (transactions, relay_buffer, ledger)
│   │   ├── TransactionRepo.ts  # CRUD for transactions + relay_buffer rows
│   │   └── LedgerRepo.ts       # Balance + nonce cache
│   ├── gateway/
│   │   └── GatewaySync.ts      # Relay buffer → RPC flush, confirmation poll, reconcile
│   ├── store/
│   │   ├── walletStore.ts      # Zustand: address, balance, pendingOutgoing
│   │   └── meshStore.ts        # Zustand: peers, relay buffer, mesh events
│   ├── hooks/
│   │   ├── useWallet.ts        # Wallet init, balance refresh
│   │   ├── useMesh.ts          # Mesh start/stop lifecycle
│   │   └── useTransactionQueue.ts # Outbox management
│   ├── context/
│   │   └── WalletContext.tsx   # Root context: key init, background task reg
│   ├── components/             # Shared UI primitives
│   └── constants/
│       ├── colors.ts           # Design tokens
│       └── config.ts           # WalletConnect project ID, Sepolia RPC
├── constants/
│   ├── ble.ts                  # BLE UUIDs, TTL, packet size, relay buffer cap
│   └── chain.ts                # Chain ID, RPC endpoints + fallbacks
├── plugins/
│   └── withBLEAdvertiserCompat.js  # Expo config plugin: Android BLE advertiser compat patch
├── scripts/
│   └── patch-native.js         # Post-install native patching
├── app.json                    # Expo config: permissions, bundle IDs, plugins
├── eas.json                    # EAS Build profiles (development, preview, production)
├── babel.config.js
├── metro.config.js
├── tailwind.config.js
├── tsconfig.json
└── vitest.config.ts            # Unit test config (crypto + BLE codec tests)

Quick Start

Prerequisites

Tool Min Version Install
Node.js 20 https://nodejs.org
npm 10 Bundled with Node 20
Expo CLI latest npm i -g expo-cli
EAS CLI 16.32+ npm i -g eas-cli
Xcode (iOS) 15 Mac App Store
Android Studio (Android) Hedgehog https://developer.android.com/studio
CocoaPods (iOS) 1.14+ gem install cocoapods

Steps

  1. Clone and install dependencies

    git clone https://github.com/mkwallet/MeshWallet.git
    cd MeshWallet
    npm install
  2. Set your WalletConnect Project ID

    Create a .env file in the repo root:

    echo "EXPO_PUBLIC_WC_PROJECT_ID=your_project_id_here" > .env

    Get a free project ID at https://cloud.walletconnect.com.

  3. Install iOS native dependencies

    npx expo prebuild --platform ios
    cd ios && pod install && cd ..
  4. Run on iOS simulator

    npm run ios
  5. Run on Android emulator / device

    npm run android
  6. Run unit tests

    npx vitest run

Note: BLE scanning and advertising require a physical device. Simulators will not discover peers.

Development Build (EAS)

To run native modules (BLE, SecureStore) that require a custom dev client:

npm run devbuild:android   # EAS cloud build → install APK on device

Environment Variables

Variable Description Default Required
EXPO_PUBLIC_WC_PROJECT_ID WalletConnect v2 Cloud project ID 'YOUR_WALLETCONNECT_PROJECT_ID' Yes (for dApp pairing)

All other configuration is static in source:

Constant Location Value
Sepolia chain ID constants/chain.ts 11155111
Primary RPC URL constants/chain.ts https://ethereum-sepolia-rpc.publicnode.com
RPC fallbacks constants/chain.ts drpc, 0xrpc, 1rpc, rpc.sepolia.org
Etherscan API constants/chain.ts https://api-sepolia.etherscan.io/api
BLE Service UUID constants/ble.ts A1B2C3D4-E5F6-7890-ABCD-EF0123456789
Default TTL constants/ble.ts 12
Max packet size constants/ble.ts 512 bytes
Relay buffer cap constants/ble.ts 50 packets

Why @offline-protocol/mesh-sdk?

  1. Abstracts BLE central + peripheral simultaneously. Managing ble-plx (central) and react-native-ble-advertiser (peripheral) as separate libraries requires platform-specific glue code for Android API level splits and MIUI scan-filter bugs. The SDK collapses both roles into a single OfflineProtocol instance.
  2. Native mesh routing. The SDK handles multi-hop relay internally; MeshRouter only needs to deduplicate and TTL-gate packets at the application layer, not implement flooding logic itself.
  3. Stable public-key–based addressing. neighbor_discovered events expose peer_id which maps directly to the wallet's secp256k1 public key hex, enabling computeAddress() to derive the Ethereum address of a discovered peer with no extra handshake.
  4. Transport-agnostic. The SDK can fall back to Wi-Fi Direct or other transports without API changes to BLEManager.ts.

Why @noble/* cryptography?

  1. Audited pure-TypeScript. No native bindings means the same code runs on iOS, Android, and in Vitest unit tests without mocking.
  2. Explicit primitives. @noble/curves/secp256k1 exposes sign, verify, and getPublicKey directly; there is no hidden key-derivation abstraction that could silently weaken security.
  3. Minimal bundle footprint. Tree-shaking eliminates unused cipher suites; only secp256k1, keccak_256, and aes/gcm are included in the final bundle.
  4. @noble/ciphers AES-256-GCM. Authenticated encryption with per-packet 24-byte random nonces prevents replay attacks on the BLE mesh layer.

Contributing

  1. Fork the repository and create a feature branch from main:
    git checkout -b feat/your-feature
  2. Make changes. Run lint and tests before committing:
    npm run lint
    npx vitest run
  3. Commit with a conventional message (feat:, fix:, chore:, etc.).
  4. Open a pull request against main. Describe the motivation and include relevant test output.

Code style: TypeScript strict mode, Prettier (config in prettier.config.js), ESLint (eslint.config.js). Run npm run format to auto-fix.


License

Private — all rights reserved.

About

Self-custodial Ethereum wallet that routes transactions over a BLE mesh network for offline-first transfers between nearby devices.

Resources

Stars

Watchers

Forks

Packages

 
 
 

Contributors