diff --git a/CLAUDE.md b/CLAUDE.md index 103df11..21438fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,62 +4,84 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands -### Build +### Solana Programs (Rust) ```bash -# Build a single program (produces .so in target/deploy/) -cargo build-sbf --manifest-path programs/descro/Cargo.toml -cargo build-sbf --manifest-path programs/descro_ext_resolvers/Cargo.toml - -# Build both (registry must be built first — descro depends on it) +# Build (registry must be built first — descro depends on it) cargo build-sbf --manifest-path programs/descro_ext_resolvers/Cargo.toml && \ cargo build-sbf --manifest-path programs/descro/Cargo.toml -``` -### Test - -Tests use **LiteSVM** (in-process Solana runtime, no validator needed). They load the compiled `.so` from `target/deploy/` via `include_bytes!`, so the relevant program(s) must be built before running tests. - -```bash -# Run all tests for a program +# Test (programs must be built first — tests load .so via include_bytes!) cargo test --manifest-path programs/descro/Cargo.toml cargo test --manifest-path programs/descro_ext_resolvers/Cargo.toml -# Run a single test by name +# Single test / specific file cargo test --manifest-path programs/descro/Cargo.toml -- test_create_escrow -cargo test --manifest-path programs/descro/Cargo.toml -- resolve_updates_stats_when_resolver_registered - -# Run tests in a specific file cargo test --manifest-path programs/descro/Cargo.toml --test test_resolve + +# Lint / Format +cargo clippy --manifest-path programs/descro/Cargo.toml +cargo fmt --manifest-path programs/descro/Cargo.toml ``` -### Lint / Format +### TypeScript SDK (`sdk/`) + +No build step — consumed as raw TypeScript via `transpilePackages` in the app. Install from repo root: ```bash -cargo clippy --manifest-path programs/descro/Cargo.toml -cargo clippy --manifest-path programs/descro_ext_resolvers/Cargo.toml -cargo fmt --manifest-path programs/descro/Cargo.toml +yarn install +``` + +### App (`app/`) + +```bash +# Dev server (Turbopack) +yarn workspace descro-app run dev + +# Production build (generates tamagui.generated.css then runs next build) +yarn workspace descro-app run build +``` + +## Repository Structure + +``` +programs/ +├── descro/ — Escrow Program +└── descro_ext_resolvers/ — Resolver Registry +sdk/ — @descro/sdk TypeScript client (workspace package) +├── src/ +│ ├── idl/ — Anchor-generated IDL JSON files +│ ├── escrow.ts — EscrowClient (instruction builders + fetchers) +│ ├── registry.ts — RegistryClient +│ ├── listener.ts — WebSocket subscriptions +│ ├── pda.ts — PDA derivation helpers + program IDs +│ ├── types.ts — TypeScript types matching on-chain state +│ └── index.ts — Re-exports + DescroSdk convenience class +app/ — Next.js + Tamagui playground (workspace package) +├── src/app/ — Next.js App Router pages + providers +├── src/components/ — UI components (EscrowDetail, ResolverPanel, …) +└── src/hooks/ — useEscrows, useEscrowDetail ``` ## Architecture +### Yarn Workspaces + +Root `package.json` declares `workspaces: ["sdk", "app"]`. The repo uses `nodeLinker: node-modules` (in `.yarnrc.yml`) because Next.js Turbopack does not support Yarn PnP. Run `yarn install` from the repo root. + +The app depends on the SDK via `"@descro/sdk": "workspace:*"` and lists it in `transpilePackages` in `next.config.ts` so Next.js compiles the TypeScript source directly (no SDK build step needed). + ### Two-Program Protocol -``` -programs/ -├── descro/ — Escrow Program (Phase 1) -└── descro_ext_resolvers/ — Resolver Registry (Phase 2) -``` - -`descro` depends on `descro_ext_resolvers` (with `features = ["cpi"]`). The registry does NOT depend on the escrow — this is intentional to avoid a circular dependency. Communication is one-way: escrow → registry via CPI. +`descro` depends on `descro_ext_resolvers` (with `features = ["cpi"]`). Communication is one-way: escrow → registry via CPI. The registry does not depend on the escrow. ### Escrow Program (`descro`) -**State:** A single `EscrowAccount` PDA holds all trade state. SOL is held separately in a `Vault` PDA (system-owned, data-free). The two-account split makes SOL transfers simpler: the vault is a pure lamport store. +**State:** `EscrowAccount` PDA holds trade state. SOL is held in a separate `Vault` PDA (system-owned, data-free). **PDA seeds:** -- Escrow: `["escrow", seller_pubkey, escrow_id_le_bytes]` — allows one seller to have multiple concurrent escrows -- Vault: `["vault", escrow_account_pubkey]` — uniquely derived from the escrow account +- Escrow: `["escrow", seller_pubkey, escrow_id_le_bytes]` +- Vault: `["vault", escrow_account_pubkey]` **Instruction flow:** ``` @@ -68,39 +90,25 @@ create_escrow (seller) → deposit (buyer) → complete (buyer) cancel (seller, only from AwaitingDeposit) ``` -**SOL transfers** use raw `invoke` / `invoke_signed` (not Anchor's CPI helpers) because Anchor 1.0.x changed `CpiContext::new` to take `Pubkey` instead of `AccountInfo`, which conflicts with how system_program transfers work in practice. +**SOL transfers** use raw `invoke` / `invoke_signed` (not Anchor's CPI helpers) because Anchor 1.0.x changed `CpiContext::new` to take `Pubkey` instead of `AccountInfo`. ### Resolver Registry (`descro_ext_resolvers`) -**State:** `ResolverEntry` PDA at `["resolver", authority_pubkey]`. Tracks name, type, fee, and on-chain resolution stats. +**State:** `ResolverEntry` PDA at `["resolver", authority_pubkey]`. Tracks name, type, fee, and resolution stats. -**CPI authorization for `update_stats`:** Only the escrow program may call `update_stats`. This is enforced via a `["escrow_authority"]` PDA that only the escrow program can derive and sign for (`invoke_signed`). The registry verifies this signer has `seeds::program = ESCROW_PROGRAM_ID` (hardcoded constant in `lib.rs`). +**CPI authorization:** Only the escrow program may call `update_stats`, enforced via an `["escrow_authority"]` PDA that only the escrow program can sign for. The registry verifies `seeds::program = ESCROW_PROGRAM_ID`. -**`update_stats` is optional from the escrow side:** In `resolve()`, the handler checks `resolver_entry.data_is_empty()` before attempting the CPI. Resolvers that are not registered in the registry still work — stats are simply not tracked. +**Optional CPI:** `resolve()` checks `resolver_entry.data_is_empty()` before calling the registry — unregistered resolvers still work. -### Anchor 1.0.x Notes +### Anchor / SDK Notes -- `CpiContext::new(program: Pubkey, accounts: T)` — first arg is `Pubkey`, not `AccountInfo` -- `AccountInfo<'info>` in `#[derive(Accounts)]` structs is deprecated; use `UncheckedAccount<'info>` with `/// CHECK:` doc comments -- `#[derive(InitSpace)]` with `#[max_len(N)]` on `String` fields for automatic space calculation -- The `cpi` Cargo feature (`no-entrypoint`) is required on the dependency when calling another Anchor program - -### Test Infrastructure - -Each program has a `tests/common/mod.rs` with shared helpers: -- `setup()` — creates LiteSVM, loads `.so`, airdrops to test keypairs -- PDA derivation helpers (`escrow_pda`, `vault_pda`, `resolver_entry_pda`, …) -- `send()` / `try_send()` — wraps instruction in a `VersionedTransaction` -- `ix_*` builders — construct typed `Instruction` structs via Anchor's `InstructionData` + `ToAccountMetas` -- `read_*` helpers — deserialise on-chain accounts with `AccountDeserialize::try_deserialize` - -The `test_resolve_with_registry.rs` test loads **both** `.so` files into one LiteSVM instance to test the full CPI flow end-to-end. +- **Anchor 1.0.x:** `CpiContext::new` takes `Pubkey` not `AccountInfo`; use `UncheckedAccount<'info>` with `/// CHECK:` instead of `AccountInfo<'info>` in account structs; `#[derive(InitSpace)]` + `#[max_len(N)]` for strings. +- **`@coral-xyz/anchor@0.32.x`:** Use `new Program(idl, provider)` — the IDL's `address` field provides the program ID. The 3-argument form `new Program(idl, programId, provider)` is broken in 0.32.x. +- **LiteSVM tests:** each `tests/common/mod.rs` has `setup()`, PDA helpers, `send()`/`try_send()`, `ix_*` builders, and `read_*` deserializers. `test_resolve_with_registry.rs` loads both `.so` files to test the full CPI flow. ### Program IDs | Program | ID | |---|---| | `descro` | `DjVR4EuYV6USMJFfsGZwhZ3y8rtWsmG8EvDY96GTqqi3` | -| `descro_ext_resolvers` | `Fg6PaFpoGXkYsidMpWTK6W2BeZ7FEfcYkg476zPFsLnS` *(placeholder — update after first deployment)* | - -The `descro_ext_resolvers` ID is hardcoded as `ESCROW_PROGRAM_ID` inside the registry program. After deploying the registry for the first time, update `declare_id!` in `programs/descro_ext_resolvers/src/lib.rs` **and** `ESCROW_PROGRAM_ID` in `programs/descro_ext_resolvers/src/lib.rs` to match. +| `descro_ext_resolvers` | `GwUPAKs3HHzCpj8uhet4NAnxk9GWNwfrYbpihu5DyFp` |