Skip to content

Latest commit

 

History

History
572 lines (400 loc) · 23.3 KB

File metadata and controls

572 lines (400 loc) · 23.3 KB

Claude Guidelines for Flipcash iOS

This file provides instructions for Claude when working on the Flipcash iOS codebase.


Maintaining This Document

Claude should proactively update this file when discovering critical information that would prevent mistakes or save significant time in future sessions. This includes:

  • New hard rules or constraints discovered through errors
  • Critical patterns that aren't obvious from the code
  • Module boundaries or dependencies that caused issues
  • Non-obvious project conventions

Keep this document lean. Only add information that is:

  1. Not discoverable by reading the code directly
  2. Would cause errors or significant rework if unknown
  3. Applies broadly across the project (not one-off edge cases)

When adding new information, place it in the appropriate existing section. Remove outdated information when it no longer applies.


Plans & Analysis Records

Record analyses and implementation plans in .claude/plans/ when:

  • Performing deep-dive analysis of new features or RPC changes
  • Planning multi-step implementations
  • Documenting architectural decisions
  • Investigating complex systems that span multiple files

File naming: YYYY-MM-DD-<topic>.md (e.g., 2025-11-27-swap-rpc-analysis.md)

Purpose: These records allow future sessions to reference prior analysis without re-exploring the codebase. Keep them detailed but focused on actionable information.


Reflections

Review .claude/reflections/index.md before making changes. This log documents past situations where fixes went off track — over-engineering, breaking existing patterns, or introducing regressions. Reading it helps avoid repeating the same mistakes.


Behavior & Approach

Working Style

  • Understand the context. Take your time to understand how the changes should fit into the complete project. Perhaps a refactor is required. Perhaps the current structure is not ideal. Take your time to indetify this.
  • Double-check your work. Verify changes compile and don't break existing functionality.
  • Ask clarifying questions. When requirements are ambiguous or something is unclear or can have multiple meanings, don't assume. Ask clarifying questions where needed but try to keep these as concise and as minimal as possible.

Before Making Changes

  1. Read the relevant files first - never propose changes to code you haven't read
  2. Understand the existing patterns and conventions in the current file but also any related or dependant files
  3. Check module boundaries (see Hard Rules below)
  4. Consider impact on other parts of the codebase

Communication

  • Be direct and concise
  • When uncertain, say so rather than guessing
  • Provide file paths with line numbers when referencing code (e.g., Session.swift:326)

Hard Rules (Non-Negotiable)

Module Boundaries

Flipcash must NEVER import CodeServices:

// ❌ NEVER do this in Flipcash code
import CodeServices

// ✅ ALWAYS use this instead
import FlipcashCore

FlipcashCore re-exports necessary types. Violating this creates tight coupling with legacy code.

Testing Framework

Use Swift Testing, NOT XCTest:

// ❌ WRONG
import XCTest
class MyTests: XCTestCase { ... }

// ✅ CORRECT
import Testing
@Suite struct MyTests { ... }

Exhaustive Switch Statements

Always prefer switch over if case for enums:

// ❌ BAD: Silent failure if enum changes
guard case .sufficient(let amount) = result else {
    showError()
    return
}

// ✅ GOOD: Compiler error if enum changes
switch result {
case .sufficient(let amount):
    handleSuccess(amount)
case .insufficient(let shortfall):
    handleError(shortfall)
}

Modernize Incrementally

When writing new code or touching isolated screens, prefer modern Swift/SwiftUI APIs. This is a gradual migration — don't refactor working code just to modernize it, but do use modern patterns in net-new or self-contained work.

Legacy Modern Notes
ObservableObject / @Published @Observable Use @State in views instead of @StateObject
@EnvironmentObject @Environment For new dependencies; existing @EnvironmentObject stays until the injected type is migrated
@AppStorage wrapping UserDefaults manually @AppStorage directly For simple per-screen preferences
onChange(of:perform:) (deprecated) onChange(of:initial:_:) Use initial: true when the handler should also fire on appear

Existing ObservableObject classes (Session, Client, controllers) stay as-is until their dependents are migrated. A single class must use one system — either ObservableObject with @Published, or @Observable. Mixing causes silent observation failures.

Generated Files

Never modify files generated by FlipcashGen - changes will be overwritten. Instead, update the service files that use the generated code.

Legacy Code

