diff --git a/AGENTS.md b/AGENTS.md index 16a3fb3e7..dfba85486 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,322 +2,24 @@ Guidance for AI assistants (Claude Code, Gemini, Codex, etc.) collaborating on this repository. -## Purpose & Audience +## Skills -This document orients coding agents to the repo structure, development workflow, coding standards, and core architectural patterns used across Gem Wallet Core. +**All skills are mandatory reading** before making changes. -## Project Overview - -Gem Wallet Core is a Rust-based cryptocurrency wallet backend engine supporting 35+ blockchain networks. It is a Cargo workspace with 50+ crates covering transaction processing, asset management, DeFi integrations, swap operations, and cross-platform mobile support. - -## Repository Layout - -### Applications (`apps/`) -- **API Server** (`apps/api/`): REST API with WebSocket price streaming -- **Daemon** (`apps/daemon/`): Background services for asset updates, push notifications, transaction indexing -- **Dynode** (`apps/dynode/`): Dynamic blockchain node proxy with caching, monitoring, and metrics - -### Cross-Platform Library (`gemstone/`) -Shared Rust library compiled to iOS Swift Package and Android AAR using UniFFI bindings. Contains blockchain RPC clients, swap integrations, payment URI decoding, and message signing. Uses the separate `swapper` and `signer` crates for swap and signing operations. -- Uses `swapper` crate via `gemstone::gem_swapper` module for on-device swap integrations -- Uses `signer` crate for cryptographic signing operations across multiple blockchain types -- **Always use UniFFI remote types for external models** - See [UniFFI Remote and External Types](https://mozilla.github.io/uniffi-rs/latest/types/remote_ext_types.html) for proper integration patterns - -### Blockchain Support -Individual `gem_*` crates for each blockchain with unified RPC client patterns: -- **Bitcoin family** (`gem_bitcoin`): Bitcoin, Bitcoin Cash, Litecoin, Dogecoin -- **EVM chains** (`gem_evm`, `gem_bsc`): Ethereum, Polygon, Arbitrum, Optimism, Base, zkSync, Linea, BSC -- **Alternative L1s**: Solana (`gem_solana`), Sui (`gem_sui`), TON (`gem_ton`), Aptos (`gem_aptos`), NEAR (`gem_near`), Stellar (`gem_stellar`), Algorand (`gem_algorand`), Tron (`gem_tron`), XRP (`gem_xrp`), Cardano (`gem_cardano`), Polkadot (`gem_polkadot`) -- **Cosmos ecosystem** (`gem_cosmos`): Cosmos Hub, Osmosis, Celestia, Injective, Sei, Noble - -### Utility Binaries (`bin/`) -- **uniffi-bindgen** (`bin/uniffi-bindgen/`): UniFFI bindings generator for iOS and Android -- **generate** (`bin/generate/`): Code generation utilities -- **gas-bench** (`bin/gas-bench/`): Gas benchmarking tool for blockchain operations -- **img-downloader** (`bin/img-downloader/`): Image asset downloader utility - -### Core Services & Infrastructure Crates - -#### Blockchain Infrastructure -- `gem_client/`: Client trait abstraction used across services; implementations: `ReqwestClient` (backend) and `AlienProvider` (mobile) -- `gem_jsonrpc/`: Internal JSON-RPC client library (replaces external alloy dependencies) -- `gem_hash/`: Hashing utilities for blockchain operations -- `chain_primitives/`: Primitive types specific to blockchain operations -- `chain_traits/`: Common traits for blockchain implementations - -#### Cross-Chain Operations -- `swapper/`: Standalone swap/exchange integration crate supporting DEX and CEX swaps across multiple chains -- `signer/`: Cryptographic signing operations for transactions across multiple blockchain types - -#### Data & Storage -- `primitives/`: Central types and models shared across the system -- `storage/`: Database models, migrations, and data access layer using Diesel ORM -- `cacher/`: Caching layer for improved performance - -#### Pricing & Market Data -- `pricer/`: Asset pricing aggregation and management -- `prices_dex/`: DEX-specific price feeds and calculations -- `coingecko/`: CoinGecko API integration for market data - -#### NFT & Digital Assets -- `nft/`: NFT data models and business logic -- `nft_client/`: NFT marketplace API clients -- `nft_provider/`: NFT data provider integrations (OpenSea, Magic Eden, NFTScan) - -#### Integrations & Services -- `fiat/`: Fiat on-ramp/off-ramp providers (MoonPay, Transak, Mercuryo, Banxa) -- `name_resolver/`: Blockchain naming service integrations (ENS, SNS, etc.) -- `security_provider/`: Security and fraud detection provider integrations -- `api_connector/`: Backend API connector utilities -- `gem_hypercore/`: Perpetuals (perps) trading support via Hyperliquid integration - -#### Utilities & Support -- `localizer/`: i18n support for 20+ languages using Fluent -- `serde_serializers/`: Custom Serde serializers/deserializers used across crates -- `number_formatter/`: Number and currency formatting utilities -- `job_runner/`: Background job execution framework -- `search_index/`: Search indexing and query capabilities -- `streamer/`: Real-time data streaming utilities -- `tracing/`: Logging and tracing infrastructure -- `settings/`: Configuration management -- `settings_chain/`: Chain-specific configuration settings - -## Technology Stack - -- Framework: Rust workspace with Rocket web framework -- Database: PostgreSQL (primary), Redis (caching) -- Message Queue: RabbitMQ with Lapin -- RPC: Custom `gem_jsonrpc` client library for blockchain interactions -- Mobile: UniFFI for iOS/Android bindings -- Serialization: Serde with custom serializers -- Async: Tokio runtime -- Testing: Built-in Rust testing with integration tests - -## Development Workflow - -All commands use the `just` task runner. Run from the workspace root unless specified. - -### Build -- `just build`: Build the workspace -- `just build-gemstone`: Build cross-platform library -- `just gemstone build-ios`: Build iOS Swift Package (run in `gemstone/`) -- `just gemstone build-android`: Build Android AAR (run in `gemstone/`) - -### Test -- `just test-workspace`: Run all workspace tests -- `just test-all`: Run all tests including integration -- `just test `: Test a specific crate -- `just gemstone test-ios`: Run iOS integration tests (run in `gemstone/`) -- `cargo test --test integration_test --package --features `: Run integration tests manually - -### Code Quality -- `just format`: Format all code -- `just lint`: Run clippy with warnings as errors -- `just fix`: Auto-fix clippy issues -- `just unused`: Find unused dependencies with cargo-machete - -### Database -- `just migrate`: Run Diesel migrations -- `just setup-services`: Start Docker services (PostgreSQL, Redis, Meilisearch, RabbitMQ) - -### Mobile -- `just gemstone install-ios-targets`: Install iOS Rust targets (run in `gemstone/`) -- `just gemstone install-android-targets`: Install Android Rust targets and `cargo-ndk` (run in `gemstone/`) -- Note: Mobile builds require UniFFI bindings generation and platform-specific compilation - -### Generating Bindings (After Core Code Changes) -**IMPORTANT**: When you modify code in `gemstone/`, `swapper/`, `signer/`, or any crate that affects the mobile API, you MUST regenerate the platform bindings. - -#### Swift Bindings (iOS) -- `just gemstone bindgen-swift`: Generate Swift bindings only (run in `gemstone/`) -- `just gemstone build-ios`: Full iOS build including Swift binding generation (run in `gemstone/`) -- Generated files location: `gemstone/generated/swift/` and copied to `gemstone/target/spm/` - -#### Kotlin Bindings (Android) -- `just gemstone bindgen-kotlin`: Generate Kotlin bindings only (run in `gemstone/`) -- `just gemstone build-android`: Full Android build including Kotlin binding generation (run in `gemstone/`) -- Generated files location: `gemstone/generated/kotlin/` and copied to `gemstone/android/gemstone/src/main/java/uniffi/` - -#### When to Regenerate Bindings -1. After adding/modifying public functions in `gemstone/src/lib.rs` -2. After changing any UniFFI-exposed types or interfaces -3. After modifying `swapper/` or `signer/` crates that are exposed via gemstone -4. Before committing changes that affect the mobile API surface -5. When UniFFI schema or configuration changes - -### Utilities -- `just localize`: Update localization files -- `just generate-ts-primitives`: Generate TypeScript types from Rust -- `just outdated`: Check for outdated dependencies - -## Coding Standards - -Follow the existing code style patterns unless explicitly asked to change - -### Code Formatting -- Line length: 160 characters maximum (configured in `rustfmt.toml`) -- Indentation: 4 spaces (Rust standard) -- Imports: Automatically reordered with rustfmt -- Only format files you modified: `rustfmt --edition 2024 ...` (avoid `just format` which formats entire workspace) -- Formatter enforces consistent style across all crates/workspace - -### Commit Messages -- Write descriptive messages following conventional commit format - -### Naming and Conventions -- Files/modules: `snake_case` (e.g., `asset_id.rs`, `chain_address.rs`) -- Crates: Prefixed naming (`gem_*` for blockchains, `security_*` for security) -- Functions/variables: `snake_case` -- Structs/enums: `PascalCase` -- Constants: `SCREAMING_SNAKE_CASE` -- Helper names: inside a module stick to concise names that rely on scope rather than repeating crate/module prefixes (e.g., prefer `is_spot_swap` over `is_hypercore_spot_swap` in `core_signer.rs`). -- Don't use `util`, `utils`, `normalize`, or any other similar names for modules or functions. -- Avoid using `matches!` for pattern matching as much as possible, it's easy to missing a case later. -- Avoid type suffixes like `_str`, `_int`, `_vec` in variable names; Rust's type system makes them redundant. -- Don't add docstrings, comments, type annotations, or inline code comments unless explicitly asked to (including in mod.rs files). - -### Imports -1. Standard library imports first -2. External crate imports -3. Local crate imports -4. Module re-exports with `pub use` - -IMPORTANT: Always import models and types at the top of the file. Never use inline imports inside functions (e.g., `use crate::models::SomeType` inside a function). Never use full paths inline (e.g., `storage::DatabaseClient::new()`), always import types first. Declare all imports in the file header. - -### Error Handling -- Prefer plain `Error` types over `thiserror` macros -- Implement `From` traits for error conversion -- Use consistent `Result` return types -- Propagate errors with the `?` operator -- Add smart `From` conversions (e.g., `From for SignerError`) so callers can prefer `?` over manual `map_err`. -- Use constructor methods on error types (e.g., `SignerError::invalid_input("msg")`) instead of verbose enum construction (e.g., `SignerError::InvalidInput("msg".into())`) - -### JSON Parameter Extraction -- Use `primitives::ValueAccess` trait methods for `serde_json::Value` access instead of manual `.get().ok_or()` chains -- Available methods: `get_value(key)` → `&Value`, `at(index)` → `&Value`, `string()` → `&str` -- Chain methods for compound access: `params.get_value("transactions")?.at(0)?.string()?` -- Add accessor methods on parent types (e.g., `TransactionLoadInput::get_data_extra()`) to avoid pattern-matching boilerplate at call sites - -### Database Patterns -- Separate database models from domain primitives -- Use `as_primitive()` methods for conversion -- Diesel ORM with PostgreSQL backend -- Support transactions and upserts - -### Async Patterns -- Tokio runtime throughout -- Async client structs returning `Result` -- Use `Arc>` for shared async state - -## Architecture & Patterns - -### Key Development Patterns -- One crate per blockchain using unified RPC client patterns -- UniFFI bindings require careful Rust API design for mobile compatibility -- Use `BigDecimal` for financial precision -- Use async/await with Tokio across services -- Database models use Diesel ORM with automatic migrations -- Consider cross-platform performance constraints for mobile -- Shared U256 conversions: prefer `u256_to_biguint` and `biguint_to_u256` from `crates/gem_evm/src/u256.rs` for Alloy `U256` <-> `BigUint` conversions. - -### Code Organization -- **Modular structure**: Break down long files into smaller, focused modules by logical responsibility -- **Avoid duplication**: Before writing new code, search for existing implementations in the codebase; reuse existing code or crates -- **Shared crates**: If functionality could be reused, create a shared crate rather than duplicating logic -- **Bird's eye view**: Step back and look at the overall structure; identify opportunities to simplify and consolidate -- **Avoid `mut`**: Prefer immutable bindings; use `mut` only when truly necessary -- **No `#[allow(dead_code)]`**: Remove dead code instead of suppressing warnings -- **No unused fields**: Remove unused fields from structs/models; don't keep fields "for future use" -- **Constants for magic numbers**: Extract magic numbers into named constants with clear meaning -- **Minimum interface**: Don't expose unnecessary functions; if client only needs one function, don't add multiple variants -- **Use uniffi::remote**: For UniFFI wrapper types around external models, use `#[uniffi::remote]` instead of creating duplicate structs with `From` implementations: - ```rust - use primitives::AuthNonce; - pub type GemAuthNonce = AuthNonce; - #[uniffi::remote(Record)] - pub struct GemAuthNonce { pub nonce: String, pub timestamp: u32 } - ``` - -### Repository Pattern - -Services access repositories through direct methods on `DatabaseClient`. This pattern: -- Separates data access and business logic -- Assigns each repository a specific domain (assets, devices, etc.) -- Implements all repository traits directly on `DatabaseClient` -- Returns primitive types from repository methods, not database models -- Simplifies the API via direct method calls - -Example: -```rust -pub struct AssetsClient { - database: Box, -} - -impl AssetsClient { - pub fn new(database_url: &str) -> Self { - let database = Box::new(DatabaseClient::new(database_url)); - Self { database } - } - - pub fn get_asset(&mut self, id: &str) -> Result> { - self.database.assets().get_asset(id) - } - - pub fn get_assets_by_device_id(&mut self, device_id: &str) -> Result, Box> { - let subscriptions = self.database.subscriptions().get_subscriptions_by_device_id(device_id)?; - // ... process subscriptions - self.database.assets().get_assets(asset_ids) - } -} -``` - -Direct repository access methods available on `DatabaseClient` include: -- `assets()` - Asset operations -- `devices()` - Device operations -- `subscriptions()` - Subscription operations -- `prices()` - Price operations -- `transactions()` - Transaction operations -- And more... - -### RPC Client Patterns -- Use `gem_jsonrpc::JsonRpcClient` for blockchain RPC interactions -- **Use `primitives::hex`** for hex encoding/decoding (not `alloy_primitives::hex`) -- RPC calls expect hex strings directly; avoid double encoding -- Use `JsonRpcClient::batch_call()` for batch operations -- Propagate errors via `JsonRpcError` - -### Blockchain Provider Patterns -- Each blockchain crate has a `provider/` directory with trait implementations -- Provider methods should fetch raw data via RPC, then call mapper functions for conversion -- Place mapper functions in separate `*_mapper.rs` files for clean separation -- Example: `get_balance_coin()` calls `self.get_balance()` then `balances_mapper::map_coin_balance()` -- This pattern ensures consistent data transformation and testability across all blockchain implementations - -## Testing - -### Conventions -- Place integration tests in `tests/` directories -- Use `#[tokio::test]` for async tests -- Prefix test names descriptively with `test_` -- Use `Result<(), Box>` for test error handling -- Configure integration tests with `test = false` and appropriate `required-features` for manual execution -- Prefer real networks for RPC client tests (e.g., Ethereum mainnet) -- Test data management: For long JSON test data (>20 lines), store in `testdata/` and load with `include_str!()`; per-crate layout is typically `src/`, `tests/`, `testdata/` -- Never use `.expect()` in tests; use `.unwrap()` instead for brevity -- Mock methods: add `mock()` constructors in `testkit/` modules (e.g., `WalletConnectRequest::mock(...)`, `TransferDataExtra::mock()`) instead of building structs inline in tests -- Direct `assert_eq!`: derive `PartialEq` on test-relevant enums and use `assert_eq!` with constructed expected values instead of destructuring with `let ... else { panic! }` or `match ... { _ => panic! }` -- Test helpers: create concise constructor functions (e.g., `fn object(json: &str) -> EnumType`, `fn sign_message(chain, sign_type, data) -> Action`) for frequently constructed enum variants in test modules - -### Integration Testing -- Add integration tests for RPC functionality to verify real network compatibility -- Prefer recent blocks for batch operations (more reliable than historical blocks) -- Verify both successful calls and proper error propagation -- Use realistic contract addresses (e.g., USDC) for `eth_call` testing +- [Project Structure](skills/project-structure.md) — Repo layout, crates, and tech stack +- [Development Commands](skills/development-commands.md) — Build, test, lint, format, mobile +- [Code Style](skills/code-style.md) — Formatting, naming, imports, code organization +- [Error Handling](skills/error-handling.md) — Error types, propagation, JSON access +- [Architecture](skills/architecture.md) — Provider/mapper, repository, RPC, UniFFI patterns +- [Tests](skills/tests.md) — Test conventions, mocks, integration tests +- [Defensive Programming](skills/defensive-programming.md) — Safety rules and exhaustive patterns +- [Common Issues](skills/common-issues.md) — Known anti-patterns and their fixes ## Task Completion -Before finishing a task, always: -1. **Review for simplification**: Take an overall look at the code you touched and identify opportunities to simplify (reduce duplication, extract helpers, consolidate modules, remove dead code) -2. **Run tests**: Ensure all relevant tests pass -3. **Run clippy**: `cargo clippy -p -- -D warnings` -4. **Format only touched files**: `rustfmt --edition 2024 ` +Before finishing a task: +1. **Review for simplification** — reduce duplication, extract helpers, consolidate modules, remove dead code +2. **Keep changes minimal** — code must be concise and focused; reviewers cannot realistically review thousands of lines per PR, so only include what is necessary for the task +3. **Run tests**: `just test ` +4. **Run clippy**: `cargo clippy -p -- -D warnings` +5. **Format only touched files**: `rustfmt --edition 2024 ` diff --git a/skills/architecture.md b/skills/architecture.md new file mode 100644 index 000000000..79e3fe5e6 --- /dev/null +++ b/skills/architecture.md @@ -0,0 +1,102 @@ +# Architecture + +## Key Principles + +- One crate per blockchain using unified RPC client patterns +- UniFFI bindings require careful Rust API design for mobile compatibility +- Use `BigDecimal` for financial precision +- Use async/await with Tokio across services +- Database models use Diesel ORM with automatic migrations +- Consider cross-platform performance constraints for mobile + +## Provider/Mapper Pattern + +Each blockchain crate has a `provider/` directory with trait implementations. Provider methods fetch raw data via RPC, then call mapper functions for conversion. Place mapper functions in separate `*_mapper.rs` files. + +```rust +// good — provider delegates to mapper (crates/gem_hypercore/src/provider/balances.rs) +use super::balances_mapper::{map_balance_coin, map_balance_staking, map_balance_tokens}; + +#[async_trait] +impl ChainBalance for HyperCoreClient { + async fn get_balance_coin(&self, address: &str) -> Result> { + let available = self.get_balance(address).await?; + Ok(map_balance_coin(available, self.chain)) + } +} +``` + +```rust +// good — provider/staking.rs calls staking_mapper +async fn get_staking_validators(&self, apy: Option) -> Result, Box> { + let validators = self.get_validators().await?; + Ok(staking_mapper::map_staking_validators(validators, self.chain, apy)) +} +``` + +This pattern ensures consistent data transformation and testability across all blockchain implementations. + +## Repository Pattern + +Services access repositories through direct methods on `DatabaseClient`: + +```rust +pub struct AssetsClient { + database: Box, +} + +impl AssetsClient { + pub fn new(database_url: &str) -> Self { + let database = Box::new(DatabaseClient::new(database_url)); + Self { database } + } + + pub fn get_asset(&mut self, id: &str) -> Result> { + self.database.assets().get_asset(id) + } + + pub fn get_assets_by_device_id(&mut self, device_id: &str) -> Result, Box> { + let subscriptions = self.database.subscriptions().get_subscriptions_by_device_id(device_id)?; + // ... process subscriptions + self.database.assets().get_assets(asset_ids) + } +} +``` + +Available repository accessors on `DatabaseClient`: +- `assets()` — Asset operations +- `devices()` — Device operations +- `subscriptions()` — Subscription operations +- `prices()` — Price operations +- `transactions()` — Transaction operations +- And more... + +Key properties: +- Separates data access and business logic +- Assigns each repository a specific domain +- Implements all repository traits directly on `DatabaseClient` +- Returns primitive types from repository methods, not database models + +## RPC Client Patterns + +- Use `gem_jsonrpc::JsonRpcClient` for blockchain RPC interactions +- **Use `primitives::hex`** for hex encoding/decoding (not `alloy_primitives::hex`) +- RPC calls expect hex strings directly; avoid double encoding +- Use `JsonRpcClient::batch_call()` for batch operations +- Propagate errors via `JsonRpcError` + +## UniFFI Patterns + +Use `#[uniffi::remote]` for wrapper types around external models instead of creating duplicate structs with `From` implementations: + +```rust +// good — uniffi::remote avoids duplication +use primitives::AuthNonce; +pub type GemAuthNonce = AuthNonce; +#[uniffi::remote(Record)] +pub struct GemAuthNonce { pub nonce: String, pub timestamp: u32 } +``` + +## Shared Utilities + +- **U256 conversions**: Prefer `u256_to_biguint` and `biguint_to_u256` from `crates/gem_evm/src/u256.rs` for Alloy `U256` <-> `BigUint` conversions diff --git a/skills/code-style.md b/skills/code-style.md new file mode 100644 index 000000000..a9b51bcfa --- /dev/null +++ b/skills/code-style.md @@ -0,0 +1,101 @@ +# Code Style + +Follow the existing code style patterns unless explicitly asked to change. + +## Formatting + +- Line length: 160 characters maximum (configured in `rustfmt.toml`) +- Indentation: 4 spaces (Rust standard) +- Imports: Automatically reordered with rustfmt +- Only format files you modified: `rustfmt --edition 2024 ...` +- Avoid `just format` which formats the entire workspace + +## Commit Messages + +Write descriptive messages following conventional commit format. + +## Naming + +- Files/modules: `snake_case` (e.g., `asset_id.rs`, `chain_address.rs`) +- Crates: Prefixed naming (`gem_*` for blockchains, `security_*` for security) +- Functions/variables: `snake_case` +- Structs/enums: `PascalCase` +- Constants: `SCREAMING_SNAKE_CASE` + +### Scope-appropriate names + +Inside a module, use concise names that rely on scope rather than repeating the crate/module prefix. + +```rust +// bad — redundant prefix inside gem_hypercore::core_signer module +fn is_hypercore_spot_swap(order: &Order) -> bool { ... } + +// good — scope already provides context +fn is_spot_swap(order: &Order) -> bool { ... } +``` + +### Forbidden names + +Don't use `util`, `utils`, `normalize`, or any similar names for modules or functions. + +### No type suffixes + +Avoid type suffixes like `_str`, `_int`, `_vec` in variable names; Rust's type system makes them redundant. + +```rust +// bad +let address_str = "0x1234"; +let balances_vec = vec![100, 200]; + +// good +let address = "0x1234"; +let balances = vec![100, 200]; +``` + +### No unsolicited documentation + +Don't add docstrings, comments, type annotations, or inline code comments unless explicitly asked to (including in `mod.rs` files). + +## Imports + +Order: +1. Standard library imports +2. External crate imports +3. Local crate imports +4. Module re-exports with `pub use` + +**IMPORTANT**: Always import models and types at the top of the file. Never use inline imports inside functions. Never use full paths inline — always import types first. + +```rust +// bad — inline import inside function body +fn process_data() { + use crate::models::SomeType; + let item = SomeType::new(); +} + +// bad — full path inline +fn process_data() { + let client = storage::DatabaseClient::new(url); +} + +// good — all imports at file header +use crate::models::SomeType; +use storage::DatabaseClient; + +fn process_data() { + let item = SomeType::new(); + let client = DatabaseClient::new(url); +} +``` + +## Code Organization + +- **Modular structure**: Break down long files into smaller, focused modules by logical responsibility +- **Avoid duplication**: Search for existing implementations before writing new code; reuse existing code or crates +- **Shared crates**: If functionality could be reused, create a shared crate rather than duplicating logic +- **Bird's eye view**: Step back and look at the overall structure; identify opportunities to simplify and consolidate +- **Avoid `mut`**: Prefer immutable bindings; use `mut` only when truly necessary +- **No `#[allow(dead_code)]`**: Remove dead code instead of suppressing warnings +- **No unused fields**: Remove unused fields from structs/models; don't keep fields "for future use" +- **Constants for magic numbers**: Extract magic numbers into named constants with clear meaning +- **Minimum interface**: Don't expose unnecessary functions; if client only needs one function, don't add multiple variants diff --git a/skills/common-issues.md b/skills/common-issues.md new file mode 100644 index 000000000..e2a5bfd84 --- /dev/null +++ b/skills/common-issues.md @@ -0,0 +1,57 @@ +# Common Issues + +Known anti-patterns found in the codebase and their fixes. + +## `alloy_primitives::hex` — Use `primitives::hex` + +Several files import `alloy_primitives::hex` directly. Always use `primitives::hex` for consistency. + +```rust +// bad — direct alloy import +use alloy_primitives::hex; +let bytes = hex::decode(input)?; + +// good — use the project's re-export +use primitives::hex; +let bytes = hex::decode(input)?; +``` + +Known occurrences: +- `crates/gem_hypercore/src/signer/core_signer.rs` +- `crates/gem_hypercore/src/core/hahser.rs` +- `crates/signer/src/eip712/hash_impl.rs` +- `crates/gem_rewards/src/transfer_provider/evm/provider.rs` + +## Misspelled File: `hahser.rs` + +`crates/gem_hypercore/src/core/hahser.rs` should be `hasher.rs`. Fix when touching this file. + +## Duplicate Constants + +Before defining a new constant, check `crates/primitives/src/asset_constants.rs` for existing definitions. Reuse rather than redefine. + +## Inline `use` in Diesel Query Functions + +Diesel DSL imports (e.g., `use crate::schema::assets::dsl::*`) inside query functions are the **one exception** to the no-inline-imports rule. This is idiomatic Diesel usage and prevents DSL name collisions at module scope. + +```rust +// acceptable — Diesel DSL exception +fn get_asset(conn: &mut PgConnection, id: &str) -> QueryResult { + use crate::schema::assets::dsl::*; + assets.filter(asset_id.eq(id)).first(conn) +} +``` + +## `println!` in Service Code + +Found in `apps/api/` and `apps/daemon/`. Replace with `tracing::info!`/`tracing::error!` — see [Defensive Programming](defensive-programming.md#no-println-in-production-code). + +## Technical Debt Markers + +The codebase has ~20 `TODO`/`FIXME` comments marking deferred work. Key areas: +- **Deprecated API endpoints** (`apps/api/src`): Old `/notifications`, `/wallets`, `/price_alerts` routes pending removal after client migration +- **Hardcoded fees**: Stellar transaction fee is hardcoded as `"1000"` string +- **Gas estimation**: Thorchain memo byte-length gas limits marked FIXME +- **Swap status**: Thorchain refunded transactions default to `Failed` status + +When working near these areas, consider resolving the TODO if scope permits. diff --git a/skills/defensive-programming.md b/skills/defensive-programming.md new file mode 100644 index 000000000..9ef5acd2b --- /dev/null +++ b/skills/defensive-programming.md @@ -0,0 +1,102 @@ +# Defensive Programming + +Safety rules to prevent bugs and maintain production reliability. + +## No `matches!` — Use Exhaustive `match` + +The `matches!` macro silently ignores new variants added later. Use exhaustive `match` with explicit arms. + +```rust +// bad — new variants silently return false +fn is_transfer(action: &Action) -> bool { + matches!(action, Action::Transfer { .. }) +} + +// good — compiler forces handling all variants +fn is_transfer(action: &Action) -> bool { + match action { + Action::Transfer { .. } => true, + Action::Swap { .. } | Action::Stake { .. } | Action::Sign { .. } => false, + } +} +``` + +## No `#[allow(dead_code)]` + +Remove dead code instead of suppressing warnings. Dead code increases maintenance burden and hides actual issues. + +```rust +// bad +#[allow(dead_code)] +fn old_calculation(x: u64) -> u64 { x * 2 } + +// good — delete it entirely +``` + +## No `todo!()` / `unimplemented!()` + +Implement the functionality or return an error. Panicking macros in production code cause crashes. + +```rust +// bad — panics at runtime +fn get_fee(chain: Chain) -> u64 { + todo!("implement fee calculation") +} + +// good — return error for unhandled cases +fn get_fee(chain: Chain) -> Result { + match chain { + Chain::Ethereum => Ok(21000), + Chain::Bitcoin => Ok(1000), + _ => Err(Error::UnsupportedChain(chain)), + } +} +``` + +## No `println!` in Production Code + +Use structured logging (`tracing` crate) instead of `println!` in service code. `println!` bypasses log levels, timestamps, and monitoring integrations. + +```rust +// bad — apps/daemon/src/worker/prices/charts_updater.rs +println!("update charts {}", coin_id.id.clone()); +println!("update charts error: {err}"); + +// bad — apps/api/src/main.rs +println!("api start service: {}", service.as_ref()); + +// good — structured logging with context +tracing::info!(coin_id = %coin_id.id, "updating charts"); +tracing::error!(%err, "charts update failed"); +tracing::info!(service = %service.as_ref(), "api service starting"); +``` + +## No `.unwrap()` / `.expect()` in Production Code + +Return `Result` instead. Panicking in production causes service crashes. + +```rust +// bad — panics on None/Err +let value = map.get("key").unwrap(); +let data = serde_json::from_str(input).expect("invalid json"); + +// good — propagate errors +let value = map.get("key").ok_or(Error::MissingKey("key"))?; +let data: MyStruct = serde_json::from_str(input)?; +``` + +Note: `.unwrap()` is fine in tests — see [Tests](tests.md). + +## Prefer Immutable Bindings + +Use `mut` only when truly necessary. Immutable bindings prevent accidental mutation. + +```rust +// bad — unnecessary mut +let mut result = calculate_fee(amount); +return result; + +// good +let result = calculate_fee(amount); +return result; +``` diff --git a/skills/development-commands.md b/skills/development-commands.md new file mode 100644 index 000000000..de4ce04e6 --- /dev/null +++ b/skills/development-commands.md @@ -0,0 +1,86 @@ +# Development Commands + +All commands use the `just` task runner. Run from the workspace root unless specified. + +## Build + +```sh +just build # Build the workspace +just build-gemstone # Build cross-platform library +just gemstone build-ios # Build iOS Swift Package (run in gemstone/) +just gemstone build-android # Build Android AAR (run in gemstone/) +``` + +## Test + +```sh +just test-workspace # Run all workspace tests +just test-all # Run all tests including integration +just test # Test a specific crate +just gemstone test-ios # Run iOS integration tests (run in gemstone/) +cargo test --test integration_test --package --features # Manual integration test +``` + +## Code Quality + +```sh +just format # Format all code (prefer per-file below) +just lint # Run clippy with warnings as errors +just fix # Auto-fix clippy issues +just unused # Find unused dependencies with cargo-machete +``` + +**Formatting and Linting** — only format files touched in the PR. Running `just format` reformats the entire workspace and inflates diffs, making review impractical. +```sh +rustfmt --edition 2024 ... +cargo clippy -p -- -D warnings +``` + +## Database + +```sh +just migrate # Run Diesel migrations +just setup-services # Start Docker services (PostgreSQL, Redis, Meilisearch, RabbitMQ) +``` + +## Mobile + +```sh +just gemstone install-ios-targets # Install iOS Rust targets (run in gemstone/) +just gemstone install-android-targets # Install Android Rust targets and cargo-ndk (run in gemstone/) +``` + +Note: Mobile builds require UniFFI bindings generation and platform-specific compilation. + +## Generating Bindings (After Core Code Changes) + +> **IMPORTANT**: When you modify code in `gemstone/`, `swapper/`, `signer/`, or any crate that affects the mobile API, you MUST regenerate the platform bindings. + +### Swift Bindings (iOS) +```sh +just gemstone bindgen-swift # Generate Swift bindings only (run in gemstone/) +just gemstone build-ios # Full iOS build including Swift binding generation (run in gemstone/) +``` +Generated files: `gemstone/generated/swift/` → copied to `gemstone/target/spm/` + +### Kotlin Bindings (Android) +```sh +just gemstone bindgen-kotlin # Generate Kotlin bindings only (run in gemstone/) +just gemstone build-android # Full Android build including Kotlin binding generation (run in gemstone/) +``` +Generated files: `gemstone/generated/kotlin/` → copied to `gemstone/android/gemstone/src/main/java/uniffi/` + +### When to Regenerate Bindings +1. After adding/modifying public functions in `gemstone/src/lib.rs` +2. After changing any UniFFI-exposed types or interfaces +3. After modifying `swapper/` or `signer/` crates that are exposed via gemstone +4. Before committing changes that affect the mobile API surface +5. When UniFFI schema or configuration changes + +## Utilities + +```sh +just localize # Update localization files +just generate-ts-primitives # Generate TypeScript types from Rust +just outdated # Check for outdated dependencies +``` diff --git a/skills/error-handling.md b/skills/error-handling.md new file mode 100644 index 000000000..fded376be --- /dev/null +++ b/skills/error-handling.md @@ -0,0 +1,117 @@ +# Error Handling + +## Principle + +Prefer plain `Error` enums with `From` impls over `thiserror` macros. Use the `?` operator for propagation. + +## Error Type Pattern + +Define plain error enums with `Display`, `Error`, and constructor methods: + +```rust +// good — crates/primitives/src/signer_error.rs +#[derive(Debug, Clone)] +pub enum SignerError { + InvalidInput(String), + SigningError(String), +} + +impl std::fmt::Display for SignerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SignerError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), + SignerError::SigningError(msg) => write!(f, "Signing error: {}", msg), + } + } +} + +impl std::error::Error for SignerError {} + +impl SignerError { + pub fn invalid_input(message: impl Into) -> Self { + Self::InvalidInput(message.into()) + } + + pub fn signing_error(message: impl Into) -> Self { + Self::SigningError(message.into()) + } +} +``` + +## Constructor Methods over Verbose Enums + +```rust +// bad — verbose enum construction +return Err(SignerError::InvalidInput("missing field".into())); + +// good — constructor method +return Err(SignerError::invalid_input("missing field")); +``` + +## From Impls for `?` Operator + +Add `From` conversions so callers can use `?` instead of manual `map_err`: + +```rust +// good — From impls enable ? operator +impl From for SignerError { + fn from(error: serde_json::Error) -> Self { + SignerError::InvalidInput(error.to_string()) + } +} + +impl From for SignerError { + fn from(error: HexError) -> Self { + SignerError::InvalidInput(error.to_string()) + } +} + +// Then callers can write: +let data: MyStruct = serde_json::from_str(input)?; // auto-converts error +``` + +```rust +// bad — manual map_err when From impl exists +let data = serde_json::from_str(input) + .map_err(|e| SignerError::InvalidInput(e.to_string()))?; + +// good — ? with From impl +let data: MyStruct = serde_json::from_str(input)?; +``` + +## JSON Parameter Extraction + +Use the `primitives::ValueAccess` trait instead of manual `.get().ok_or()` chains: + +```rust +// Trait provides: get_value(key), at(index), string() +use primitives::ValueAccess; + +// bad — manual extraction +let tx = params.get("transactions") + .ok_or("Missing transactions")? + .as_array() + .ok_or("Expected array")? + .get(0) + .ok_or("Missing first element")? + .as_str() + .ok_or("Expected string")?; + +// good — chained ValueAccess methods +let tx = params.get_value("transactions")?.at(0)?.string()?; +``` + +Add accessor methods on parent types to avoid pattern-matching boilerplate at call sites (e.g., `TransactionLoadInput::get_data_extra()`). + +## Database Patterns + +- Separate database models from domain primitives +- Use `as_primitive()` methods for conversion +- Diesel ORM with PostgreSQL backend +- Support transactions and upserts + +## Async Patterns + +- Tokio runtime throughout +- Async client structs returning `Result` +- Use `Arc>` for shared async state diff --git a/skills/project-structure.md b/skills/project-structure.md new file mode 100644 index 000000000..94086a815 --- /dev/null +++ b/skills/project-structure.md @@ -0,0 +1,98 @@ +# Project Structure + +Gem Wallet Core is a Rust-based cryptocurrency wallet backend engine supporting 35+ blockchain networks. It is a Cargo workspace with 50+ crates covering transaction processing, asset management, DeFi integrations, swap operations, and cross-platform mobile support. + +## Directory Tree + +``` +apps/ # Backend services (API, Daemon, Dynode) +gemstone/ # Cross-platform mobile library (UniFFI → iOS/Android) +crates/ # Shared libraries and blockchain implementations +bin/ # Utility binaries +skills/ # Agent guidance documents (this directory) +``` + +## Applications (`apps/`) + +- **API Server** (`apps/api/`): REST API with WebSocket price streaming +- **Daemon** (`apps/daemon/`): Background services for asset updates, push notifications, transaction indexing +- **Dynode** (`apps/dynode/`): Dynamic blockchain node proxy with caching, monitoring, and metrics + +## Cross-Platform Library (`gemstone/`) + +Shared Rust library compiled to iOS Swift Package and Android AAR using UniFFI bindings. Contains blockchain RPC clients, swap integrations, payment URI decoding, and message signing. +- Uses `swapper` crate via `gemstone::gem_swapper` module for on-device swap integrations +- Uses `signer` crate for cryptographic signing operations across multiple blockchain types +- **Always use UniFFI remote types for external models** — see [UniFFI Remote and External Types](https://mozilla.github.io/uniffi-rs/latest/types/remote_ext_types.html) + +## Blockchain Support + +Individual `gem_*` crates for each blockchain with unified RPC client patterns: +- **Bitcoin family** (`gem_bitcoin`): Bitcoin, Bitcoin Cash, Litecoin, Dogecoin +- **EVM chains** (`gem_evm`, `gem_bsc`): Ethereum, Polygon, Arbitrum, Optimism, Base, zkSync, Linea, BSC +- **Alternative L1s**: Solana (`gem_solana`), Sui (`gem_sui`), TON (`gem_ton`), Aptos (`gem_aptos`), NEAR (`gem_near`), Stellar (`gem_stellar`), Algorand (`gem_algorand`), Tron (`gem_tron`), XRP (`gem_xrp`), Cardano (`gem_cardano`), Polkadot (`gem_polkadot`) +- **Cosmos ecosystem** (`gem_cosmos`): Cosmos Hub, Osmosis, Celestia, Injective, Sei, Noble + +## Utility Binaries (`bin/`) + +- **uniffi-bindgen** (`bin/uniffi-bindgen/`): UniFFI bindings generator for iOS and Android +- **generate** (`bin/generate/`): Code generation utilities +- **gas-bench** (`bin/gas-bench/`): Gas benchmarking tool for blockchain operations +- **img-downloader** (`bin/img-downloader/`): Image asset downloader utility + +## Core Crates + +### Blockchain Infrastructure +- `gem_client/`: Client trait abstraction; implementations: `ReqwestClient` (backend) and `AlienProvider` (mobile) +- `gem_jsonrpc/`: Internal JSON-RPC client library (replaces external alloy dependencies) +- `gem_hash/`: Hashing utilities for blockchain operations +- `chain_primitives/`: Primitive types specific to blockchain operations +- `chain_traits/`: Common traits for blockchain implementations + +### Cross-Chain Operations +- `swapper/`: Standalone swap/exchange integration crate supporting DEX and CEX swaps across multiple chains +- `signer/`: Cryptographic signing operations for transactions across multiple blockchain types + +### Data & Storage +- `primitives/`: Central types and models shared across the system +- `storage/`: Database models, migrations, and data access layer using Diesel ORM +- `cacher/`: Caching layer for improved performance + +### Pricing & Market Data +- `pricer/`: Asset pricing aggregation and management +- `prices_dex/`: DEX-specific price feeds and calculations +- `coingecko/`: CoinGecko API integration for market data + +### NFT & Digital Assets +- `nft/`: NFT data models and business logic +- `nft_client/`: NFT marketplace API clients +- `nft_provider/`: NFT data provider integrations (OpenSea, Magic Eden, NFTScan) + +### Integrations & Services +- `fiat/`: Fiat on-ramp/off-ramp providers (MoonPay, Transak, Mercuryo, Banxa) +- `name_resolver/`: Blockchain naming service integrations (ENS, SNS, etc.) +- `security_provider/`: Security and fraud detection provider integrations +- `api_connector/`: Backend API connector utilities +- `gem_hypercore/`: Perpetuals (perps) trading support via Hyperliquid integration + +### Utilities & Support +- `localizer/`: i18n support for 20+ languages using Fluent +- `serde_serializers/`: Custom Serde serializers/deserializers used across crates +- `number_formatter/`: Number and currency formatting utilities +- `job_runner/`: Background job execution framework +- `search_index/`: Search indexing and query capabilities +- `streamer/`: Real-time data streaming utilities +- `tracing/`: Logging and tracing infrastructure +- `settings/`: Configuration management +- `settings_chain/`: Chain-specific configuration settings + +## Technology Stack + +- **Framework**: Rust workspace with Rocket web framework +- **Database**: PostgreSQL (primary), Redis (caching) +- **Message Queue**: RabbitMQ with Lapin +- **RPC**: Custom `gem_jsonrpc` client library for blockchain interactions +- **Mobile**: UniFFI for iOS/Android bindings +- **Serialization**: Serde with custom serializers +- **Async**: Tokio runtime +- **Testing**: Built-in Rust testing with integration tests diff --git a/skills/tests.md b/skills/tests.md new file mode 100644 index 000000000..42895d386 --- /dev/null +++ b/skills/tests.md @@ -0,0 +1,115 @@ +# Tests + +## Conventions + +- Place integration tests in `tests/` directories +- Use `#[tokio::test]` for async tests +- Prefix test names descriptively with `test_` +- Use `Result<(), Box>` for test error handling +- Configure integration tests with `test = false` and appropriate `required-features` for manual execution +- Prefer real networks for RPC client tests (e.g., Ethereum mainnet) + +## Use `.unwrap()`, not `.expect()` in Tests + +```rust +// bad — unnecessary message in tests +let client = reqwest::Client::builder() + .timeout(timeout) + .build() + .expect("failed to build reqwest client"); + +// good — concise, failure is obvious from test context +let client = reqwest::Client::builder() + .timeout(timeout) + .build() + .unwrap(); +``` + +## Test Data Management + +For long JSON test data (>20 lines), store in `testdata/` and load with `include_str!()`. Per-crate layout is typically `src/`, `tests/`, `testdata/`. + +```rust +// good — external test data +let response: ApiResponse = serde_json::from_str( + include_str!("../../testdata/balances_response.json") +).unwrap(); +``` + +## Mock Pattern with `testkit/` Modules + +Add `mock()` constructors in `testkit/` modules instead of building structs inline in tests: + +```rust +// good — crates/gem_hypercore/src/testkit.rs +impl AssetPositions { + pub fn mock() -> Self { + Self { + asset_positions: vec![], + margin_summary: MarginSummary { + account_value: "10000".to_string(), + total_ntl_pos: "5000".to_string(), + total_raw_usd: "5000".to_string(), + total_margin_used: "2000".to_string(), + }, + // ... + } + } +} + +// good — parameterized mock +impl OpenOrder { + pub fn mock(coin: &str, oid: u64, order_type: &str, trigger_px: f64, limit_px: Option) -> Self { + Self { + coin: coin.to_string(), + oid, + trigger_px: Some(trigger_px), + limit_px, + is_position_tpsl: true, + order_type: order_type.to_string(), + } + } +} +``` + +## Direct `assert_eq!` + +Derive `PartialEq` on test-relevant enums and use `assert_eq!` with constructed expected values: + +```rust +// bad — destructuring with panic +let result = parse_action(input); +let Action::SignMessage { chain, data, .. } = result else { + panic!("Expected SignMessage"); +}; +assert_eq!(chain, Chain::Ethereum); + +// good — direct comparison +assert_eq!(result, Action::SignMessage { + chain: Chain::Ethereum, + sign_type: SignDigestType::Eip191, + data: "hello".to_string(), +}); +``` + +## Test Helpers + +Create concise constructor functions for frequently constructed enum variants in test modules: + +```rust +// good — helper avoids repetitive boilerplate +fn sign_message(chain: Chain, sign_type: SignDigestType, data: &str) -> WalletConnectAction { + WalletConnectAction::SignMessage { + chain, + sign_type, + data: data.to_string(), + } +} +``` + +## Integration Testing + +- Add integration tests for RPC functionality to verify real network compatibility +- Prefer recent blocks for batch operations (more reliable than historical blocks) +- Verify both successful calls and proper error propagation +- Use realistic contract addresses (e.g., USDC) for `eth_call` testing