Skip to content

Commit 8304419

Browse files
committed
Fix syncing: trigger header sync when peers connect
The SPV client's syncHeaders() was never called. The peer manager started connections but never notified the SPV client when a peer completed the handshake. Changes: - Added onPeerReady callback to PeerManager (dispatches to subscribers) - SPV client registers for onPeerReady, triggers syncHeaders() on first ready peer, and sends Bloom filter to each new peer - Made BloomFilter.numHashFuncs and .tweak public (needed by SPV client to serialize filterload messages for new peers) - Wallet store no longer sets isSyncing:true prematurely - the SPV client's onSyncProgress event drives the syncing state - Updated README with current architecture and features
1 parent fccf77d commit 8304419

5 files changed

Lines changed: 109 additions & 52 deletions

File tree

README.md

Lines changed: 69 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,39 @@ Built with Expo SDK 55, React Native 0.83, and pure TypeScript cryptography.
66

77
## Features
88

9-
- **True SPV wallet** — connects directly to the FairCoin P2P network, no server dependency
9+
- **True SPV wallet** — connects directly to the FairCoin P2P network via DNS seeds, no server dependency
1010
- **HD wallet** — BIP39 (24-word mnemonic) + BIP32 + BIP44 (`m/44'/119'/0'/...`)
1111
- **Multi-wallet** — create, import, switch between, and manage multiple wallets
12-
- **QR code** — scan to send, show to receive
13-
- **Deep links** — handles `faircoin:` URIs (BIP21-style)
14-
- **PIN & biometrics** — 6-digit PIN with fingerprint/face unlock
12+
- **QR code** — camera scanner to send, QR display to receive
13+
- **Deep links** — handles `faircoin:` URIs (BIP21-style), opens Send pre-filled
14+
- **PIN & biometrics** — 6-digit PIN with fingerprint/face unlock on app open
15+
- **Address book** — save and manage contacts
16+
- **Coin control** — select specific UTXOs for transactions
1517
- **Masternode** — detect 5,000 FAIR collateral UTXOs, start masternodes
1618
- **FastSend** — instant confirmation via masternode quorum (SwiftTX)
17-
- **Secure** — keys stored in OS keychain (Keychain/EncryptedSharedPreferences), per-wallet SQLite databases
19+
- **BIP38** — encrypted private key export/import
20+
- **Secure** — keys in OS keychain, per-wallet SQLite databases, edge-to-edge UI
1821

1922
## Platforms
2023

21-
| Platform | Status | How |
22-
|----------|--------|-----|
23-
| Android | Ready | Expo Go / APK build |
24-
| iOS | Ready | Expo Go / IPA build |
25-
| Desktop | Ready | Electron wrapping Expo web export |
24+
| Platform | Status | Build |
25+
|----------|--------|-------|
26+
| Android | Production | APK via GitHub Actions |
27+
| iOS | Production | Expo prebuild + Xcode |
28+
| Desktop | Production | Electron (Windows, macOS, Linux) |
2629

2730
## Tech Stack
2831

2932
| Layer | Technology |
3033
|-------|-----------|
3134
| Framework | Expo SDK 55, React Native 0.83 |
32-
| Navigation | expo-router (native tabs on Android/iOS) |
33-
| Styling | NativeWind (Tailwind CSS) |
34-
| State | Zustand |
35-
| Crypto | `@noble/secp256k1`, `@noble/hashes`, `@scure/bip32`, `@scure/bip39` |
36-
| Storage | expo-secure-store (keys), expo-sqlite (chain data) |
37-
| P2P | Custom SPV client (TCP via react-native-tcp-socket / Node.js net) |
38-
| Desktop | Electron (IPC bridge for TCP + secure storage) |
35+
| Navigation | expo-router with native tabs (NativeTabs on Android/iOS, headless on web) |
36+
| Styling | NativeWind 4.x (Tailwind CSS) |
37+
| State | Zustand 5 |
38+
| Crypto | `@noble/secp256k1` 2.x, `@noble/hashes` 1.8, `@scure/bip32` 1.7, `@scure/bip39` 1.6 |
39+
| Storage | expo-secure-store (native keychain), expo-sqlite (chain data) |
40+
| P2P | Custom SPV client over TCP (react-native-tcp-socket on mobile, Node.js net on Electron) |
41+
| Desktop | Electron with custom `app://` protocol for proper SPA routing |
3942