Do not modify:

  • Code/ - Legacy Code Wallet (inactive)
  • Flipchat/ - Legacy chat app (inactive)
  • PoolController and pools-related code - feature is deprecated

Database Schema Changes

Bump SQLiteVersion in Info.plist on every schema change. The app does not run migrations — when the version number increases, the database is deleted and rebuilt from server data on next login (SessionAuthenticator.initializeDatabase). This means:

  • Adding/removing tables or columns → bump version
  • Changing which table a query reads from → bump version if the old schema can't satisfy the new query
  • No migration code needed, but all data must be recoverable from server

Logging: Variables Go in Metadata

All variable data must go in structured metadata. The message string is a constant, free-form description. Two reasons, in order of importance:

  1. Privacy. The redactors (PatternRedactor, SensitiveKeyRedactor in FlipcashCore/Sources/FlipcashCore/Logging/Middleware/) only scan entry.metadata. Anything interpolated into the message is written verbatim to the file export, the Bugsnag ring buffer attachment, and OSLog. Putting every variable in metadata means values that look innocent today get the redactor safety net automatically — instead of relying on developers to spot which ones are sensitive.
  2. Queryability. Metadata is structured key=value, so you can grep owner= or filter by key in a structured log viewer. Interpolated values get baked into a string and lose their key.
// ❌ BAD: leaks the public key in plaintext to every log sink
logger.info("New encryption box, public key: \(box.publicKey.base58)")

// ❌ BAD: even non-sensitive variables don't belong in the message
logger.info("Requested swap of \(amount) for \(token.symbol)", metadata: [
    "swapId": "\(swapId.base58)",
])

// ✅ GOOD: message is a constant, every variable is in metadata
logger.info("New encryption box", metadata: ["publicKey": "\(box.publicKey.base58)"])
logger.info("Requested swap", metadata: [
    "amount": "\(amount)",
    "token": "\(token.symbol)",
    "swapId": "\(swapId.base58)",
])

Never log proto blobs whole. A naked \(response.tokenAccountInfos) or \(notification) recursively serializes every field, including the base58 ones. Extract the specific diagnostic values you actually need into metadata instead — usually a count, a type, or an error, not the whole record.

Package.resolved Policy

Always commit the workspace Package.resolved:

  • Code.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved - MUST be committed
  • ❌ Individual package Package.resolved files - ignored by git

This ensures deterministic builds across all developers and CI systems while minimizing merge conflicts. The workspace Package.resolved is the single source of truth for all dependency versions.


Getting Started

Open Code.xcodeproj in Xcode 16.x. Swift packages resolve automatically on first open. Build and run the Flipcash scheme.

Regenerating Protos

Swift gRPC bindings in FlipcashAPI/Sources/FlipcashAPI/Generated and FlipcashCoreAPI/Sources/FlipcashCoreAPI/Generated are generated from .proto files pulled from the server-protobuf repos. To regenerate:

cd Scripts
./run -a flipcashPayments
./run -a flipcashCore

Each invocation clones the latest .proto files from the upstream repo, replaces the local proto/ directory, and regenerates the Swift code in Generated/.

Required tools (checked by the script; aborts if missing):

  • protocbrew install protobuf
  • protoc-gen-swiftbrew install swift-protobuf
  • protoc-gen-grpc-swift (version 1.x, not 2.x) — ./Scripts/install-grpc-swift-1-plugin.sh

The "Generate Flipcash Services" Xcode scheme wraps these same two commands — use either, they produce the same output.

Never modify files under Generated/ directly — changes will be overwritten on the next regen.


Architecture & Patterns

Design Pattern: MVVM + Container DI

Container (DI)
├── Client (gRPC)
├── FlipClient (Flipcash APIs)
├── AccountManager (Keychain)
└── SessionContainer (when logged in)
    ├── Session (main state, ObservableObject)
    ├── RatesController
    │   ├── VerifiedProtoService (actor – caches verified exchange rate + reserve state proofs)
    │   └── LiveMintDataStreamer (actor – bidirectional streaming for rates/reserves)
    ├── HistoryController
    └── Database (SQLite)
  • ViewModels provide over multi-screen flows or complex navigation patterns but not necessary for standalone, self-contained, isolated screens.
  • Session is the main state object after authentication
  • Controllers handle business logic and data persistence

gRPC Call Options

Two CallOptions presets exist in CodeService.swift — using the wrong one causes subtle bugs:

