Skip to content

Add RoadFlare personal rideshare network + Phase 5-6 infrastructure#37

Merged
variablefate merged 66 commits intomainfrom
roadflare
Feb 4, 2026
Merged

Add RoadFlare personal rideshare network + Phase 5-6 infrastructure#37
variablefate merged 66 commits intomainfrom
roadflare

Conversation

@variablefate
Copy link
Owner

Summary

This branch adds the RoadFlare feature - a personal rideshare network where riders can build trusted relationships with specific drivers. Also includes major infrastructure improvements from Phase 5-6.

RoadFlare Feature

  • Follower system: Riders follow drivers, drivers approve/decline/mute followers
  • Encrypted location broadcast: Drivers share location only with approved followers (Kind 30014)
  • Shared keypair encryption: Single encryption for N followers via ECDH
  • Key rotation on mute: Security-preserving removal with automatic key refresh
  • Three-state driver mode: OFFLINE → ROADFLARE_ONLY → AVAILABLE
  • Cross-device sync: Kind 30011 (rider favorites) and Kind 30012 (driver state)

Phase 5: NostrService Domain Decomposition

  • Split 2500+ line NostrService into focused domain services
  • NostrCryptoHelper.kt - NIP-44 encryption utilities
  • ProfileBackupService.kt - Profile and history backup
  • RoadflareDomainService.kt - All RoadFlare Nostr methods
  • Backward-compatible facade maintains all existing method signatures

Phase 6: Payment Test Harness

  • 180 unit tests with Robolectric + MockK
  • FakeMintApi - Mock mint HTTP API with queue-based responses
  • FakeNip60Store - Mock NIP-60 storage with call ordering verification
  • Nip60Store interface for testable NIP-60 operations
  • Proof-conservation contract tests for safety invariants
  • CashuTokenCodec extraction for stateless token operations

Security Hardening

  • Backup exclusions for secrets and privacy data
  • Pubkey validation wired through all parsers
  • WebSocket concurrency fixes (bounded channel, generation tracking)
  • Relay-level signature verification (NIP-01)
  • Encryption fallback warning dialog

Stability Fixes

  • Driver availability state bouncing fix (selective clearing, timestamp guards)
  • RoadFlare status detection (cross-device sync, out-of-order rejection)
  • Duplicate confirmation race condition fix
  • Cancellation race conditions and cleanup leaks

Other Improvements

  • Shared UI components extracted to common module
  • PaymentHash moved from offer to confirmation (boost bug fix)
  • HTLC preimage storage for future-proof refunds
  • Notification coordinator architecture
  • Remote config support (Kind 30182)

Test plan

  • Build both apps successfully
  • Deploy to device and verify startup
  • Test RoadFlare follow/approve flow
  • Test direct offer to available driver
  • Test RoadFlare-only mode offers
  • Verify payment flow completes
  • Run unit tests: ./gradlew test

Stats

  • 63 commits since diverging from main
  • 181 files changed
  • +22,351 / -9,680 lines

🤖 Generated with Claude Code

variablefate and others added 30 commits January 26, 2026 22:07
Implements the complete RoadFlare system allowing riders to build a
personal network of favorite drivers. Drivers keep 100% of fares,
calculated using real-time location for accuracy.

New Nostr events:
- Kind 30011: Rider's followed drivers list (NIP-44 encrypted)
- Kind 30012: Driver's RoadFlare state (keypair, followers, muted)
- Kind 30014: Driver location broadcast (NIP-44 to shared keypair)
- Kind 3186: Ephemeral key share (driver → follower)
- Kind 3188: Key acknowledgement (rider → driver)

