Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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
124 changes: 114 additions & 10 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,8 @@ docker-compose up -d
7. **paymaster-sponsoring** - Sponsoring logic and webhook handling
8. **paymaster-common** - Shared utilities, monitoring, and service management
9. **paymaster-cli** - Command-line interface for setup and management
10. **website** - Landing page with documentation links and useful resources
10. **paymaster-client** - Rust client library for the paymaster JSON-RPC API (builder pattern, build+sign+execute flow)
11. **website** - Landing page with documentation links and useful resources

### Key Services

Expand All @@ -123,14 +124,6 @@ The service uses environment variables and configuration files. Key configuratio
4. Service locks a relayer, executes transaction, then releases relayer
5. Relayer balances are monitored and rebalanced as needed

### Testing

Most crates include comprehensive test suites. Key testing utilities:
- Mock implementations for external dependencies
- Test transactions and accounts in `paymaster-starknet/testing`
- Integration tests for RPC endpoints
- Relayer lock testing with mock coordination layers

### Error Handling

The codebase uses `thiserror` for error handling with comprehensive error types:
Expand Down Expand Up @@ -159,6 +152,11 @@ The codebase uses `thiserror` for error handling with comprehensive error types:
- Comprehensive error handling for network issues
- Gas price monitoring and fee estimation

### Builder Pattern
- Builders must use the typestate pattern to enforce required fields at compile time
- Required parameters go in the constructor or in methods that transition state
- Optional parameters work on any state via a generic impl block

### Relayer Management
- Segregated locking to prevent race conditions
- Automatic rebalancing via AVNU swaps
Expand All @@ -167,4 +165,110 @@ The codebase uses `thiserror` for error handling with comprehensive error types:

### Language
- The whole codebase is strictly using English
- New/edited comments must be in English as well
- New/edited comments must be in English as well
-
- ## Code Style

Follows rustfmt config: max_width=170, chain_width=80, Unix newlines.

**Don't use section comments**: Avoid comments like `// ============` or `// --- Section ---` to delimit code sections. Use module structure and whitespace instead.

## Tools & Plugins

- **rust-analyzer-lsp**: Use this MCP plugin for Rust code analysis
- **plugin:context7:context7**: Use for up-to-date documentation on libraries

## Testing Guidelines

Most crates include comprehensive test suites. Key testing utilities:
- Mock implementations for external dependencies
- Test transactions and accounts in `paymaster-starknet/testing`
- Integration tests for RPC endpoints
- Relayer lock testing with mock coordination layers

### Test Patterns

- Unit tests: `#[cfg(test)] mod tests` at bottom of file
- Integration tests: Use testcontainers with `setup_mongo()`
- Naming: `should_<action>_when_<condition>`
- Structure: Given/When/Then comments
- Nested modules per function: `mod function_name { #[test] fn should_... }`

### Test Pragmatism

Write tests that provide real value. Avoid over-testing and redundant coverage:

- **Test each behavior once**: If a helper function has 2 behaviors (success/error), test those 2 cases in unit tests for that function
- **Don't re-test dependencies**: If function B calls function A, only test B's happy path with A succeeding. A's error cases are already covered by A's own tests
- **Avoid trivial tests**: Don't test obvious things like "constructor sets fields" or "getter returns value"
- **Focus on boundaries**: Test edge cases at the lowest level where they occur, not at every layer

Example - What NOT to do:
```rust
// parse_signature already has tests for empty input, invalid hex, etc.
// Don't re-test those cases in verify_signature_endpoint!

// ❌ BAD: Re-testing parse_signature errors through the endpoint
#[case::empty_signature(json!({ ..., "signature": [] }))] // Already tested in parse_signature
#[case::invalid_hex(json!({ ..., "signature": ["invalid"] }))] // Already tested

// ✅ GOOD: Test only verify_signature_endpoint's own logic
#[case::address_mismatch(...)] // Endpoint's own validation
#[case::nonce_not_found(...)] // Endpoint's own behavior
```

**Rule of thumb**: If a test failure would point you to a dependency's code rather than the function being tested, the test is redundant.

### rstest Parameterized Tests

Use `rstest` for parameterized tests, grouped by outcome:
- **Split by success/error**: Don't mix success and error cases in the same function
- **Split by HTTP status**: Group endpoint tests by expected status code (400, 401, etc.)
- **Payload in `#[case]`**: Put the full input data directly in the case attribute for visibility

