Skip to content
Merged
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
144 changes: 40 additions & 104 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,119 +1,55 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Solana program (Pinocchio, `no_std`) for token delegations + pull-payment
subscriptions, with Codama-generated Rust/TS clients and a demo webapp.
Build/test recipes: `just --list`. ADRs: `docs/00*.md`.

## Required Versions
## Gotchas

- **Rust**: See `rust-toolchain.toml` (auto-installed by rustup)
- **Node.js**: See `.nvmrc` (use `nvm use` or `fnm use`)
- **pnpm**: See `package.json` `packageManager` field
**Wire format is not Anchor's.** Instruction discriminators are 1 byte, not
Anchor's 8. Events are the exception: emitted via self-CPI with Anchor's
`Sha256("anchor:event")[..8]` tag so indexers pick them up, followed by a
1-byte event discriminator. Any tool that assumes Anchor layout (IDL
converters, fuzzers, decoders) will mis-decode instructions; use raw
instruction calls or `clients/rust`.

## Build Commands
**The IDL is a build-script side effect.** `program/build.rs` writes
`idl/subscriptions.json` only when `GENERATE_IDL=1` is set (`just
generate-idl` runs `cargo check` with it). A plain `cargo build` leaves a
stale IDL. `#[codama(...)]` attributes silently drift from the Rust types;
`just check-generated` is the only thing that catches it, and CI fails on it.

```bash
# Full build (program .so → IDL → clients → TS client dist)
just build
**Account versioning runs on raw bytes.** `check_and_update_version` must be
called before any typed struct load: a lazy migration can change the layout
under you. It also validates the kind byte first so a migration never mutates
a wrong-kind account.

# Individual steps
just generate-idl # Generate IDL via Codama (GENERATE_IDL=1 cargo check, build.rs writes it)
just generate-clients # Generate TypeScript + Rust clients from IDL
just build-program # Build .so binary only (cargo build-sbf)
just build-client # Build TypeScript client (tsup)
**TS integration tests need two validator passes.** `just test-client` runs a
fork pass, then restarts surfpool with `--offline` for `*.offline.test.ts`.
Reason: surfpool forwards `getProgramAccounts` to public mainnet-beta, which
rejects it (-32603). Anything scanning program accounts belongs in an
`.offline.test.ts` file.

# Formatting and linting
just fmt # cargo fmt + prettier
just check # fmt-check + lint-check
**surfpool-sdk's in-process VM cannot execute our `.so`.** For TS-side program
tests use node-litesvm, not the SDK VM.

# Testing
just test # All tests (program + client)
just unit-test # Rust unit tests
just integration-test # Rust LiteSVM integration tests
just test-program # backwards-compatible alias for unit-test
just test-client # Vitest against Surfpool
just test-and-benchmark # CU report → cu_report.md
**LiteSVM clock reads are not stable.** `current_ts()` is read more than once
per instruction, so period-boundary tests with tiny periods flake. Pin time
with the absolute `set_clock` helper instead of relative advances.

# Deployment
just deploy-idl-devnet # Write IDL on-chain via program-metadata
just deploy-idl-mainnet
just verify-mainnet # solana-verify against repo
**No BigInt `toJSON` patch in the webapp.** A global patch was added once and
corrupted RPC request serialization (silent, took days to find). Serialize
bigints at the call site.

# Dependencies
pnpm install # all workspaces
```
**`declare_id!` in `program/src/lib.rs` is parsed by `sed`** in the justfile
and scripts. Keep it a single literal line.

## Architecture

Solana program using **Pinocchio** (lightweight `no_std` framework) with **Codama** for IDL-driven client generation.

### Code Flow

```
program/src/lib.rs (declares ID) + entrypoint.rs (dispatches via SubscriptionsInstruction enum)
program/src/instructions/*.rs (instruction processors)
program/src/state/*.rs (PDA account structs)
program/src/event_engine.rs (self-CPI event emission)
```

### Client Generation Pipeline

```
Rust code with #[codama(...)] attributes
program/build.rs → idl/subscriptions.json
scripts/generate-clients.ts
clients/rust/src/generated/ (auto-generated)
clients/typescript/src/generated/ (auto-generated; wrapped by hand-written SDK in src/)
```

### Architecture Decision Records

- [docs/001-program-architecture.md](docs/001-program-architecture.md) — Subscription Authority + delegations + PDA design
- [docs/002-subscriptions-architecture.md](docs/002-subscriptions-architecture.md) — Plans + pull-payment subscriptions
- [docs/003-versioning-migration-architecture.md](docs/003-versioning-migration-architecture.md) — Three-tier account versioning/migration
- [docs/004-program-upgrade-mechanism.md](docs/004-program-upgrade-mechanism.md) — Upgrade authority and deployment