Features:
- RoadFlare tab in both rider and driver apps
- QR code driver profile sharing
- Real-time driver location with encrypted broadcasts
- Shared Nostr keypair model (single encryption per broadcast)
- Key rotation on follower removal
- DND mode for drivers (persists until explicit toggle)
- Follower approval workflow with stale key detection
- Post-ride "Add to Favorites" prompt
- RoadFlare-tagged ride offers (Kind 3173 with roadflare tag)
- Sync adapters for cross-device state recovery
- Background RoadFlare alert service for drivers
- Fare calculation using exact driver distance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace DND toggle with OFFLINE → ROADFLARE_ONLY → AVAILABLE driver
states. Remove all DND infrastructure from data models, serialization,
sync adapters, UI, and documentation. Fix subscription leak in
onCleared(), remove dead RideOfferEvent.createRoadflare(), extract
shared processIncomingOffer() helper, and reuse decryptRoadflareLocation
across rider screens.
Remove hardcoded $2.50 base fee and $5.00 minimum from RoadFlare fare
calculation. Minimum now comes from remote config (roadflareMinimumFareUsd)
with $5.00 default. Fallback is flat 5000 sats when no BTC price available.
Add roadflareMinimumFareUsd to AdminConfig and RemoteConfigManager cache.
…, refund safety

- Add ensureWalletReady() pre-check before ride offers to verify/clean NIP-60 proofs
- Replace single-retry cleanup in lockForRide with 3-attempt loop
- Add 120s clock skew buffer to HTLC refund timing (fixes mint rejection)
- Keep PendingHtlc status as LOCKED on refund failure instead of FAILED
- Reduce HTLC locktime from 2 hours to 15 minutes

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ailable

- Reorder DriverStatusBadge conditions to check explicit OFFLINE before staleness
- Remove drivers from selection sheet when OFFLINE status received
- Pass isDriverOnline from DriverViewModel to RoadflareTab for accurate status
- Add locationless Kind 30173 events for ROADFLARE_ONLY mode drivers
- ROADFLARE_ONLY drivers now publish availability without location/geohash
  (invisible to geographic search but trackable by pubkey subscription)
- Handle missing location gracefully in DriverAvailabilityEvent.parse()
- Make location nullable in DriverAvailabilityEvent.create() (null = ROADFLARE_ONLY mode)
- Add status parameter to NostrService.broadcastAvailability()
- Remove redundant methods: createOffline(), createWithoutLocation(),
  broadcastOffline(), broadcastAvailabilityWithoutLocation()
- Update DriverViewModel to use unified method with explicit parameters

Reduces ~80 lines of code duplication while maintaining the same behavior.
cancelRide() was setting local state to OFFLINE but not broadcasting
a Kind 30173 offline event, causing riders/followers to see stale
"available" status. Now broadcasts locationless offline event to
match the UI state.
NostrService:
- Merge sendRideOffer() and sendRoadflareOffer() into unified method
- New signature: sendRideOffer(driverPubKey, driverAvailabilityEventId?, ...)
- driverAvailabilityEventId is null for RoadFlare offers

RiderViewModel bug fixes:
- Fix #1 (wrong fare): Store fareEstimate/fareEstimateWithFees in RoadFlare state updates
- Fix #2 (boost fails): Add roadflareTargetDriverPubKey/Location fallback in boostDirectOffer()
- Fix #3 (countdown skipped): Reset directOfferTimedOut=false in all send methods

RiderUiState:
- Add roadflareTargetDriverPubKey and roadflareTargetDriverLocation fields
  for tracking RoadFlare target driver info needed by boost functionality
…r names

Architecture refactor:
- paymentHash now sent in Kind 3175 confirmation instead of Kind 3173 offer
- Fixes boost bug where resending offer overwrote driver's paymentHash with null
- HTLC is now locked AFTER acceptance using driver's wallet pubkey
- Driver extracts paymentHash from confirmation.paymentHash in subscribeToConfirmation()

RoadFlare UX improvement:
- FollowedDriversRepository now persists driver names to SharedPreferences
- Names load instantly on app startup (no pubkey flash)
- Cleanup on unfollow prevents orphaned cache entries