Example - Unit tests split by success/error:
```rust
#[rstest]
#[case::valid_hex(vec!["0x123", "0x456"], 2)]
#[case::single_element(vec!["0x123"], 1)]
fn should_parse_valid_signature_when(#[case] input: Vec<&str>, #[case] expected_len: usize) {
let result = parse_signature(&input.into_iter().map(String::from).collect::<Vec<_>>());
assert_eq!(result.unwrap().len(), expected_len);
}

#[rstest]
#[case::empty(vec![])]
#[case::decimal_rejected(vec!["123", "456"])]
fn should_reject_invalid_signature_when(#[case] input: Vec<&str>) {
let result = parse_signature(&input.into_iter().map(String::from).collect::<Vec<_>>());
assert!(result.is_err());
}
```

Example - HTTP endpoint tests grouped by status code:
```rust
#[rstest]
#[case::invalid_wallet_address(json!({ "wallet_address": "invalid", ... }))]
#[case::invalid_message_address(json!({ "wallet_address": "0x...", "message": { "address": "invalid" }, ... }))]
#[actix_rt::test]
async fn should_return_400_when(#[case] payload: serde_json::Value) {
// ... test returning 400
}

#[rstest]
#[case::address_mismatch(json!({ ... }))]
#[actix_rt::test]
async fn should_return_401_when(#[case] payload: serde_json::Value) {
// ... test returning 401
}
```

**Note**: Tests requiring specific setup (database operations before request) should remain as individual functions.

## CLAUDE.md Maintenance

**REQUIRED**: Update this file when making significant architecture changes:
- Document major new features
- Update environment variables when adding new ones
- Keep the "Progress Tracker" section up to date
- Update API structure when adding new endpoints
35 changes: 25 additions & 10 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ members = [
"crates/paymaster-sponsoring",
"crates/paymaster-cli",
"crates/paymaster",
"crates/paymaster-client",
]

[workspace.package]
Expand Down Expand Up @@ -38,7 +39,7 @@ serde = "1.0.219"
serde_json = "1.0.139"
serde_with = "3.14.0"
simple_logger = "5.0.0"
starknet = { git = "https://github.com/xJonathanLEI/starknet-rs", tag = "starknet/v0.17.0" }
starknet = { git = "https://github.com/florian-bellotti/starknet-rs", branch = "bugfix/typed-data" }
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we put starknet-rs under AVNU instead ?

Copy link
Contributor Author

@florian-bellotti florian-bellotti Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's just temporary. We need to fix xJonathanLEI/starknet-rs instead. It won't be merged like this

testcontainers = "0.23.3"
thiserror = "2.0.11"
tokio = "1.43.0"
Expand Down
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,57 @@ const { transaction_hash } = result;

🔗 [Full Integration Guide available here](https://docs.out-of-gas.xyz/docs/dapp-integration)

### Rust Client

Add the dependency to your `Cargo.toml`:

```toml
[dependencies]
paymaster-client = { git = "https://github.com/avnu-labs/paymaster" }
```

```rust
use paymaster_client::{PaymasterClient, STRK_TOKEN};

#[tokio::main]
async fn main() -> Result<(), paymaster_client::Error> {
let client = PaymasterClient::builder("https://sepolia.paymaster.avnu.fi")
.api_key("YOUR_API_KEY")
.build()?;

// Sponsored transaction (gas paid by the paymaster)
let resp = client.transaction(your_account_address)
.call(your_call())
.sponsored()
.send(&your_wallet)
.await?;

println!("tx hash: {:#x}", resp.transaction_hash);

// Non-sponsored transaction (gas defaults to STRK)
let resp = client.transaction(your_account_address)
.call(your_call())
.send(&your_wallet)
.await?;

println!("tx hash: {:#x}", resp.transaction_hash);

// Two-step flow: inspect fees before signing
let prepared = client.transaction(your_account_address)
.call(your_call())
.gas_token(STRK_TOKEN)
.build()
.await?;

println!("Estimated fee: {:#x}", prepared.fee.estimated_fee_in_strk);
let resp = prepared.send(&your_wallet).await?;

println!("tx hash: {:#x}", resp.transaction_hash);

Ok(())
}
```

## 📖 Documentation

📚 [Full documentation available here](https://docs.out-of-gas.xyz)
Expand Down
19 changes: 19 additions & 0 deletions crates/paymaster-client/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "paymaster-client"
version = { workspace = true }
edition = { workspace = true }
repository = { workspace = true }

[dependencies]
paymaster-rpc = { path = "../paymaster-rpc" }
paymaster-starknet = { path = "../paymaster-starknet" }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true, features = ["arbitrary_precision", "raw_value"] }
starknet = { workspace = true }
thiserror = { workspace = true }
tokio = { workspace = true }
tracing = { workspace = true, features = ["attributes"] }

[dev-dependencies]
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
wiremock = { workspace = true }
Loading
Loading