Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
327 changes: 18 additions & 309 deletions AGENTS.md

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions skills/architecture.md
Original file line number Diff line number Diff line change
@@ -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<C: Client> ChainBalance for HyperCoreClient<C> {
async fn get_balance_coin(&self, address: &str) -> Result<CoinBalance, Box<dyn Error + Send + Sync>> {
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<f64>) -> Result<Vec<DelegationValidator>, Box<dyn Error + Send + Sync>> {
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<DatabaseClient>,
}

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<Asset, Box<dyn Error + Send + Sync>> {
self.database.assets().get_asset(id)
}

pub fn get_assets_by_device_id(&mut self, device_id: &str) -> Result<Vec<Asset>, Box<dyn Error + Send + Sync>> {
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
101 changes: 101 additions & 0 deletions skills/code-style.md
Original file line number Diff line number Diff line change
@@ -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 <file1> <file2> ...`
- 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
57 changes: 57 additions & 0 deletions skills/common-issues.md
Original file line number Diff line number Diff line change
@@ -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<AssetRow> {
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.
102 changes: 102 additions & 0 deletions skills/defensive-programming.md
Original file line number Diff line number Diff line change
@@ -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<u64, Error> {
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;
```
Loading
Loading