Files changed:
- RideConfirmationEvent: Added paymentHash + escrowToken fields
- RideOfferEvent: Removed paymentHash from create()
- NostrService: Updated sendRideOffer() and confirmRide() signatures
- RiderViewModel: Pass paymentHash in confirmRide() not sendRideOffer()
- DriverViewModel: Extract paymentHash from confirmation, not offer
- FollowedDriversRepository: Persist name cache to SharedPreferences
…osed

- Exclude sensitive SharedPrefs from cloud/device backup (both apps)
- Restrict user-installed CAs to debug builds only (both apps)
- Crypto self-test now aborts connection on hash_to_curve failure
- Remove deprecated :app module from settings.gradle.kts
- Delete app/ directory (deprecated, unused, outdated event kinds)
- Remove hash_to_curve runtime test (validated during development)
Security hardening:
- Verify all event signatures at relay level before processing
- Add expected pubkey validation for critical ride events:
  - RideAcceptanceEvent (wallet_pubkey for P2PK escrow)
  - RideConfirmationEvent (payment_hash, escrow_token)
  - DriverRideStateEvent (settlement, preimage)
  - RiderRideStateEvent (PIN verification)

Uses Quartz library's event.verify() for Schnorr signature validation.
- Add bounded message channel (capacity=256) for ordered processing
- Connection generation tracking prevents stale callback corruption
- Synchronized blocks for state+socket coherence
- Early shouldReconnect check prevents post-disconnect reconnects
- Atomic StateFlow.update for events/notices lists
- Fix backup rules to exclude correct SharedPreferences filenames
  (ridestr_secure_keys.xml, ridestr_wallet_keys.xml, etc.)
- Add privacy-sensitive data to backup exclusions (ride history,
  saved locations, vehicles) - Nostr sync is recovery path
- Wire expectedDriverPubKey validation in subscribeToAcceptance()
- Wire expectedRiderPubKey validation in subscribeToConfirmation()
- Update RiderViewModel call sites for direct offer acceptance
- Update DriverViewModel call sites for confirmation subscription
Document recent security improvements:
- Backup exclusions (420f54b): correct SharedPrefs filenames + privacy data
- Pubkey validation (420f54b): expectedDriverPubKey/expectedRiderPubKey wiring
- WebSocket concurrency (0e658f6): bounded channel, generation tracking
- Signature verification (d7c9064): relay-level event.verify()
- Add ensureRoadflareStateSynced() for cross-device sync on go-online
- Reject stale/out-of-order Kind 30014 events with timestamp tracking
- Add staleness filter to RoadFlare selection sheet (10-min freshness)
- Add auto key refresh request (status="stale" in Kind 3188)
- Handle key refresh requests in driver MainActivity
- Add status parameter to RoadflareKeyAckEvent.create() and publishRoadflareKeyAck()
Previously ensureRoadflareStateSynced() only fetched from Nostr when
local state was completely empty. This caused issues when two devices
had different RoadFlare keys - the phone would keep its stale local
key instead of syncing the newer one from Nostr.

Now the function always fetches from Nostr on go-online and compares
keyUpdatedAt timestamps. If Nostr has newer data, it replaces local
state. This ensures cross-device consistency when the same driver
account is used on multiple devices.
- Replace wholesale state replacement with union merge strategy
- Merge follower lists (union by pubkey, prefer approved + higher keyVersionSent)
- Merge muted lists (union, never auto-unmute from sync)
- Clamp keyVersionSent to selected key version to prevent invalid claims
- Add retry on null fetch before pushing local state
- Clear muted list when rider unfollows (enables safe union)
- Add background refresh after sync to detect unfollowed riders
- Add syncTriggeredRefresh SharedFlow for MainActivity observer
Extract refreshRoadflareFollowers() as local suspend function and use it
for both UI refresh button and sync-triggered background refresh.