### Key Modules

- `program/src/instructions/` — 17 instruction handlers (discriminators 0–16) + an internal `emit_event` self-CPI handler + `helpers/` (validation, token ops, traits)
- `program/src/state/` — Account structs (SubscriptionAuthority, FixedDelegation, RecurringDelegation, Plan, SubscriptionDelegation) + `versioning/`
- `program/src/events/` — Event structs and self-CPI emission
- `program/src/errors.rs` — Error codes (ranges 100-699)
- `program/src/event_engine.rs` — Self-CPI dispatcher for events

### Testing

- Rust unit tests: inline `#[cfg(test)]` modules across `program/src/` plus `program/src/tests/`. Run via `just unit-test`.
- Rust integration tests: LiteSVM-based workspace crate in `tests/integration-tests/`. Run via `just integration-test`.
- TypeScript: Vitest against Surfpool, in `clients/typescript/test/`. Includes Squads + Swig smart-wallet integration tests and security-focused tests.
- CU benchmarks: set `CU_REPORT=1` to write `cu_report.md` (posted as PR comment in CI).

### Program ID

`De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44`

## Audit Status

Audited by Cantina. See [audits/AUDIT_STATUS.md](audits/AUDIT_STATUS.md) for the baseline commit, fix-verified commit, and verification commands. Audit report PDF is in `audits/`.

## Workspace Structure

- `program/` — Pinocchio program (workspace member `subscriptions-program`)
- `clients/rust/` — Codama-generated Rust client (`subscriptions`)
- `clients/typescript/` — Hand-written SDK wrapping Codama-generated TS (`@solana/subscriptions`)
- `webapp/` — Vite + React 19 + Node API demo (faucet, deploy wizard, marketplace)
- `docs/` — Numbered ADRs
- `audits/` — Audit report and AUDIT_STATUS.md
- `runbooks/` — txtx Surfpool deployment runbooks
- `scripts/` — Validator + webapp shell scripts, client codegen TS (`generate-clients.ts`)
**Release order:** bump the version string before cutting buffers. v0.5.0
shipped `-beta.1` on-chain because the bump came after the freeze.

## Conventions

- **Pinocchio, not Anchor**: do not introduce `anchor-lang`. Use `pinocchio::AccountView`, `Address`, `ProgramResult`.
- **No `mod.rs` business logic**: only module declarations and re-exports.
- **PDA seeds co-located with state**: each state struct exposes its seed pattern; helpers live in `state/common.rs`.
- **Codama attributes drive IDL**: keep `#[codama(...)]` macros in sync with Rust types — `just generate-idl && git diff` catches drift.
- Pinocchio, never `anchor-lang`. `AccountView`, `Address`, `ProgramResult`.
- `no_std` + `extern crate alloc`; `std` only under `#[cfg(test)]`.
- `mod.rs` holds declarations and re-exports only, no logic.
- PDA seeds live with the state struct; shared helpers in `state/common.rs`.
89 changes: 89 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Contributing

Thanks for contributing to Subscriptions, the Solana program and clients for managed token delegations on SPL Token and Token-2022.

## Before you start

- Search existing issues and pull requests before opening a new one.
- For substantial changes, open an issue or start a discussion first so maintainers can confirm the approach. In general, small PRs are preferred.
- Do not include secrets, private keys, seed phrases, or production credentials in issues, pull requests, commits, logs, or screenshots.
- All commits into a Solana Foundation repository require [commit signature verification](https://docs.github.com/en/authentication/managing-commit-signature-verification/about-commit-signature-verification) to be enabled. Your PRs will not be merged without this.

## Security vulnerabilities

Do not report security vulnerabilities in public issues. Follow the [security policy](./SECURITY.md) and use the [Report a Vulnerability](https://github.com/solana-foundation/subscriptions/security/advisories/new) link. Expect a response in the advisory, typically within 72 hours.

## Development setup

Toolchain versions are checked into the repository: Rust in `rust-toolchain.toml` (installed automatically by rustup), Node.js in `.nvmrc` (`nvm use` or `fnm use`), and pnpm in the `packageManager` field of `package.json`. Do not update language runtimes, the Solana CLI, or package-manager versions as an incidental part of another change.

```sh
just setup # verify tooling, install dependencies, configure git hooks
just build # program .so, IDL, generated clients, TypeScript client
just check # format check + lint check (also run by the pre-push hook)
just test # Rust unit tests, LiteSVM integration tests, TypeScript client tests
```

`just --list` shows every recipe. The TypeScript client tests start and stop a local Surfpool validator; `just test-client` runs a fork pass followed by an offline pass.

## Making a change

Keep changes focused. A pull request should solve one problem and include the tests, documentation, generated artifacts, or migration notes needed to keep the repository usable.

Before opening a pull request:

- Run `just check` and `just test` for the affected code.
- Add or update tests when behavior changes. Rust unit tests live alongside the code in `program/src/`, integration tests in `tests/integration-tests/`, and TypeScript tests in `clients/typescript/test/`.
- Regenerate committed artifacts with `just generate-clients` whenever program types, instructions, events, or `#[codama(...)]` attributes change, and commit the resulting `idl/` and `clients/*/src/generated/` diff. CI runs `just check-generated` and fails on drift.
- Update `README.md`, the ADRs in `docs/`, and `CHANGELOG.md` when the change is part of the user-facing contract.
- Explain any new dependency and why the existing dependency set is insufficient. The program is built on Pinocchio; do not introduce `anchor-lang`.

For onchain changes, document relevant account validation, authority, state-transition, or value-movement considerations. Include a threat-model note when the change creates or modifies a trust boundary. Architecture decisions are recorded in [docs/](docs/) — read the relevant ADR before changing PDA layouts, delegation semantics, account versioning, or the upgrade path, and add a new ADR when the decision itself changes.

Compute-unit cost is part of the contract for an onchain program. CI posts a CU report on each PR; run `just test-and-benchmark` locally to see the same numbers before pushing a change to an instruction handler.

## Pull requests

Write a clear title and description that explain the problem, the approach, and how you tested it. Link related issues and call out behavior changes, compatibility concerns, or follow-up work. See the [AI use](#ai-use) section for how to disclose AI use in your PRs. Use [Conventional Commits](https://www.conventionalcommits.org/) for your commit naming, and name branches `<type>/<short-description>` (for example `fix/plan-period-overflow`).

By default, [Greptile](https://www.greptile.com) is enabled on all Solana Foundation repositories. Before maintainers review, all Greptile comments must be resolved with either a code fix or an explanation of why no change is needed.

Once CI is approved to run by maintainers, all CI errors must be addressed before the PR will be merged.

Maintainers may ask you to rebase, split a broad change, add tests, or revise documentation before merging.

Reviewers are assigned from [CODEOWNERS](.github/CODEOWNERS). Changes to the program, the IDL, or the generated clients need a review from a program maintainer.

## AI use

You may use AI-assisted tools, but you should review the generated code, understand its behavior, and run the same checks expected of any other contribution.

If you are building with AI on Solana, check out the [Solana Dev Skill](https://github.com/solana-foundation/solana-dev-skill) or the [Solana MCP](https://mcp.solana.com/) to aid in your work. This repository ships a [CLAUDE.md](./CLAUDE.md) with the repo-specific gotchas an agent needs — the non-Anchor wire format, the IDL build-script behavior, the two-pass TypeScript test setup — read it before letting an agent loose here.

Ensure that the generated code adheres to the project's coding standards and best practices. Maintainers can close PRs if they appear to be low-effort AI slop. In particular, audit your changes for the following AI code smells that increase maintenance burden:

- Comments that explain why the _previous_ behavior was wrong and the new behavior is correct. This can be helpful context for reviewers as a Github comment in the review, but we do not need a history of every code change living in the codebase
- Large blocks of comments with high density of technical jargon; comments should be distilled to clearly explain _why_ this code is doing something (if it's not obvious), not _what_ (the code should speak for itself).
- Drive-by refactoring of code that is not relevant to the actual change being made.

Two more that matter here: never hand-edit files under `idl/` or `clients/*/src/generated/` — regenerate them — and do not let an agent add defensive checks or allocations to instruction handlers without checking the CU report.

### Disclosure

It can be helpful to note the extent to which AI was used in the change. For example, adding

> I wrote all of the code for this feature, and had Claude update the documentation and create tests accordingly

or

> I architected the change and handed all implementation over to Codex

to the pull request description can be helpful context for reviewers.

### Communication

If maintainers have suggested changes, feedback, or questions about your code, you should not be copy/pasting the questions to an LLM and copy/pasting the response. You being able to distill the information that AI produces it what makes your contribution valuable.

## License

By contributing, you agree that your contributions are licensed under the project's [LICENSE](./LICENSE).
Loading