Preset Timeout Use For Example RPCs
.default 15 seconds Unary RPCs (request → response) fetchMessages, sendMessage, createAccounts
.streaming None Server-streaming and bidirectional RPCs openMessageStream, submitIntent, streamLiveMintData, statefulSwap

The defaultCallOptions on each gRPC client is .default, so unary RPCs get the 15s timeout automatically. Streaming RPCs must explicitly pass callOptions: .streaming — omitting it silently applies the 15s deadline, killing long-lived streams.

// ❌ BAD: Uses default 15s timeout — stream dies silently
let stream = service.openMessageStream(request) { response in ... }

// ✅ GOOD: No timeout — stream stays open, managed by keepalive
let stream = service.openMessageStream(request, callOptions: .streaming) { response in ... }

Key Architectural Concepts

  1. Quarks - Smallest unit of any currency (like cents for dollars)
  2. ExchangedFiat - Wraps underlying currency + converted display value
  3. BondingCurve - Pricing for custom currencies
  4. AccountCluster - Manages keys per mint
  5. VerifiedState - Bundles server-signed exchange rate proof (rateProto) and optional reserve state proof (reserveProto). Required when submitting any payment intent. For launchpad currencies, reserveProto is mandatory — the server rejects intents without it.
  6. SendCashOperation - Orchestrates peer-to-peer transfers via a rendezvous handshake. Has two concurrent paths: Path 1 (advertise bill with verified state) and Path 2 (listen for grab, then transfer). Both paths share a resolved VerifiedState.

Technology Stack

Required Technologies

Technology Version/Notes
Swift 6.0 (language mode); Xcode toolchain 16.x
iOS Minimum 17.0
UI Framework SwiftUI (primary), UIKit (AppDelegate, navigation)
Testing Swift Testing (import Testing)
Database SQLite via SQLite.swift (fork, see below)
Networking gRPC via grpc-swift
Crypto Ed25519 via CodeCurves

Package Structure

Flipcash/          # Main app - focus here
FlipcashCore/      # Business logic, models, clients
FlipcashUI/        # UI components, theme
FlipcashAPI/       # gRPC proto definitions
CodeServices/      # Shared Solana services (don't import in Flipcash)
CodeCurves/        # Ed25519 cryptography
CodeScanner/       # C++/OpenCV circular code scanning (see below)

SQLite.swift Fork

We use a fork of SQLite.swift (dbart01/SQLite.swift), not the official stephencelis/SQLite.swift. The fork is pinned to master branch and adds two changes on top of the official 0.15.4 base:

  1. Upsert WHERE clause fix — moves whereClause after DO UPDATE SET (the official repo places it before ON CONFLICT, producing invalid SQL for filtered upserts like table.filter(...).upsert(...))
  2. Custom dispatch queue injection — adds a queue: parameter to Connection.init so callers can supply their own DispatchQueue
  3. Public Setter access (pending)Setter.column and Setter(excluded:) need to be made public so callers can build custom ON CONFLICT SET clauses (e.g., COALESCE(excluded.column, column) for conditional upserts). See Database+Balance.swift TODO.

Do not switch to the official repo without verifying:

  • Filtered upserts still generate valid SQL
  • Connection.init(queue:) is no longer needed
  • Custom SET clause building still compiles

CodeScanner Project

C++ library for encoding, decoding, and scanning custom circular 2D codes ("Kik Codes"). Uses OpenCV 4.10.0 and a bundled ZXing Reed-Solomon subset.

  • Location: CodeScanner/
  • Public API: CodeScanner/CodeScanner/Code.h (KikCodes class — encode, decode, scan)
  • Used by: CodeExtractor.swift, CashCode.Payload+Encoding.swift
  • Full spec: .claude/spec.md (API details, build docs, OpenCV upgrade history)
  • Updating OpenCV: cd CodeScanner && ./Scripts/build_opencv.sh --version <version>

Code Style & Conventions

File Organization

  • Screens go in Flipcash/Core/Screens/
  • ViewModels are colocated with their screens
  • Models go in FlipcashCore/Sources/FlipcashCore/Models/
  • Database models go in Flipcash/Core/Controllers/Database/Models/

Naming Conventions

  • ViewModels: {Screen}ViewModel (e.g., GiveViewModel)
  • Screens: {Name}Screen (e.g., ScanScreen)
  • Controllers: {Domain}Controller (e.g., RatesController)

Import Order