Fixes codex review findings:
- Background refresh now adds new followers (was missing)
- Background refresh now fetches display names (was missing)
- Removes code duplication between UI and background refresh paths
Adds debug section to Driver app's Developer Options showing:
- Local key version and timestamp
- Nostr sync status (match/mismatch detection)
- Check, Sync, and Rotate Key buttons

Helps diagnose cross-device key sync issues.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add HtlcRefundOutcome sealed class for detailed failure diagnostics
- Add wallet key validation to catch mismatch after wallet restore
- Add getActiveKeysetFromMint() to fix keyset mismatch after mint switch
- Add failureReason field to HtlcRefundInfo for UI display
- Auto-backup wallet key to NIP-60 after connect
- Auto-backup wallet key before HTLC creation (protects existing users)
- Fix help text from "2 hours" to "15 min + 2 min buffer"
- Add comprehensive diagnostic logging for HTLC refunds
- Use java.util.Date instead of java.time.Instant for API safety
NUT-14 requires 64-hex preimage in witness. Some mints don't verify
the hash (allowing zeros workaround), but if they fix this, refunds
would fail. Now storing preimage when creating HTLCs so refunds can
use the real preimage if mints enforce hash verification.

- Add preimage field to PendingHtlc
- Thread preimage through lockForRide() → PendingHtlc → refund
- Fallback to zeros for old HTLCs without stored preimage
- Validate preimage format (64 hex chars) before storing
- CLAUDE.md: Update "HTLC Refund Gap" to "HTLC Refund Preimage Storage" (fixed)
- common README: Add preimage storage note to HTLC Locktime & Refund Flow section
- cashu-wallet SKILL: Update PendingHtlc data model, creation/refund flows
- Add comprehensive offer type comparison (Direct, Broadcast, RoadFlare)
- Document RoadFlare architecture: encryption model, follower lifecycle, key rotation
- Add payment methods section with HTLC preimage storage details
- Add Mermaid diagrams for ride flows and state machines
- Update Kind 3188 with status values (received vs stale) and refresh flow
- Document driver availability states and state machine entry points
- Consolidate RoadFlare status detection documentation
…recoverable HTLCs

- HTLC refund: Try spec-compliant first (no preimage), retry with zeros fallback if mint requires it
- Gate debug logging behind BuildConfig.DEBUG in CashuBackend, RoadFlare files
- Remove HTLC secret logging for security
- Add IRRECOVERABLE status for HTLCs that cannot be refunded (wallet key mismatch)
- Add acknowledgeIrrecoverableHtlc() to clear stuck HTLCs from pending sats
- Add "Clear" button in WalletSettings for HTLCs locked >30min
- Keyset selection: prefer explicit active=true, fall back to first sat keyset with warning

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Propagate actual HTTP response code (e.g., 429, 500) instead of
hard-coded 400 in MintRejected failure. This improves error messages
and debugging when HTLC refunds fail due to rate limiting or server
errors.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
variablefate and others added 28 commits January 31, 2026 10:09
When multiple drivers are contacted in a RoadFlare batch and a
non-first driver accepts, update the state to reflect the actual
accepting driver instead of keeping the stale first-driver reference.
If PIN verification is triggered multiple times, don't create a new
deposit invoice if one already exists. This fixes a race condition
where the rider pays the first invoice but the driver tracks the
second (unpaid) quote, causing the payment to fail.
Fixes stale deposit state blocking new cross-mint rides. The guard in
shareDepositInvoice() checks pendingDepositQuoteId != null to prevent
duplicate invoices, but this field wasn't cleared on all ride-end paths.

Added clearing to: cancelRide(), finishAndGoOnline(), cancelCurrentRide(),
bridge claim failure/exception, completeRideInternal(), handleConfirmationTimeout(),
and clearAcceptedOffer().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…reset

- Wrap 18 sensitive log statements (preimage, paymentHash, escrowToken,
  secret) with BuildConfig.DEBUG checks to prevent partial secrets from
  appearing in release logcat
