This file provides guidance to AI assistants (Claude Code, Gemini, Codex, etc.) when working with code in this repository.
Gem Wallet is a sophisticated multi-chain cryptocurrency wallet iOS application built with SwiftUI. The project follows a highly modular architecture with a Rust core for blockchain operations and a Swift frontend organized into independent feature modules.
The codebase is organized into three main layers:
- Features/ - 22 independent UI feature modules (Assets, Transfer, Staking, etc.)
- Packages/ - Core utilities and shared components (Primitives, Components, Store, etc.)
- Services/ - Business logic and data layer services
- core/ - Rust-based blockchain engine integrated via FFI
- MVVM with SwiftUI: Observable ViewModels with declarative UI
- Dependency Injection: AppResolver pattern with environment-based injection
- Protocol-Based Design: Services implement protocols for testability
- Swift Package Manager: Each feature and package is an independent SPM module
- Tab-based navigation with independent NavigationPath stacks per tab
- Centralized NavigationStateManager for state coordination
- Deep linking support via URL parsing and routing
# Bootstrap the project (required for first setup)
just bootstrap
# Clean build artifacts and DerivedData
just clean
# Build the project
just build
# Build a specific package
just build-package $PACKAGE. Example: just build-package Primitives
# Build and run tests
just test-all
# Build and run tests for a specific package
just test $PACKAGE. Example: just test PrimitivesTests
# Run UI tests
just test-ui
# Generate code (models, SwiftGen assets)
just generate
# Generate Rust-to-Swift bindings
just generate-stone# Generate model types from Rust core
just generate-model
# Generate SwiftGen assets and localization
just generate-swiftgen
# Update localization files
just localize# Resolve all Swift Package dependencies
just spm-resolve-all
# Update core submodule
just core-upgrade
# Generate UML diagrams
just uml-app- Build Logs: Build logs can be found in
build/DerivedData. When ajust buildcommand fails, examine the output in the console for specific Xcode errors. - Failing Tests: When a test fails using
just test TARGET, first try running the test directly in Xcode to use the interactive debugger. - Environment Issues: If you encounter issues after pulling new changes, run
just bootstrapagain to ensure all dependencies and generated files are up to date. - Rust Core Issues: If you suspect an issue in the
core/submodule, consult thecore/README.mdfor specific Rust-related debugging steps.
- Primary Build Method: Use
just buildorjust build-package PACKAGE(already uses xcodebuild internally) - If swift build fails: Use xcodebuild directly instead:
# For full project build xcodebuild -project Gem.xcodeproj -scheme Gem -sdk iphonesimulator -destination "platform=iOS Simulator,name=iPhone 17" build
- SwiftUI: Native iOS UI framework
- Swift Package Manager: Dependency management
- Rust Core: Blockchain operations via FFI bindings
- SQLite: Local data storage via custom Store package
- Combine: Reactive programming support
- WalletCore: TrustWallet's blockchain library
- SwiftGen: Code generation for assets and localization
- SwiftFormat: Code formatting
- Xcode AI Documentation:
/Applications/Xcode.app/Contents/PlugIns/IDEIntelligenceChat.framework/Versions/A/Resources/AdditionalDocumentation- Contains latest SwiftUI, AppKit, UIKit, and framework documentation
- Includes guides for new Apple features and APIs
- Reference these docs when implementing new iOS features or using latest Apple frameworks
Each feature in Features/ follows this pattern:
Features/[FeatureName]/
├── Package.swift # Independent SPM package
├── Sources/
│ ├── Scenes/ # SwiftUI Views
│ ├── ViewModels/ # Observable ViewModels
│ ├── Protocols/ # Protocols
│ ├── Types/ # Feature-specific types
│ └── Views/ # Reusable components
│ ├── Services/ # Services
├── Tests/ # Unit tests
└── TestKit/ # Testing utilities
Services follow protocol-based design:
protocol ServiceProtocol {
func operation() async throws -> Result
}
class ConcreteService: ServiceProtocol {
private let store: Store
private let provider: Provider
func operation() async throws -> Result {
// Implementation
}
}ViewModels use Swift's @Observable macro:
@Observable
@MainActor
final class FeatureViewModel {
private let service: ServiceProtocol
var isLoading: Bool = false
var data: [Model] = []
func fetch() async { ... }
}Services are injected via SwiftUI Environment:
.environment(\.walletService, services.walletService)
.environment(\.assetsService, services.assetsService)- Each feature module has its own test suite
- TestKit packages provide mocking utilities
- Protocol-based testing with dependency injection
- Both unit tests and UI tests are supported
- Unit tests:
just test - UI tests:
just test-ui - Specific test:
just test TARGET(e.g.just test AssetsTests) - Run all tests:
just test-all - Tests use iPhone 16 simulator by default
- To run tests, always use the
justcommands above, not directxcrun swift testcommands
- Use simple, descriptive method names:
showManageTokennottestShowManageToken_whenAssetIsEnabled_returnsFalse - Keep tests extremely concise - 2-3 assertions max per test
- Test only the essential behavior, nothing extra
- Always use existing TestKit mocks instead of creating custom mock services
- Create mock extensions in TestKit packages when they don't exist, not in test files
- Use clean mock syntax:
AssetSceneViewModel.mock(.mock(metadata: .mock(isEnabled: true))) - If a struct/class doesn't have a
.mock()method, create one in the appropriate TestKit
struct AssetSceneViewModelTests {
@Test
func showManageToken() {
#expect(AssetSceneViewModel.mock(.mock(metadata: .mock(isEnabled: true))).showManageToken == false)
#expect(AssetSceneViewModel.mock(.mock(metadata: .mock(isEnabled: false))).showManageToken == true)
}
@Test
func allBannersActive() {
let banners = [Banner.mock()]
let model = AssetSceneViewModel.mock(.mock(metadata: .mock(isActive: true)), banners: banners)
#expect(model.allBanners.count == 1)
#expect(model.allBanners == banners)
}
}// In Packages/Primitives/TestKit/Banner+PrimitivesTestKit.swift
public extension Banner {
static func mock(
event: BannerEvent = .stake,
wallet: Wallet = .mock()
) -> Banner {
Banner(
wallet: wallet,
event: event
)
}
}- Use existing service mocks:
WalletsService.mock(),AssetsService.mock(), etc. - Use shorthand syntax:
.mock()instead ofWalletsService.mock() - Use
.constant(nil)for bindings instead of creating custom ones - Follow the pattern of existing TestKit services like
BannerSetupService.mock()
- Short, simple tests: Keep inline with direct assertions
#expect(CollectibleViewModel.mock(assetData: .mock(asset: .mock(tokenId: "12345"))).tokenIdValue == "12345")
- Long lines: Separate model creation from comparison for better readability
let shortModel = CollectibleViewModel.mock(assetData: .mock(asset: .mock(tokenId: "123"))) let longModel = CollectibleViewModel.mock(assetData: .mock(asset: .mock(tokenId: "1234567890123456789"))) #expect(shortModel.tokenIdText == "#123") #expect(longModel.tokenIdText == "1234567890123456789")
- Complex mock setups: Use multiline formatting with proper indentation
#expect(CollectibleViewModel.mock(assetData: .mock( collection: .mock(contractAddress: "0x123"), asset: .mock(tokenId: "456") )).contractText == "0x123")
- Avoid unnecessary variables for simple cases - only use when readability is compromised
- Skip trivial tests: Don't write tests for simple scenarios like empty collections or obvious behaviors unless they test critical business logic
- Located in
core/directory - Shared between iOS and Android applications
- Provides blockchain-specific operations and cryptographic functions
- IMPORTANT: When working with the Rust core, read
core/AGENTS.mdfor detailed guidance on core architecture, development workflow, and coding standards
- Generated via
just generate-stone - Compiled into
GemstoneFFI.xcframework - Integrated through
GemstoneSwift package
- IMPORTANT: Files marked with "Generated by typeshare" comments are auto-generated and will be overwritten
- Notable generated files include:
Packages/Primitives/Sources/WalletConnector.swift- Other model files generated from Rust core
- Never modify generated files directly - add extensions in separate files
- Place extensions in the appropriate
Extensions/directory following the naming pattern:TypeName+Primitives.swift
CRITICAL REQUIREMENT: Always verify the project builds successfully before claiming work is complete.
- ALWAYS run
just buildbefore stating that tasks are finished - Use xcodebuild if needed: If
just buildfails or you need to build directly, use xcodebuild (see "Build Command Guidelines" in Debugging section) - Fix all build errors before marking tasks as completed
- Test core changes with
just generate-stonewhen modifying Rust core - Verify iOS integration after any core modifications
- If build fails, identify and fix all errors before proceeding
- Never claim "migration complete" or "tasks done" without a successful build
- Follow existing SwiftUI and Swift concurrency patterns
- Use
@Observablefor ViewModels instead ofObservableObject - Prefer async/await over Combine for new code
- Use protocol-based design for services
- Avoid adding explanatory comments in tests - test code should be self-documenting
- Never add comments for obvious or simple code operations (e.g., "Check if column exists", "Add column to table", "Verify tables were created")
- When extending types to conform to protocols, use explicit protocol conformance syntax:
// Good - explicit protocol conformance extension MyService: ServiceProtocol { func performAction() { } } // Bad - plain extension with comment // MARK: - ServiceProtocol extension MyService { func performAction() { } }
- Spacing Guidelines: Always use
Spacingconstants from theStylepackage instead of hardcoded numeric values:// Good - using Style constants VStack(spacing: Spacing.small) { ... } .padding(.vertical, Spacing.extraSmall) // Bad - hardcoded values VStack(spacing: 2) { ... } .padding(.vertical, 4)
- One Type Per File: Each service, type, view model, actor, or component must have its own separate file in the appropriate folder. Never inline multiple types in a single file
- Use
// MARK: - Actionsto separate action methods in ViewModels
- YAGNI (You Aren't Gonna Need It): Don't add functionality until it's actually needed
- No Dead Code: Remove unused methods, properties, and extensions
- Single Responsibility: Each class/struct should have one clear purpose
- Avoid Cargo Cult Programming: Don't copy patterns from other files without understanding their necessity
- Code Should Be Self-Documenting: Use clear, descriptive names instead of comments when possible
- Keep It Simple: Avoid over-engineering solutions. A single method is often better than multiple overloaded methods when the use case is straightforward. Don't create unnecessary abstractions or method variations unless they provide clear value
- Features should not directly depend on each other
- Shared functionality goes in Packages/
- Services handle cross-feature business logic
- Create new feature module in
Features/ - Follow the established directory structure
- Add navigation support in
Navigation/ - Register with
AppResolverif needed - Include comprehensive tests
- Verify All Code Is Used: Every method, property, and extension should be called/referenced somewhere
- Check for Patterns: Don't blindly copy patterns from existing code without understanding their purpose
- Minimize API Surface: Only make public what needs to be public
- Test-Driven Implementation: Write tests that verify actual usage, not just coverage
This project follows a GitFlow-like branching strategy:
main: Represents the latest production release.develop: The primary branch for ongoing development. All feature branches are merged intodevelop.feature/...: Branches for new features, branched fromdevelop.release/...: Branches for preparing a new production release.hotfix/...: Branches for critical production fixes.
The project uses semantic versioning (Major.Minor.Patch). The version can be updated using the just bump-version command, which leverages the scripts/bump-version.sh script.
- Before committing, ensure your changes are compliant with the project's style by running formatters and linters if available.
- Write clear, concise commit messages that explain the "why" behind the change.
- Xcode: Latest version required
- macOS: Apple Silicon Mac recommended (Intel Macs need additional setup)
- iOS: Minimum deployment target defined in project settings
- Swift: Uses latest Swift features including async/await and @Observable
- Managed through Lokalise platform
- Generated files in
Packages/Localization/ - Update with
just localize - Supports 20+ languages
- Keystore operations handled by dedicated
Keystorepackage - Biometric authentication supported
- Secure preferences stored in Keychain
- Rust core provides memory-safe cryptographic operations