import SwiftUI       // System frameworks first
import FlipcashCore  // Then internal packages
import FlipcashUI

Avoid Over-Engineering

  • Don't add features beyond what was asked
  • Don't add error handling for impossible scenarios
  • Don't create abstractions for one-time operations
  • Don't add comments to code you didn't change
  • Three similar lines of code is better than a premature abstraction

Testing

Framework: Swift Testing

import Testing
@testable import Flipcash

@Suite("Session Tests")
struct SessionTests {

    @Test("Sufficient funds returns correct amount")
    func sufficientFunds() {
        // Arrange
        let session = makeTestSession()

        // Act
        let result = session.hasSufficientFunds(for: amount)

        // Assert
        #expect(result == .sufficient(amount))
    }
}

Running Tests

Simulator device names and OS versions in test commands change with Xcode releases. Update destinations as new simulators become available.

# Run all tests
xcodebuild test -scheme Flipcash -destination 'platform=iOS Simulator,name=iPhone 17' -testPlan AllTargets

# Run UI tests on current and previous OS
xcodebuild test -scheme Flipcash -only-testing:FlipcashUITests \
  -destination 'platform=iOS Simulator,name=iPhone 16,OS=18.6' \
  -destination 'platform=iOS Simulator,name=iPhone 17'

# Run specific test suite
xcodebuild test -scheme Flipcash -destination 'platform=iOS Simulator,name=iPhone 17' -only-testing:FlipcashTests/SessionTests

Test Naming

  • Use descriptive names that explain the scenario
  • Format: func testMethodName_scenario_expectedResult() or use @Test("description")

Test the Actual Implementation

NEVER recreate functionality in tests. Always test the actual implementation:

// ❌ BAD: Recreates the logic, proves nothing about the real code
@Test func testTotalBalance() {
    let sum = balance1.converted.decimalValue + balance2.converted.decimalValue
    let total = Quarks(fiatDecimal: sum, ...)
    #expect(total.formatted() == "$8.10")  // Tests nothing real
}

// ✅ GOOD: Tests the actual Session.totalBalance implementation
@Test func testTotalBalance() {
    let session = makeTestSession(balances: [balance1, balance2])
    let total = session.totalBalance
    #expect(total.converted.formatted() == "$8.10")
}

If the code under test is difficult to call directly, create test support extensions or mock dependencies rather than duplicating the logic.

Test Support Extensions

Keep production code clean - test-only helpers belong in the test target:

// ❌ BAD: Adding #if DEBUG to production code
// Flipcash/Core/Controllers/RatesController.swift
#if DEBUG
func configureTestRates(...) { ... }
#endif

// ✅ GOOD: Extension in test target
// FlipcashTests/TestSupport/RatesController+TestSupport.swift
extension RatesController {
    func configureTestRates(...) { ... }
}

Place test support extensions in FlipcashTests/TestSupport/ using the naming pattern {Type}+TestSupport.swift.

CI Compatibility

All tests must work on both Xcode Cloud and locally. Never use APIs that are sandboxed or unavailable on Xcode Cloud:

  • Process / ProcessInfo for shelling out (sandboxed on Xcode Cloud)
  • xcrun simctl from within tests
  • ❌ Host-only filesystem access
  • UIPasteboard, XCUIApplication, XCUIElement — standard XCUITest APIs

Regression Tests

Every crash fixed from Bugsnag (or similar) gets a dedicated regression test in FlipcashTests/Regressions/.

  • One file per incident: Regression_{bugsnag_id}.swift
  • Suite name includes the short ID: @Suite("Regression: {short_id} – {brief description}")
  • Reproduce the crash path, not just the low-level fix. If the crash came through EnterAmountCalculator, test through EnterAmountCalculator.
// FlipcashTests/Regressions/Regression_698ef3b65e6cc4bb5554e13d.swift

@Suite("Regression: 698ef3b – Quarks comparison overflow for high-rate currencies")
struct Regression_698ef3b {

    @Test("CLP quarks comparison across 6 and 10 decimal precisions does not overflow")
    func quarksComparison_CLP_doesNotOverflow() { ... }
}

Git & Workflow

Commit Messages

<type>: <short description>

<optional body explaining why>

Types: feat, fix, refactor, test, docs, chore