- Add walletStorage.clearCounters() call in resetWallet() to prevent
  NUT-13 derivation desync when importing new mnemonic after reset
When restoring vehicles from backup, the addVehicle() logic forced the
first vehicle to be primary (since list was empty after clear), while
subsequent vehicles kept their backup isPrimary value. This caused
multiple vehicles to be marked as primary.

Added VehicleRepository.restoreFromBackup() that:
- Bypasses addVehicle() to avoid the forced primary logic
- Normalizes the primary flag to ensure exactly one vehicle is primary
- Handles edge cases: no primary (make first primary), multiple primaries
  (keep only the first one marked)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ters

Clears orphaned activeVehicleId after vehicle restore from another device,
preventing incorrect "Last used" badge. Also deletes deprecated VehicleSyncAdapter
and SavedLocationSyncAdapter (superseded by ProfileSyncAdapter).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Wire sound/vibration settings to DriverOnlineService, RiderActiveService,
  and RoadflareListenerService (Finding 1)
- Keep "Ride cancelled" notification until next action (Finding 2)
- Add driverOnlineStatus tracking to prevent duplicate RoadFlare
  notifications when DriverViewModel is active (Finding 3)
- Make RoadFlare request notifications dismissible and auto-cancel
  on app resume (Finding 4)
- Add notification permission check and request flow for RoadFlare
  alerts on Android 13+ (Finding 5)
- Add RoadflareOnly status with proper notification text (Finding 6)
- Route notifications to proper channels (RIDE_UPDATE, RIDE_CANCELLED)
  based on status type (Finding 7)
- Make DriverOnlineService authoritative for driverOnlineStatus
- Set status in onStartCommand (closes race window with RoadflareListenerService)
- Clear stale status in onCreate/onDestroy
- Channel selection now checks alertStack.isNotEmpty() first
- Chat and ride request alerts both route to CHANNEL_RIDE_REQUEST
- Remove redundant ViewModel status updates (service is single source of truth)
Phase 3 notification refactor:
- Add AlertType sealed interface for unified alert types
- Add NotificationCoordinator for centralized status/alert management
- Add NotificationTextProvider interface with driver/rider implementations
- Wire services to use coordinator, eliminating ~150 lines of duplication
- Preserve critical behaviors: status-driven clearing, NewRequest two-layer,
  priority regression fix (Cancelled is high-priority without alerts)