4043
## FairCoin Protocol
4144

@@ -51,14 +54,16 @@ Built with Expo SDK 55, React Native 0.83, and pure TypeScript cryptography.
5154
| Tx hash | Double SHA-256 |
5255
| Signing curve | secp256k1 |
5356
| Masternode collateral | 5,000 FAIR |
57+
| DNS seeds | `seed1.fairco.in`, `seed2.fairco.in` |
5458

5559
## Getting Started
5660

5761
### Prerequisites
5862

5963
- [Node.js](https://nodejs.org/) 22+
6064
- [Bun](https://bun.sh/) (package manager)
61-
- [Expo Go](https://expo.dev/go) on your phone/emulator
65+
- JDK 17+ (for Android builds)
66+
- Android SDK (for Android builds)
6267

6368
### Install
6469

@@ -71,46 +76,55 @@ bun install
7176
### Development
7277

7378
```bash
74-
# Start Expo dev server
79+
# Start Expo dev server (requires Expo Go or dev client)
7580
bun start
7681

77-
# Android
82+
# Build and run on Android (requires Android SDK + JDK)
7883
bun run android
7984

80-
# iOS
85+
# Build and run on iOS (requires Xcode)
8186
bun run ios
8287

83-
# Web (for Electron)
88+
# Web (for Electron development)
8489
bun run web
8590
```
8691

8792
### Desktop (Electron)
8893

8994
```bash
90-
# Development
95+
# Build web export + launch Electron
9196
bun run export:web
92-
bun run electron -- --dev
97+
bun run electron
9398

94-
# Production
95-
bun run electron:build
99+
# Development mode (connects to Expo dev server)
100+
bun run electron -- --dev
96101
```
97102

98-
### Build APK
103+
### Build APK Locally
99104

100105
```bash
101-
# Local build
102106
npx expo prebuild --platform android
103107
cd android && ./gradlew assembleRelease
104108
```
105109

106-
Or push a release tag — GitHub Actions will automatically build and attach the APK.
107-
108110
### Type Check
109111

110112
```bash
111113
bun run typecheck
112114
```
113115

116+
## CI/CD
117+
118+
GitHub Actions automatically build and attach release artifacts when you create a GitHub release:
119+
120+
- **Android**: APK built with Gradle, attached to release
121+
- **Desktop**: Windows (.exe), macOS (.dmg), Linux (.deb, .AppImage) via electron-builder
122+
123+
```bash
124+
# Create a release (triggers builds)
125+
gh release create v1.0.1 --title "v1.0.1" --notes "Release notes..."
126+
```
127+
114128
## Project Structure
115129

116130
```
@@ -121,6 +135,10 @@ FAIRWallet/
121135
│ ├── lock.tsx # PIN/biometric lock screen
122136
│ ├── masternode.tsx # Masternode management
123137
│ ├── wallets.tsx # Multi-wallet manager
138+
│ ├── contacts.tsx # Address book
139+
│ ├── coin-control.tsx # UTXO selection
140+
│ ├── export-key.tsx # BIP38 key export
141+
│ ├── transaction/ # Transaction detail
124142
│ ├── onboarding/ # Welcome, create, restore, PIN setup
125143
│ └── (tabs)/ # Main app (wallet, send, receive, settings)
126144
├── src/
@@ -138,50 +156,58 @@ FAIRWallet/
138156
│ ├── p2p/ # SPV P2P client
139157
│ │ ├── messages.ts # Wire protocol serialization
140158
│ │ ├── bloom-filter.ts # BIP37 Bloom filter
141-
│ │ ├── peer.ts # TCP peer connection
142-
│ │ ├── peer-manager.ts # Multi-peer pool
159+
│ │ ├── peer.ts # TCP peer connection + handshake
160+
│ │ ├── peer-manager.ts # Multi-peer pool + discovery
143161
│ │ ├── spv-client.ts # Header sync + Merkle validation
162+
│ │ ├── dns-seeds.ts # DNS-over-HTTPS seed resolution
144163
│ │ ├── masternode.ts # Masternode broadcast/ping
145-
│ │ └── fastsend.ts # SwiftTX instant confirmations
164+
│ │ ├── fastsend.ts # SwiftTX instant confirmations
165+
│ │ ├── socket-provider.ts # Platform TCP adapter
166+
│ │ └── header-store.ts # SQLite ↔ SPV bridge
146167
│ ├── wallet/ # Wallet state
147168
│ │ ├── key-manager.ts # HD key derivation + gap limit
148169
│ │ ├── utxo-set.ts # UTXO tracking + coin selection
149-
│ │ └── wallet-store.ts # Zustand store (multi-wallet)
170+
│ │ └── wallet-store.ts # Zustand store (multi-wallet, P2P)
150171
│ ├── storage/ # Persistence
151172
│ │ ├── database.ts # SQLite (per-wallet)
152-
│ │ └── secure-store.ts # Keychain (mnemonics, PIN, biometrics)
173+
│ │ ├── secure-store.ts # Keychain (mnemonics, PIN, biometrics)
174+
│ │ └── kv-store.ts # Platform adapter (native keychain / localStorage)
153175
│ └── ui/components/ # Reusable UI
154-
│ ├── Button.tsx # Multi-variant button
176+
│ ├── Button.tsx
155177
│ ├── TransactionItem.tsx
156178
│ ├── SyncStatus.tsx
157-
│ └── QRScanner.tsx # Camera-based QR scanner
179+
│ └── QRScanner.tsx
158180
├── electron/ # Desktop wrapper
159-
│ ├── main.js # TCP P2P + safeStorage via IPC
181+
│ ├── main.js # Custom app:// protocol + TCP P2P + safeStorage
160182
│ └── preload.js # contextBridge API
183+
├── plugins/
184+
│ └── withGradle813.js # Expo config plugin: Gradle 9→8.13 downgrade
161185
├── .github/workflows/
162-
│ └── build-android.yml # Auto-build APK on GitHub releases
186+
│ ├── build-android.yml # APK build on release
187+
│ └── build-desktop.yml # Electron builds on release
163188
└── app.json # Expo config
164189
```
165190

166191
## Deep Links
167192

168-
FAIRWallet handles `faircoin:` URIs:
193+
FAIRWallet handles `faircoin:` URIs following BIP21 conventions:
169194

170195
```
171196
faircoin:FxxxxAddress
172197
faircoin:FxxxxAddress?amount=10.5
173198
faircoin:FxxxxAddress?amount=10.5&label=Donation&message=Thanks
174199
```
175200

176-
Clicking a `faircoin:` link opens FAIRWallet and pre-fills the Send screen.
201+
Tapping a `faircoin:` link opens FAIRWallet and navigates to the Send screen with address and amount pre-filled.
177202

178203
## Security
179204

180-
- Private keys and mnemonics are stored in the OS keychain (iOS Keychain / Android EncryptedSharedPreferences)
181-
- PIN is hashed with SHA-256 before storage
205+
- Private keys and mnemonics stored in OS keychain (iOS Keychain / Android EncryptedSharedPreferences)
206+
- PIN hashed with SHA-256 before storage
182207
- Each wallet has its own isolated SQLite database
183208
- All cryptography uses audited libraries (`@noble/*`, `@scure/*`)
184209
- No external servers — SPV client connects directly to FairCoin nodes
210+
- BIP38 encrypted key export for backups
185211

186212
## License
187213

src/p2p/bloom-filter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,8 +92,8 @@ function murmurHash3(data: Uint8Array, seed: number): number {
9292

9393
export class BloomFilter {
9494
private readonly data: Uint8Array;
95-
private readonly numHashFuncs: number;
96-
private readonly tweak: number;
95+
readonly numHashFuncs: number;
96+
readonly tweak: number;
9797
private readonly flags: number;
9898

9999
constructor(size: number, numHashFuncs: number, tweak: number, flags: number) {

src/p2p/peer-manager.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export interface PeerManagerConfig {
2222
}
2323

2424
export type MessageHandler = (peer: Peer, command: string, payload: Uint8Array) => void;
25+
export type PeerReadyHandler = (peer: Peer) => void;
2526

2627
// ---------------------------------------------------------------------------
2728
// Constants
@@ -48,6 +49,7 @@ export class PeerManager {
4849
private readonly failedAddresses: Map<string, number> = new Map(); // address -> fail count
4950

5051
private messageHandlers: MessageHandler[] = [];
52+
private peerReadyHandlers: PeerReadyHandler[] = [];
5153
private reconnectTimer: ReturnType<typeof setInterval> | undefined;
5254
private discoveryTimer: ReturnType<typeof setInterval> | undefined;
5355
private running = false;
@@ -175,6 +177,13 @@ export class PeerManager {
175177
this.messageHandlers.push(handler);
176178
}
177179

180+
/**
181+
* Register a handler called when a peer completes the version handshake.
182+
*/
183+
onPeerReady(handler: PeerReadyHandler): void {
184+
this.peerReadyHandlers.push(handler);
185+
}
186+
178187
/**
179188
* Remove a previously registered message handler.
180189
*/
@@ -262,6 +271,10 @@ export class PeerManager {
262271
onReady: (peer: Peer) => {
263272
// Reset fail count on successful connection
264273
this.failedAddresses.delete(peer.host);
274+
// Notify subscribers
275+
for (const handler of this.peerReadyHandlers) {
276+
handler(peer);
277+
}
265278
},
266279
onMessage: (peer: Peer, command: string, payload: Uint8Array) => {
267280
this.dispatchMessage(peer, command, payload);

src/p2p/spv-client.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,7 @@ export class SPVClient {
259259

260260
this.peerManager = new PeerManager(peerManagerConfig);
261261
this.peerManager.onMessage(this.handlePeerMessage.bind(this));
262+
this.peerManager.onPeerReady(this.handlePeerReady.bind(this));
262263
}
263264

264265
// -----------------------------------------------------------------------
@@ -289,6 +290,28 @@ export class SPVClient {
289290
this.peerManager.stop();
290291
}
291292

293+
/**
294+
* Called when a peer completes the version/verack handshake.
295+
* Sends our Bloom filter and begins header sync if not already syncing.
296+
*/
297+
private handlePeerReady(peer: Peer): void {
298+
// Send Bloom filter to the newly connected peer
299+
if (this.bloomFilter) {
300+
const filterPayload = serializeFilterLoad(
301+
this.bloomFilter.toBytes(),
302+
this.bloomFilter.numHashFuncs,
303+
this.bloomFilter.tweak,
304+
1, // BLOOM_UPDATE_ALL
305+
);
306+
peer.sendMessage("filterload", filterPayload);
307+
}
308+
309+
// Begin header sync on first ready peer
310+
if (this.running && !this.syncing) {
311+
void this.syncHeaders();
312+
}
313+
}
314+
292315
/**
293316
* Register event handlers.
294317
*/

src/wallet/wallet-store.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -375,17 +375,12 @@ export const useWalletStore = create<WalletState>((set, get) => ({
375375
set({ networkStatus: "Connecting to peers..." });
376376
await spvClient.start();
377377

378-
const peerCount = spvClient.getPeerManager().getReadyPeers().length;
379378
set({
380-
connectedPeers: peerCount,
381379
chainHeight: spvClient.getChainHeight(),
382-
isSyncing: true,
383-
networkStatus: peerCount > 0
384-
? `Connected to ${peerCount} peer${peerCount === 1 ? "" : "s"}`
385-
: "Waiting for peers...",
380+
networkStatus: "Waiting for peers...",
386381
});
387382

388-
// Start a periodic peer count updater
383+
// Periodic peer count updater
389384
const peerUpdateInterval = setInterval(() => {
390385
if (!spvClient) {
391386
clearInterval(peerUpdateInterval);

0 commit comments

Comments
 (0)