Before Committing

  1. Code compiles without errors and no new warnings: xcodebuild build -scheme Flipcash
  2. Tests pass: xcodebuild test -scheme Flipcash -destination 'platform=iOS Simulator,name=iPhone 17' -testPlan AllTargets
  3. Review changes with git diff
  4. Module boundaries respected (no CodeServices in Flipcash)
  5. Switch statements are exhaustive (no unnecessary default cases)
  6. Changes are minimal and focused on the task

Common Pitfalls

Pitfall Solution
Modifying generated proto files Update service files instead
Working on pools/betting code Feature is deprecated, ignore it
Adding unnecessary abstractions Keep it simple, solve the current problem
Completing a transaction without refreshing balances Call session.updatePostTransaction() after any transaction completes
Canceling/modifying SendCashOperation in dismissCashBill Never explicitly call cancel() or invalidateMessageStream() on SendCashOperation from dismissCashBill. After a grab, the received bill is a live SendCashOperation that others can scan ("quick give and grab" chain). Setting sendOperation = nil is fine (deinit cleans up), but explicit teardown kills a live bill. The operation's complete() method handles stream teardown on success/failure.
Using default CallOptions for streaming RPCs Streaming RPCs (openMessageStream, submitIntent, streamLiveMintData, statefulSwap) must use callOptions: .streaming. The default 15s timeout silently kills long-lived streams. See gRPC Call Options.
Showing a received bill without verifiedState Every call to showCashBill must pass verifiedState — even for received: true bills. The received bill creates a live SendCashOperation for the "quick give and grab" chain. Without verifiedState, launchpad currency transfers fail with "reserve state is required". Both receiveCash (scan) and receiveCashLink (deep link) must provide it.
matchedGeometryEffect applied after .frame .matchedGeometryEffect must come BEFORE .frame in the modifier chain. Wrong order causes hero animations to fail silently: you see two separate views fading in/out at their own static positions instead of one morphing element. Paul Hudson's hackingwithswift example uses the wrong order and does not work on current iOS. Correct: Rectangle().fill(.red).matchedGeometryEffect(id:in:).frame(width:height:). Incorrect: Rectangle().fill(.red).frame(width:height:).matchedGeometryEffect(id:in:). Also note: .transition(.identity) on a parent containing matched views kills the animation entirely — matched geometry needs the parent view to remain in the tree briefly for interpolation, and .identity removes it instantly.

Quick Reference

Key Files

Session & Auth:
- Flipcash/Core/Session/Session.swift
- Flipcash/Core/Session/SessionAuthenticator.swift

Payments & Operations:
- Flipcash/Core/Screens/Main/Operations/SendCashOperation.swift
- Flipcash/Core/Screens/Main/Operations/ScanCashOperation.swift
- FlipcashCore/Sources/FlipcashCore/Models/VerifiedState.swift
- FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/VerifiedProtoService.swift

Multi-Currency:
- FlipcashCore/Sources/FlipcashCore/Models/Fiat.swift (Quarks)
- FlipcashCore/Sources/FlipcashCore/Models/ExchangedFiat.swift
- FlipcashCore/Sources/FlipcashCore/Models/BondingCurve.swift

Rates & Streaming:
- Flipcash/Core/Controllers/RatesController.swift
- FlipcashCore/Sources/FlipcashCore/Clients/Payments API/Services/LiveMintDataStreamer.swift

Database:
- Flipcash/Core/Controllers/Database/Schema.swift
- Flipcash/Core/Controllers/Database/Database.swift

Key Constants

// USDC
PublicKey.usdc // Main stablecoin mint
PublicKey.usdc.mintDecimals // 6

// Bonding Curve
BondingCurve.startPrice  // $0.01
BondingCurve.endPrice    // $1,000,000
BondingCurve.maxSupply   // 21,000,000 tokens

Xcode MCP Server

Prefer Xcode MCP tools over xcodebuild shell commands when the Xcode MCP server is available. It provides direct integration with the open Xcode workspace for building, testing, reading/writing project files, rendering SwiftUI previews, and searching Apple documentation.

Fall back to xcodebuild when the MCP server is not connected or when you need CLI-specific options (e.g., -testPlan, -only-testing).

Build Commands

Fallback xcodebuild commands (when MCP is unavailable):

# Build
xcodebuild build -scheme Flipcash -destination 'generic/platform=iOS'

# Test
xcodebuild test -scheme Flipcash -destination 'platform=iOS Simulator,name=iPhone 17'

# Clean
xcodebuild clean -scheme Flipcash