Fix Roadflare-only race condition (Codex finding #1):
- Add startRoadflareOnly() that sets ROADFLARE_ONLY immediately
- Avoids race window where start() sets AVAILABLE then updateStatus()
  sets ROADFLARE_ONLY (~50-200ms window where requests were dropped)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add debug logging to Kind 3186 parsing (wrong recipient, expired, decrypt fail)
- Add debug logging to NostrService.publishRoadflareKeyShare
- Request key refresh when rider has no key (null) for a driver (rate-limited)
- Refactor MainActivity RoadFlare handler logging

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
When transitioning from ROADFLARE_ONLY to AVAILABLE via proceedGoOnline(),
the service status was not being updated. The notification showed "RoadFlare
only" while UI was AVAILABLE, and dedup logic in RoadflareListenerService
failed (status still ROADFLARE_ONLY instead of AVAILABLE).

Fix: Call DriverOnlineService.updateStatus(Available) in wasRoadflareOnly branch.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Change card background from primaryContainer to surfaceVariant for better
dark mode visibility on:
- Driver: "Arrived at Pickup" screen
- Rider: "Your driver has arrived" screen

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Reset RoadFlare on-ride status in completeRideInternal() so driver
  broadcasts ONLINE instead of ON_RIDE after completing a ride
- Preserve pre-ride mode (ROADFLARE_ONLY vs AVAILABLE) and restore it
  when driver clicks "Stay Online" after ride completion
- Clear stageBeforeRide in all cancellation paths to prevent stale
  mode restoration

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Bug fixes (payment safety critical):
- cancelRide(): Add timeout job cancellation before coroutine
- cancelCurrentRide(): Add profile unsubscribe + escrow state clearing
- performCancellationCleanup(): Add timeout + profile cleanup
- handleConfirmationTimeout(): Add profile unsubscribe

Quick wins:
- Unify staleness threshold to 5 min (was 10 min in RiderModeScreen)
- Add SoundManager overloads that take SettingsManager directly
- Simplify 10 sound/vibration call sites in both services

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Keep availability subscription open through DRIVER_ACCEPTED stage until
driver sends EN_ROUTE_PICKUP. This allows rider to detect if driver goes
offline during the acceptance handshake window.

Changes:
- Remove early closeDriverAvailabilitySubscription() from confirmRide()
  and autoConfirmRide() - subscription now stays open
- Add closeDriverAvailabilitySubscription() to EN_ROUTE_PICKUP handler
  when driver has acknowledged the ride
- Add closeDriverAvailabilitySubscription() to handleDriverCancellation()
  to prevent subscription leaks
- Extend availability callback to handle DRIVER_ACCEPTED stage and call
  handleDriverCancellation() when driver goes offline
- Add DRIVER_ACCEPTED to cancellation subscription stage filter

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Phase 4 ViewModel deduplication: Extract common patterns into reusable
utilities in common/util/.

PeriodicRefreshJob: Encapsulates periodic coroutine refresh pattern with
proper lifecycle management (start/stop). Used for chat message polling.

RideHistoryBuilder: Helper functions for ride history creation -
extractCounterpartyFirstName(), toDistanceMiles(), currentTimestampSeconds().
Reduces duplication across 7 history creation sites.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Move duplicated UI code from both apps into common module:

- KeyBackupScreen.kt: Key backup display (moved from both apps)
- ProfileSetupContent.kt: Profile editing form with wrapper pattern
- OnboardingComponents.kt: KeySetupScreen, BackupReminderScreen
- SettingsComponents.kt: SettingsSwitchRow, SettingsNavigationRow, SettingsActionRow

The wrapper/content pattern allows app-specific ViewModels while sharing
UI implementation. App screens now call common content composables with
role-specific customization (e.g., "Tell drivers about yourself").

Saves ~735 lines of duplicated code across both apps.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…ganization

NostrService decomposition (35% reduction, 2893→1888 lines):
- Extract NostrCryptoHelper (6 NIP-44 encryption methods)
- Extract ProfileBackupService (profile, history, backup methods)
- Extract RoadflareDomainService (16 RoadFlare methods)
- All methods delegate to domain services for backward compatibility

Payment code organization:
- Extract CashuTokenCodec for stateless token encoding/decoding
- Add region comments to CashuBackend (13 sections with HTLC invariants)
- Add region comments to WalletService (11 sections with flow invariants)
- Add correlation ID logging [RIDE xxxxxxxx] for payment tracing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update CONNECTIONS.md and RIDER_VIEWMODEL.md docs
- Update rider-app README
- Minor fix to PeriodicRefreshJob

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add NostrCryptoHelper, ProfileBackupService, RoadflareDomainService to Nostr Layer table
- Add CashuTokenCodec to Payment System table
- Add new domain service connections to Connections Table
- Note region organization in WalletService and CashuBackend descriptions

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- CONNECTIONS.md: Updated architecture diagram with domain services
  (NostrCryptoHelper, ProfileBackupService, RoadflareDomainService,
  CashuTokenCodec), added Phase 5 section header, updated Nostr Layer
  and Payment System dependency trees
- PAYMENT_ARCHITECTURE.md: Added CashuTokenCodec to file structure,
  documented Phase 5 reorganization (region comments, correlation IDs),
  added Layer 1.5 CashuTokenCodec section
- RIDER_VIEWMODEL.md: Added correlation ID logging to ride request flow
- DRIVER_VIEWMODEL.md: Added correlation ID logging to completion flow

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add 83 unit tests for payment crypto (PaymentCrypto, CashuCrypto, CashuTokenCodec)
- Configure Robolectric for Android-dependent test code
- Add COMPATIBILITY_CONTRACTS.md documenting API stability requirements
- Add PAYMENT_SAFETY.md with modification checklist and HTLC invariants
- Document correlation ID design in PAYMENT_ARCHITECTURE.md

Test files verify NUT-00 hash_to_curve, NUT-13 deterministic derivation,
BIP-39 seed generation, token encoding round-trips, and HTLC secret parsing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Replace nullable return types with sealed classes for rich error information:
- lockForRide() returns LockResult (Success/Failure variants)
- claimHtlcPayment() returns HtlcClaimResult (Success/Failure variants)

Failure variants include: NotConnected, InsufficientBalance, ProofsSpent,
PreimageMismatch, MintRejected, and others with relevant context data.

Updated callers in RiderViewModel and DriverViewModel to use exhaustive
when expressions for proper error handling and logging.

Added 22 unit tests for the new sealed classes (105 total tests pass).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add MintApi interface to abstract HTTP transport for testability
- Add OkHttpMintApi production implementation
- Add HtlcSwapOutcome and HtlcClaimOutcome sealed classes to CashuBackend
- Wire error variants: MintUnreachable, SwapRejected, TokenParseFailed, SignatureFailed, MintRejected
- Update WalletService to map CashuBackend outcomes to WalletService result types
- Add FakeMintApi for test injection
- Add CashuBackendErrorTest with 26 tests covering all outcome types

This enables proper error propagation (no more lost error context),
test injection via FakeMintApi, and compile-time exhaustiveness checking.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add RelayManager.awaitConnected() to centralize the 15-second relay
  connection wait logic with optional logging tag
- Refactor ProfileBackupService, RoadflareDomainService, NostrService,
  RemoteConfigManager to use awaitConnected() instead of duplicated loops
- Update PAYMENT_ARCHITECTURE.md to reflect NUT-11 signature format
  (sign only secret, not secret+C)
- Pass event kinds to deleteEvents() for proper NIP-09 deletion

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add HtlcClaimResult.Failure.MintUnreachable for network failures
- Wire MintUnreachable in WalletService claimHtlcPayment()
- Add CashuBackend.setTestState() and testActiveKeyset for test injection
- Route verifyProofsBalance() through effectiveMintApi
- Expand CashuBackendErrorTest with MockK integration tests
- Add test dependencies: mockk, androidx.test.core, kotlinx-coroutines-test
- Update documentation for Phase 6 test infrastructure (138 tests)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Extract Nip60Store interface from Nip60WalletSync to enable test injection
for verifying proof safety invariants. The interface abstracts 11 NIP-60
operations used by WalletService.

New test infrastructure:
- FakeNip60Store: Test double with call log for order verification
- MainDispatcherRule: JUnit rule for coroutine dispatcher override
- ProofConservationTest: 10 contract tests for proof safety
- FakeNip60StoreTest: 26 unit tests for the test double

Key contracts verified:
- Proofs remain retrievable until explicitly deleted
- Republish-before-delete order invariant (prevents proof loss)
- Failed publishes are counted for retry verification
- Proof selection filters by mint URL (multi-mint safety)

Total payment tests: 180 (all passing)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update test count from 138 to 181 tests across all docs
- Add Nip60Store interface documentation (testable abstraction)
- Add FakeNip60Store test double documentation
- Add ProofConservationTest contract tests documentation
- Add MainDispatcherRule for coroutine testing
- Update per-file test counts to match actual values
- Document publish-before-delete verification pattern

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@variablefate variablefate merged commit 2142349 into main Feb 4, 2026
@variablefate variablefate deleted the roadflare branch February 4, 2026 05:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant