diff --git a/.claude/commands/packages.md b/.claude/commands/packages.md new file mode 100644 index 00000000..c5968a51 --- /dev/null +++ b/.claude/commands/packages.md @@ -0,0 +1,18 @@ +Explain the Train Protocol packages architecture to help agents understand the codebase. + +Read and summarize the following README files to understand what each package does: +- packages/sdk/README.md — @train-protocol/sdk (core HTLC logic, API client, verification, key derivation) +- packages/react/README.md — @train-protocol/react (React hooks, providers, swap state management) +- packages/blockchains/evm/README.md — @train-protocol/evm (EVM HTLC client) + +Also check for READMEs in other blockchain packages: +- packages/blockchains/solana/ +- packages/blockchains/starknet/ +- packages/blockchains/aztec/ + +After reading, provide a concise summary of: +1. What each package does and its key exports +2. How the packages relate to each other (dependency graph) +3. The plugin registration pattern (how chain-specific packages plug into the SDK) +4. The provider hierarchy in @train-protocol/react +5. The HTLC swap flow across packages diff --git a/.claude/commands/reviewchanges.md b/.claude/commands/reviewchanges.md new file mode 100644 index 00000000..7c68a508 --- /dev/null +++ b/.claude/commands/reviewchanges.md @@ -0,0 +1,308 @@ +--- +name: code-review +description: Entropy-reducing code review. Diff-anchored but context-aware. Favors deletion, consolidation, and simplification over additive fixes. +--- + +> **TEMPORARY RULE**: Always compare against `dev` branch. Use `git diff dev...HEAD` for all diffs. Do not do any changes, review only. + +## How This Review Works + +Each layer runs as an **independent subagent** via the Task tool (`subagent_type: "general-purpose"`). This ensures complete context isolation — no layer's analysis is influenced by another layer's findings. Each subagent gets a fresh context, runs its own `git diff`, reads the affected code, and reviews through only its assigned lens. + +You (the main agent) orchestrate: determine what changed, spawn one subagent per layer **sequentially**, and relay findings to the user. Sequential execution means each subagent can safely edit code — no conflicts. + +### The Layers + +| Layer | Name | Focus | Impact | +|-------|------|-------|--------| +| 1 | **Architecture & Boundaries** | System structure, trajectory, separation of concerns | Highest | +| 2 | **Data Flow & Contracts** | Encapsulation, coupling, interface boundaries | High | +| 3 | **Testability** | Test seams, dependency injection, isolation | High | +| 4 | **Security & Trust Boundaries** | Auth, input sanitization, trust boundaries, secrets | Medium-high | +| 5 | **Correctness & Safety** | Logic, edge cases, data integrity, transactions | Medium | +| 6 | **Test Coverage** | Missing tests, edge cases, error paths, test quality | Medium | +| 7 | **Performance & Efficiency** | Hot paths, N+1 queries, pagination, buffering, cost | Medium | +| 8 | **Observability & Operability** | Logging, metrics, tracing, graceful degradation | Low-medium | +| 9 | **Code Hygiene** | Dead code, duplication, naming, clarity | Low | + +--- + +## Orchestration Process + +1. **Understand intent**: Look at the recent changes (`git diff`, `git log`) and write a 1-2 sentence summary of what was changed and why — the feature, bug fix, or refactor goal. This is the intent summary. + +2. **Triage — decide which layers to run.** Based on the diff, determine which layers are relevant and which to skip. Present the triage to the user before starting: + - List which layers will run and why + - List which layers are skipped and why + - Then proceed without waiting for confirmation + + **Triage guidelines** — skip a layer when the changes clearly have nothing for it to review: + - **Architecture & Boundaries**: Always run. Every non-trivial change has structural implications. + - **Data Flow & Contracts**: Skip if changes don't touch module interfaces, data passing, or cross-boundary communication (e.g., pure internal logic changes, styling, config). + - **Testability**: Skip if no new functions, modules, or components are introduced — only relevant when new code needs to be testable. + - **Security & Trust**: Skip if changes don't touch API endpoints, user input handling, authentication, authorization, or data access. + - **Correctness & Safety**: Always run. Any code change can introduce bugs. + - **Test Coverage**: Skip if changes are purely config, styling, documentation, or trivial refactors with no behavioral change. + - **Performance & Efficiency**: Skip if changes don't involve data access, loops, rendering, API calls, or anything on a hot path. + - **Observability & Operability**: Skip if no backend service code, error handling, or operational code changed. + - **Code Hygiene**: Always run. Any code change can leave behind mess. + + When in doubt, run the layer. The cost of a false positive ("no issues found") is low. The cost of skipping a layer that had something to catch is high. + +3. **Run selected layers sequentially.** For each layer, spawn a Task subagent (`subagent_type: "general-purpose"`) with a prompt composed from the **Subagent Prompt Template** below. Paste the full text of the relevant layer section into the template. Wait for each to complete before starting the next. + +4. **After each subagent completes**: + - If issues were found and fixed → summarize the findings to the user, then wait for "next layer" or confirmation before proceeding + - If no issues found → report "**Layer N: No issues found.**" and immediately proceed to the next layer + +5. **Update spec checkboxes**: When the user confirms a layer is complete (e.g., "looks good", "approved", "next layer"), mark the corresponding checkbox in the spec's "Code Review" section (change `- [ ]` to `- [x]`) + +--- + +## Subagent Prompt Template + +Compose each subagent's prompt by filling in this template. Replace `[placeholders]` with the actual content. Include the **full text** of the relevant layer section from this skill — do not summarize or abbreviate it. + +``` +You are reviewing code for [Layer Name] concerns. + +## What Changed + +[1-2 sentence intent summary] + +## Your Task + +**Understand first, then review.** Do not jump into line-level analysis. Start from the system level and work down. + +1. **Understand the system**: Run `git diff` to see what changed. Read the affected files and their surroundings — imports, callers, related modules. Understand what part of the system this is, what it's responsible for, and how it connects to the rest. + +2. **Evaluate the approach**: Before examining any specific code, ask: is this the right approach to the problem? Is this the right place for this change? Does the overall direction make sense? If the approach itself is wrong, that matters more than any line-level issue. + +3. **Review through your lens**: Only now apply your specific review lens to the details. Look for issues within the approach, not just within individual lines. + +4. **Fix and report**: Fix any issues by editing code directly. Report what you found and what you fixed. If nothing was found, say "No issues found." + +## Guiding Principles + +**Core question**: If a staff engineer was designing this from scratch, knowing everything they know now — is this how they would build it? + +**Goal**: Reduce system entropy. Favor simplicity over additive fixes. Prefer deletion, consolidation, and realignment over adding complexity. + +**Scope**: The diff is your entry point and center of gravity. Reason outward only as needed to evaluate fit within the broader system. + +**Discipline**: "No issues found" is a valid output. Do not invent issues. Do not nitpick style when the code is sound. Only flag things that should change, not things that could change. + +## Your Review Lens + +[Paste the full layer section content here, starting from the bold description line through all bullet points and sub-sections] +``` + +--- + +## Layer 1: Architecture & Boundaries + +**This layer produces the most significant changes.** + +This layer has two parts. First, evaluate the changes themselves for structural soundness. Then, zoom out to evaluate the architectural trajectory — where these changes are pushing the system, and whether that direction still makes sense. + +### Part A: Structural Review + +Take the bird's-eye view. Look at the system holistically — across changed and unchanged code. Identify architectural violations and places where the current implementation no longer fits the problem. + +Focus on: + +- **Separation of concerns**: Code that started with clean separation often drifts as features accumulate. Look for modules that accreted responsibilities over time. In domain-based systems, each domain should be responsible for its own concerns — check that code lives in the correct domain and cross-domain dependencies go through proper interfaces. +- **Mixed responsibilities**: Functions or modules doing multiple unrelated things. Data access interleaved with business logic. UI components making API calls or containing validation rules. Controllers doing transformation work that belongs in a service layer. +- **Scattered concerns**: Related logic spread across multiple files or layers when it should be co-located. The same concept implemented differently in different places. +- **Over-engineered abstractions**: Places where subtractive refactoring would beat additive fixes. Situations where removing code, collapsing layers, or realigning modules to current requirements would restore coherence. +- **Structural opportunities**: This is where the Core Question applies most directly. What would be different if designed from scratch? Is there a simpler, more coherent structure hiding under the accretion? +- **File size and complexity**: Are files growing too large? Files that exceed a few hundred lines or handle too many responsibilities should be broken down. Large files are a structural smell — they often indicate mixed concerns or missing abstractions. +- **Simplicity**: Prefer the simplest solution that works. Avoid clever tricks. Code should be obvious and boring, not impressive. If there's a simpler way to achieve the same result, that's the right way. +- **Emergent consolidation**: Before these changes, the existing structure may have made sense. But now that new code exists, do patterns emerge that warrant consolidation? Does the combination of old and new code reveal opportunities for shared abstractions, unified interfaces, or merged modules that weren't justified before? The right abstraction often becomes clear only after you have multiple concrete implementations. + +**Frontend/Mobile considerations:** +- **Component structure**: Are components appropriately sized and focused? Is there a clear hierarchy? Are presentational and container concerns separated following best practices? +- **State management**: Does state live at the right level? Is there prop drilling that should use context? Is global state used appropriately or overused? +- **Navigation patterns**: Does navigation follow platform conventions and best practices? + +### Part B: Architectural Trajectory + +After reviewing the structural soundness of the changes, zoom out one or two levels above the bird's-eye view. Use the changes as a central theme to read the direction the system is evolving in, then ask: **is the current architecture still the right foundation for where this system is heading?** + +This is not about the changes being wrong. It's about recognizing inflection points — moments where incremental changes are collectively pushing the system toward a shape that would be better served by a different foundational approach. Each individual change may be perfectly reasonable, but the cumulative trajectory may reveal that the system has outgrown its original architecture. + +How to evaluate trajectory: + +1. **Read the direction**: What do these changes (and recent commits in the same area) tell you about how this part of the system is evolving? What new capabilities, patterns, or responsibilities are being added? What's growing in complexity? + +2. **Project forward**: If this trajectory continues for 3-5 more iterations of similar changes, what does the system look like? Does the current architecture accommodate that gracefully, or does it start to buckle? Are we accumulating workarounds, special cases, or friction that signals a mismatch between the architecture and the problem it's solving? + +3. **Test the alternative**: If you were designing this area from scratch today — knowing the current requirements, the trajectory, and everything learned so far — would you choose the same architecture? If not, what would you choose instead? Be specific: name the pattern, the restructuring, or the different decomposition. + +4. **Assess the gap**: How wide is the gap between the current path and the better path? Is it a minor adjustment (restructure a module, introduce an abstraction) or a fundamental rethink (different data model, different responsibility boundaries, different paradigm)? Is the cost of continuing on the current path accelerating? + +**What to report**: Only raise trajectory concerns when there's a concrete, better alternative and the gap is wide enough to warrant action or at minimum awareness. State what the current trajectory is, where it leads, and what the alternative would be. Don't propose vague "we should rethink this" — name the specific architectural change and why the trajectory makes it worth considering now rather than later. + +**What NOT to report**: Don't flag trajectories that are fine. Don't speculate about hypothetical future requirements that aren't implied by the actual changes. Don't propose rewrites for their own sake. The bar is: would a staff engineer, seeing these changes, say "we're heading toward a wall — we should course-correct now while it's cheap"? + +--- + +## Layer 2: Data Flow & Contracts + +**This layer addresses how modules communicate and maintain boundaries.** + +When concerns are properly separated, each piece can be understood, tested, and changed in isolation. When encapsulation is intact, you can refactor internals without breaking callers. Violations of these principles are often the root cause of code that's hard to reason about and fragile to change. + +Focus on: + +- **Leaky abstractions**: Internal details exposed through public interfaces. Other modules reaching into internals instead of using defined contracts. +- **Implicit coupling**: Modules that depend on each other's internal structure rather than explicit interfaces. Changes in one place that unexpectedly break something elsewhere. +- **Broken or implicit data flow**: Data flow that has become implicit over time rather than explicit. State that can be mutated from outside its owning module. +- **Interface boundaries**: Are contracts clear? Are dependencies explicit? Can internals be changed without breaking callers? + +--- + +## Layer 3: Testability + +**This layer ensures the code can be verified and maintained with confidence.** + +Untestable code often signals structural problems. If you can't test something in isolation, it's usually too tightly coupled. Testability issues caught here often require restructuring — better to address now than after tests are written around a bad design. + +**Important**: This layer is about code structure that enables testing, not about writing tests. Do not propose specific tests here. Focus on structural changes that would make the code testable. + +Focus on: + +- **Dependency injection**: Are dependencies passed in or hardcoded? Can external services, databases, and time be mocked? +- **Test seams**: Are there clear boundaries where test doubles can be inserted? Or is everything tightly coupled with no injection points? +- **Side effects**: Are side effects isolated and controllable? Can you test business logic without triggering I/O? +- **Test isolation**: Can tests run independently and in parallel? Are there shared mutable state or ordering dependencies? +- **Boundary clarity**: Are the units clear? Is it obvious what constitutes a unit test vs integration test for this code? + +--- + +## Layer 4: Security & Trust Boundaries + +**This layer ensures the system is secure by design, not by accident.** + +Security issues often require structural changes — adding middleware, restructuring data flow, or introducing new validation layers. Catching these early prevents expensive rework. + +Focus on: + +- **Authentication & authorization**: Are auth checks at the right points? Can users access only what they should? Are there missing permission checks on new endpoints or operations? +- **Trust boundaries**: What data comes from users vs internal systems? Is external input treated as untrusted? Are there assumptions about data integrity that could be violated? +- **Input sanitization**: Beyond validation, is input sanitized appropriately? SQL injection, command injection, XSS, path traversal vulnerabilities. +- **Secrets handling**: Are secrets hardcoded? Properly scoped? Logged accidentally? Exposed in error messages or stack traces? +- **Audit logging**: Are sensitive operations logged for security auditing? Can we reconstruct what happened if something goes wrong? +- **Rate limiting & abuse prevention**: Can this be abused at scale? Are there missing rate limits on expensive operations? + +--- + +## Layer 5: Correctness & Safety + +**This layer focuses on bugs, edge cases, data integrity, and resource management.** + +This includes algorithmic correctness, data correctness, and resource safety. Transaction semantics, migrations, and resource lifetimes are correctness concerns — bugs in these areas cause real failures. + +Focus on: + +- **Logic defects**: Incorrect behavior, wrong conditions, off-by-one errors, race conditions. +- **Unsafe edge cases**: Input validation gaps, null/undefined handling, boundary conditions. +- **Data integrity**: Are constraints enforced at the right level? Can invalid states be represented? Are there race conditions in uniqueness checks? +- **Transactions**: Are related operations atomic when they need to be? Can partial failures leave inconsistent state? +- **Migrations**: Schema changes, data transformations, rollback safety. Will existing data work with new code? +- **Backward compatibility**: Will new data work if code is rolled back? Are there breaking changes to stored data? +- **Resource lifetimes**: Connections, file handles, subscriptions — properly acquired and released? Memory leaks? +- **Cancellation and context propagation**: Long-running operations respect cancellation? Context passed correctly? +- **Systemic risks**: Places where data integrity relies on assumptions instead of guarantees. Implicit ordering dependencies. States that "should never happen" but aren't enforced. +- **Failure modes**: What happens when things go wrong? Are errors handled appropriately? Do failures cascade or stay contained? +- **Idempotency**: Can operations be safely retried? Are there unintended side effects on repeat? + +**Frontend/Mobile considerations:** +- **Client-side state consistency**: Can the UI get into inconsistent states? Are there race conditions between user actions and async responses? +- **Optimistic updates**: If using optimistic UI, is rollback handled correctly on failure? +- **Stale data**: Can users act on stale data? Is cache invalidation handled properly? + +--- + +## Layer 6: Test Coverage + +**This layer identifies missing tests and writes them.** + +Add the missing tests directly — do not defer them or just list gaps. Read existing test files to understand the project's testing patterns, then write tests that follow those conventions. + +Focus on: + +- **Missing unit tests**: Core business logic that lacks test coverage. Functions with complex conditions or branching. +- **Missing integration tests**: Interactions between components, API endpoints, database operations that aren't tested together. +- **Edge cases**: Boundary conditions, empty inputs, maximum values, error states that should be tested but aren't. +- **Error paths**: Exception handling, failure scenarios, timeout behavior — often untested. +- **Happy path gaps**: Core user flows that should have end-to-end coverage. +- **Regression risks**: Bug fixes or complex changes that should have tests to prevent recurrence. +- **Test quality**: Existing tests that are brittle, test implementation details, or don't actually verify behavior. + +**Output**: Summarize what tests you added (files + scenarios covered). + +--- + +## Layer 7: Performance & Efficiency + +**This layer identifies performance problems and unnecessary cost.** + +Performance issues range from algorithmic complexity to infrastructure cost. Some require architectural changes (high impact), others are localized fixes (medium impact). Catch them before they reach production. + +Focus on: + +- **Algorithmic complexity**: Is this accidentally O(n^2) or worse? Are there nested loops over large datasets? +- **Hot paths**: What code runs most frequently? Is it optimized appropriately? +- **N+1 queries**: Database access patterns that make one query per item instead of batching. +- **Pagination**: Large result sets handled correctly? Cursor vs offset pagination appropriateness? +- **Buffering versus streaming**: Memory implications, backpressure handling. Are large payloads loaded entirely into memory? +- **Payload sizes**: Are API responses or database fetches pulling more data than needed? Overfetching? +- **Memory usage**: Large objects held unnecessarily, unbounded growth, missing cleanup. +- **Mobile & client performance**: If applicable — bundle sizes, render performance, unnecessary re-renders. +- **Infrastructure cost**: Operations that scale poorly with usage. Expensive calls in loops. Missing caching where it would help. + +**Frontend/Mobile considerations:** +- **Rendering performance**: Are there unnecessary re-renders? Are expensive computations memoized? Are lists virtualized when needed? +- **Bundle size**: Are imports optimized? Is code splitting and lazy loading used where appropriate? +- **Network efficiency**: Are requests batched or deduplicated? Is data overfetched? Is caching used effectively? +- **Offline & network resilience**: Does the app handle poor connectivity gracefully? Are there appropriate loading and error states? +- **Mobile-specific**: Battery impact? Memory usage on constrained devices? Respects system settings (low power mode, data saver)? + +--- + +## Layer 8: Observability & Operability + +**This layer ensures the code can be understood and operated in production.** + +Production code needs instrumentation. When something goes wrong at 3am, can the on-call engineer understand what happened? While mostly additive, some observability decisions affect correctness semantics — timeouts, retries, and error classification have behavioral implications. + +Focus on: + +- **Logging**: Is there sufficient context for debugging without excessive noise? Are log levels appropriate? Are sensitive values redacted? +- **Metrics**: Can we measure feature health? Latency, error rates, throughput? Are there metrics for the key business operations? +- **Tracing**: Can we follow a request through the system? Are trace IDs propagated correctly? +- **Error messages**: Are errors actionable? Do they help diagnose the problem or just say "something went wrong"? +- **Feature flags & kill switches**: Can this be disabled without a deploy if something goes wrong? +- **Graceful degradation**: What happens when dependencies are slow or unavailable? Does the system degrade gracefully or fail completely? +- **Timeouts & retries**: Are timeout values appropriate? Do retry policies risk amplifying failures? + +**Frontend/Mobile considerations:** +- **Client-side error tracking**: Are errors captured and reported? Is there enough context to debug issues? +- **Analytics**: Are key user actions tracked? Can we measure feature adoption and user flows? +- **Performance monitoring**: Are slow renders, long tasks, or ANRs tracked? +- **Crash reporting**: Is crash reporting set up with meaningful stack traces and context? + +--- + +## Layer 9: Code Hygiene + +**This layer is for cleanup and polish. Smallest changes.** + +Focus on: + +- **Unused, stale, dead code**: Remove it. Don't comment it out, delete it. +- **Duplicated code**: Consolidate when the rule of three applies. +- **Messy or smelly code**: Code that makes the reader work harder than necessary. Readability matters — clear control flow, no clever tricks, self-documenting structure. Comments should explain "why", not "what". +- **Bug-prone patterns**: Patterns known to cause issues — stringly-typed data, boolean parameters, primitive obsession, deeply nested conditionals. +- **Naming and clarity**: Names that don't match behavior, unclear intent, missing context. \ No newline at end of file diff --git a/.claude/rules/chain-integration.md b/.claude/rules/chain-integration.md new file mode 100644 index 00000000..aad1f723 --- /dev/null +++ b/.claude/rules/chain-integration.md @@ -0,0 +1,626 @@ +--- +paths: + - "packages/blockchains/**" +--- + +# Chain SDK Integration Rules + +Rules and patterns for adding new blockchain HTLC client SDKs. Derived from `evm`, `starknet`, `solana`, `tron`, and `aztec` implementations. + +--- + +## 1. Package Structure + +``` +packages/blockchains/{chain}/ +├── src/ +│ ├── client/ +│ │ ├── index.ts # Re-exports PublicClient + WalletClient +│ │ ├── PublicClient.ts # Read-only client class (delegates to public/*.ts) +│ │ ├── WalletClient.ts # Write client class (delegates to wallet/*.ts) +│ │ ├── helpers.ts # Event parsing helpers (chain-specific) +│ │ ├── public/ +│ │ │ ├── getUserLockDetails.ts # Includes resolveUserLock + pickEventDerivedData +│ │ │ ├── getSolverLockDetails.ts # Includes resolveSolverLock +│ │ │ ├── getTransaction.ts +│ │ │ └── recoverSwap.ts +│ │ └── wallet/ +│ │ ├── userLock.ts # Executor — orchestrates build + simulate + send +│ │ ├── buildUserLockTx.ts # Pure builder — params → TransactionRequest +│ │ ├── refund.ts +│ │ ├── buildRefundTx.ts +│ │ ├── redeemSolver.ts +│ │ ├── buildRedeemSolverTx.ts +│ │ └── buildApproveTx.ts # If the chain has an ERC20-like allowance flow +│ ├── index.ts # Registration + public exports +│ ├── types.ts # Signer interface + client config types +│ ├── constants.ts # Chain-specific constants (zero addresses, fee limits, etc.) +│ ├── utils.ts # Chain-specific utilities (hex helpers, address normalization) +│ ├── rpc.ts # Custom RPC client (if needed) +│ ├── login/ +│ │ ├── index.ts +│ │ └── wallet-sign.ts +│ ├── abis/ or artifacts/ +│ └── __tests__/ +│ ├── resolveLock.test.ts # Tests for resolveUserLock + resolveSolverLock +│ ├── helpers.test.ts # Tests for helper pure functions +│ └── register{Chain}Sdk.test.ts +├── package.json +├── tsconfig.json +└── vitest.config.ts +``` + +**Key structural rules:** +- Each read/write method lives in its own file under `client/public/` or `client/wallet/` +- `resolveUserLock()` is co-located in `getUserLockDetails.ts`, `resolveSolverLock()` in `getSolverLockDetails.ts` — exported for testing +- `pickEventDerivedData()` is co-located in `getUserLockDetails.ts` (EVM/Tron) or `client/helpers.ts` (Starknet) +- Each write method has a **paired builder file** (`build{Method}Tx.ts`) and an **executor file** (`{method}.ts`): + - The **builder** is a pure synchronous function: takes params, returns a chain-specific `TransactionRequest` (e.g., `EvmTransactionRequest = { to, data, value?, chainId? }`). No RPC, no signer. + - The **executor** is a thin orchestrator that consumes the builder, performs simulation/preflight reads, and sends via the signer. + - Builders are exposed as public methods on the wallet client (`buildUserLockTx`, `buildRefundTx`, `buildRedeemSolverTx`, plus `buildApproveTx` if the chain uses ERC20-style allowances) so integrators can sign/submit via their own infra. +- Shared utilities (`hexToUint8Array`, `encoder`, etc.) go in `src/utils.ts` + +--- + +## 2. package.json + +```jsonc +{ + "name": "@train-protocol/{chain}", + "version": "0.1.0", + "description": "Train Protocol SDK — {Chain} HTLC client", + "type": "module", + "main": "dist/esm/index.js", + "types": "dist/types/index.d.ts", + "exports": { + ".": { + "types": "./dist/types/index.d.ts", + "default": "./dist/esm/index.js" + } + }, + "sideEffects": false, + "files": ["dist", "dist/**/*"], + "scripts": { + "build": "pnpm clean && pnpm build:esm+types", + "build:esm+types": "tsc --project tsconfig.json --rootDir ./src --outDir ./dist/esm --declaration --declarationMap --declarationDir ./dist/types", + "clean": "rimraf dist tsconfig.tsbuildinfo", + "dev": "tsc ... --watch", + "check:types": "tsc --noEmit", + "test": "vitest run" + }, + "dependencies": { + // chain-specific libraries only + }, + "peerDependencies": { + "@train-protocol/sdk": "workspace:^", + "@train-protocol/auth": "workspace:^" + }, + "devDependencies": { + "@train-protocol/sdk": "workspace:^", + "@train-protocol/auth": "workspace:^", + "@types/node": "^20", + "rimraf": "^6.0.1", + "typescript": "catalog:", + "vitest": "^4.0.18" + }, + "engines": { "node": ">=18" } +} +``` + +Chain-specific libraries go in `dependencies`. The base SDK and auth package are always `peerDependencies`. + +--- + +## 3. types.ts — Signer, Config & Registry Augmentation + +Every SDK defines a **Signer** interface, two **Config** types (public + wallet), a **WalletSignConfig** type, and augments the SDK registry maps via declaration merging: + +```ts +import type { {Chain}WalletLike } from './login/index.js' + +// Augment the SDK registries so the factory callbacks are fully typed. +declare module '@train-protocol/sdk' { + interface HTLCPublicClientConfigMap { + {namespace}: {Chain}HTLCPublicClientConfig + } + interface HTLCWalletClientConfigMap { + {namespace}: {Chain}HTLCWalletClientConfig + } +} + +// Augment the auth registry for wallet sign configs. +declare module '@train-protocol/auth' { + interface WalletSignConfigMap { + {namespace}: {Chain}WalletSignConfig + } +} + +// Config passed to deriveKeyFromWallet('{namespace}', config). +export type {Chain}WalletSignConfig = { + wallet: {Chain}WalletLike + // Add address / options if the chain's key derivation needs them. +} + +// Signer wraps the chain's wallet/signing mechanism. +// Must expose the address and a way to send transactions. +export interface {Chain}Signer { + address: string + // Chain-specific signing method(s) +} + +// Public client config — read-only operations, no signer. +export type {Chain}HTLCPublicClientConfig = { + rpcUrl: string +} + +// Wallet client config — extends public config with REQUIRED signer. +export type {Chain}HTLCWalletClientConfig = {Chain}HTLCPublicClientConfig & { + signer: {Chain}Signer +} +``` + +--- + +## 4. client.ts — Class Structure & Function Ordering + +### Two-class pattern + +Each chain implements two classes: a **public client** (read-only) and a **wallet client** (write, extends public). The wallet client inherits all read methods — no code duplication. + +```ts +import { HTLCPublicClient } from '@train-protocol/sdk' +import type { IHTLCWalletClient } from '@train-protocol/sdk' + +// Public client — read-only operations, no signer required +export class {Chain}HTLCPublicClient extends HTLCPublicClient { + protected rpc: ... + + constructor(config: {Chain}HTLCPublicClientConfig) { + super() + this.rpc = ... + // Override consensus options if needed: this.consensusOptions = { minQuorum: 1 } + } + + // ── Read Operations ──────────────────────────────────────────────── + async getUserLockDetails(params: LockParams): Promise { ... } + async getSolverLockDetails(params: LockParams, nodeUrl: string): Promise { ... } + async recoverSwap(txHash: string, network: Network): Promise { ... } + async getTransaction(txHash: string): Promise { ... } +} + +// Wallet client — write operations, signer REQUIRED at construction +export class {Chain}HTLCWalletClient extends {Chain}HTLCPublicClient implements IHTLCWalletClient { + private signer: {Chain}Signer + + constructor(config: {Chain}HTLCWalletClientConfig) { + super(config) + this.signer = config.signer // guaranteed present — no runtime check needed + } + + // ── Write Operations ─────────────────────────────────────────────── + async userLock(params: UserLockParams): Promise { ... } + async refund(params: RefundParams): Promise { ... } + async redeemSolver(params: RedeemSolverParams): Promise { ... } + // ... chain-specific write helpers +} +``` + +### Ordering rules + +1. **Write operations first** — `userLock` → `refund` → `redeemSolver` +2. **Read operations second** — `getUserLockDetails` → `getSolverLockDetails` → `recoverSwap` +3. **Public helpers third** — `getTransaction` +4. **Private helpers last** — `requireSigner()` first, then chain-specific utilities +5. **Use section comments** — `// ── Write Operations ───...` / `// ── Public Helpers ───...` separator style between groups + +### Base class methods (do NOT override) + +The base `HTLCPublicClient` class provides this method — subclasses should **not** override it: + +- `getSolverLockDetailsWithConsensus(params, nodeUrls, options?)` — queries multiple nodes via `getSolverLockDetails`, validates results match across nodes (see below) + +### Cross-node consensus + +The base class provides `getSolverLockDetailsWithConsensus()` which fans out `getSolverLockDetails()` to multiple RPC nodes and validates that all successful responses agree on critical fields (`amount`, `sender`, `recipient`, `token`, `timelock`). + +**Consensus options:** +- The `HTLCPublicClient` base class sets `protected consensusOptions: Required = { minQuorum: 2, batchSize: 3 }` by default +- Subclasses can override this in their constructor (e.g., Aztec sets `minQuorum: 1` since it typically has fewer public nodes) +- Per-call `options` passed to `getSolverLockDetailsWithConsensus()` take priority over the instance default + +**How it works:** +1. Partitions `nodeUrls` into batches of `batchSize` +2. Queries each batch in parallel via `Promise.allSettled` +3. Filters for non-null results +4. Requires at least `minQuorum` agreeing results (capped to `nodeUrls.length`) +5. Compares critical fields (`amount`, `sender`, `recipient`, `token`, `timelock`, `status`) across all valid results — throws if they disagree +6. Returns the first valid result if consensus passes +7. Supports `prefetchedResult` option to skip re-querying the first node + +Chain implementations only need to implement the single-node abstract method `getSolverLockDetails(params, nodeUrl)`. + +--- + +## 5. Write Operation Patterns + +Each write method is split into two files under `client/wallet/`: a **pure builder** (`build{Method}Tx.ts`) that returns a chain-specific `TransactionRequest`, and an **executor** (`{method}.ts`) that consumes the builder, simulates, and sends via the signer. + +### Builder/executor split + +- **Builder**: synchronous, pure. Inputs are the method's params; output is `{ to, data, value?, chainId? }` (chain-specific shape). No RPC reads, no signer access. Exposed as a public method on the wallet client. +- **Executor**: async. Calls the builder, performs any preflight (allowance check via a public-client read method, simulation via `eth_call` or equivalent), then submits via the signer. Returns the same shape as before (e.g. `AtomicResult` for `userLock`, tx hash for `refund`/`redeemSolver`). +- **ERC20-style allowance** (if the chain has it): use a paired `buildApproveTx` builder plus a public-client read like `getErc20Allowance`. The executor decides whether to issue an approve before the lock — builders never do that themselves. + +### userLock (`client/wallet/userLock.ts`) + +1. Validate required params (contract, signer, nonce, solverData) +2. Parse amount with `parseUnits(amount.toString(), decimals)` +3. Handle token approval/authorization if needed (ERC20 allowance, authwit, etc.) +4. Handle native vs token branching (e.g., Solana `userLockSol` vs `userLockToken`) +5. Build transaction, set blockhash/fee payer +6. Send via signer, confirm +7. Return `{ hash, hashlock, nonce: timestamp }` + +### refund + +1. Call `this.requireSigner()` +2. Encode and send `refundUser` / `refund_user` with the hashlock +3. Return tx hash string + +### redeemSolver + +1. Call `this.requireSigner()` +2. Convert secret to chain-native format +3. Encode and send `redeemSolver` / `redeem_solver` with hashlock, index, secret +4. Return tx hash string + +### Error handling for all write operations + +```ts +try { + // simulate (if chain supports it) + send +} catch (error) { + console.error('Error in {methodName}:', error) + throw error +} +``` + +--- + +## 6. Read Operation Patterns + +### getUserLockDetails + +Each chain's `getUserLockDetails.ts` file contains both the async function and an exported `resolveUserLock()` pure function for lock field mapping: + +1. Query contract for user lock by hashlock +2. Call `resolveUserLock(result, id, params.decimals)` — returns `BaseLockDetails | null` +3. If null, return null early +4. If `txId` is provided, extract `EventDerivedData` via `pickEventDerivedData(event)` (also co-located in same file for EVM/Tron, or in `client/helpers.ts` for Starknet) +5. Return `{ ...parsedResult, ...eventDerivedData, blockTimestamp }` as `UserLockDetails` + +**`resolveUserLock` must be an exported pure function** in the same file — this enables direct unit testing: +```ts +export function resolveUserLock(result: any, id: string, decimals: number): BaseLockDetails | null { + if (/* sender is zero/empty */) return null + return { + hashlock: id, + amount: Number(formatUnits(BigInt(result.amount), decimals)), + secret: BigInt(result.secret), + sender: ..., recipient: ..., token: ..., + timelock: Number(result.timelock), + status: Number(result.status) as LockStatus, + } +} +``` + +**Key rules for field mapping:** +- All `BaseLockDetails` fields are **required** — always populate `sender`, `recipient`, `token` (use empty string `''` if absent, never `undefined`) +- `secret` is always `bigint` — use `BigInt(result.secret)`, no conditional check for zero +- `amount` uses `params.decimals` directly — no fallback like `?? 18` + +### getSolverLockDetails — Count-Then-Loop Pattern + +Each chain's `getSolverLockDetails.ts` file contains the async function, a `getSolverLockByIndex` helper, and an exported `resolveSolverLock()` pure function: + +```ts +// getSolverLockDetails delegates to getSolverLockByIndex in a loop +async function getSolverLockDetails(params, nodeUrl) { /* count-then-loop */ } + +// getSolverLockByIndex fetches one lock and calls resolveSolverLock +async function getSolverLockByIndex(params, index, nodeUrl) { /* RPC + resolve */ } + +// Pure function — exported for unit testing +export function resolveSolverLock(result, id, decimals, index): SolverLockDetails | null { /* field mapping */ } +``` + +The count-then-loop pattern: +1. Get the count of solver locks for this hashlock +2. Loop from 1 to count (**1-indexed, NOT 0-indexed**) +3. Call `getSolverLockByIndex` which calls `resolveSolverLock` internally +4. Skip nulls (empty/invalid slots handled by `resolveSolverLock`) +5. Filter by solver address (case-insensitive) if provided +6. Return first match + +**`resolveSolverLock` must be an exported pure function** — enables direct unit testing: +```ts +export function resolveSolverLock(result: any, id: string, decimals: number, index: number): SolverLockDetails | null { + if (/* sender is zero/empty */) return null + return { + hashlock: id, + amount: Number(formatUnits(BigInt(result.amount), decimals)), + secret: BigInt(result.secret), + sender: ..., recipient: ..., token: ..., + timelock: Number(result.timelock), + status: Number(result.status) as LockStatus, + reward: Number(result.reward), + rewardTimelock: Number(result.rewardTimelock), + rewardRecipient: ..., rewardToken: ..., + index, + } +} +``` + +Key points: +- **1-indexed** — contract indices start at 1 +- **All `BaseLockDetails` + `Reward` fields are required** — `secret` is always `bigint`, strings never `undefined` +- **Include `index`** in the returned `SolverLockDetails` +- **Use `params.decimals` directly** — no `?? 18` fallback + +### recoverSwap + +**Must validate the `txHash` format at the top of the function before making any RPC calls.** Throw `'Invalid transaction hash format'` if it doesn't match. Each chain has its own expected format: + +- EVM: `/^0x[a-fA-F0-9]{64}$/` +- Starknet / Aztec: `/^0x[a-fA-F0-9]{1,64}$/` +- Solana: `/^[1-9A-HJ-NP-Za-km-z]{43,88}$/` + +Then fetch transaction + receipt, parse the `UserLocked` event from logs to extract the hashlock and token address. Use the `Network` parameter to look up token decimals, then delegate to `this.getUserLockDetails()` with `txId: txHash`. Return `UserLockDetails`. If the event is not found or `getUserLockDetails` returns null, throw. + +### getTransaction + +Non-blocking status check for a transaction by hash. Used by `useUserLockPolling` to detect failed lock transactions before the lock appears on-chain. Returns `TransactionInfo | null`. + +```ts +async getTransaction(txHash: string): Promise { + try { + // 1. Fetch the transaction receipt/status using the chain's RPC + const receipt = /* chain-specific receipt fetch */ + + // 2. Map to TransactionStatus enum — must handle all three states: + // - TransactionStatus.Pending — tx exists but not yet finalized + // - TransactionStatus.Confirmed — tx succeeded + // - TransactionStatus.Failed — tx reverted/dropped/aborted + + // 3. Return TransactionInfo + return { + hash: txHash, + status, // Required + blockNumber: '...', // Optional — string + blockTimestamp: 123456, // Optional — ms since epoch (only if cheap to obtain) + } + } catch { + return null + } +} +``` + +Rules: +- **Always wrap in try/catch returning `null`** — this runs in a polling loop; thrown errors cause noisy console output +- **Must distinguish all three statuses** — `Pending`, `Confirmed`, `Failed`. Binary mappings (e.g., only Failed/Confirmed) cause incorrect early signals +- **Must be non-blocking** — do not use methods that wait for finalization (e.g., Fuel's `waitForResult`). If the chain SDK has no non-blocking alternative, document the limitation +- **Avoid unnecessary RPC calls** — do not fetch block data for `blockTimestamp` if the polling consumer only needs `status`. Keep it minimal +- **Do not fetch `blockTimestamp` by default** — only include it if the chain returns it alongside the receipt at no extra cost + +--- + +## 7. index.ts — Registration & Exports + +Because `types.ts` augments `HTLCPublicClientConfigMap`, `HTLCWalletClientConfigMap`, and `WalletSignConfigMap`, the factory callbacks receive fully-typed configs — no `as` casts needed. + +```ts +import { registerHTLCPublicClient, registerHTLCWalletClient } from '@train-protocol/sdk' +import { registerWalletSign } from '@train-protocol/auth' +import { {Chain}HTLCPublicClient, {Chain}HTLCWalletClient } from './client.js' +import { deriveKeyFrom{Chain}Wallet } from './login/index.js' + +let registered = false + +export function register{Chain}Sdk(): void { + if (registered) return // Idempotent guard + registered = true + + registerHTLCPublicClient('{namespace}', (config) => new {Chain}HTLCPublicClient(config)) + registerHTLCWalletClient('{namespace}', (config) => new {Chain}HTLCWalletClient(config)) + + registerWalletSign('{namespace}', async (config) => { + return deriveKeyFrom{Chain}Wallet(config.wallet) + }) +} + +// Public exports +export { {Chain}HTLCPublicClient, {Chain}HTLCWalletClient } from './client.js' +export type { {Chain}HTLCPublicClientConfig, {Chain}HTLCWalletClientConfig, {Chain}Signer, {Chain}WalletSignConfig } from './types.js' +export { deriveKeyFrom{Chain}Wallet } from './login/index.js' +export type { {Chain}WalletLike } from './login/index.js' +``` + +The `{namespace}` is the chain identifier used in the registry (e.g., `'eip155'` for EVM, `'aztec'` for Aztec). + +### What to export + +- `register{Chain}Sdk` — registration function +- `{Chain}HTLCPublicClient` — public (read-only) client class +- `{Chain}HTLCWalletClient` — wallet (write) client class +- `{Chain}HTLCPublicClientConfig` — public client config type +- `{Chain}HTLCWalletClientConfig` — wallet client config type +- `{Chain}Signer` — signer type +- `{Chain}TransactionRequest` — built/unsigned transaction request type returned by builders +- `deriveKeyFrom{Chain}...` — key derivation function +- Any chain-specific wallet interface types needed by consumers + +--- + +## 8. Login / Key Derivation + +Each chain needs a `login/wallet-sign.ts` that derives a deterministic login key: + +```ts +import { deriveKeyMaterial, IDENTITY_SALT } from '@train-protocol/sdk' + +export const deriveKeyFrom{Chain}Wallet = async ( + /* chain-specific wallet/provider */ +): Promise => { + // 1. Sign a fixed message: "I am using TRAIN" + // Use the chain's native signing mechanism + const signature = /* sign the message */ + + // 2. Derive key material from signature + const inputMaterial = Buffer.from(/* signature bytes */) + const identitySalt = Buffer.from(IDENTITY_SALT, 'utf8') + return Buffer.from(deriveKeyMaterial(inputMaterial, identitySalt)) +} +``` + +Rules: +- Always use `"I am using TRAIN"` as the message content +- Always use `IDENTITY_SALT` and `deriveKeyMaterial` from the base SDK +- Define a minimal wallet/provider interface (don't import the full chain SDK for the type) + +--- + +## 9. Shared SDK Imports + +Always import these utilities from `@train-protocol/sdk` instead of reimplementing: + +```ts +// Unit conversion +import { parseUnits, formatUnits } from '@train-protocol/sdk' + +// Byte/hex conversion +import { hexToBytes, bytesToHex, toHex32 } from '@train-protocol/sdk' + +// Base classes & types +import { + HTLCPublicClient, + UserLockParams, + LockParams, + RefundParams, + RedeemSolverParams, + LockStatus, + AtomicResult, + Network, + TransactionInfo, + TransactionStatus, + ConsensusOptions, +} from '@train-protocol/sdk' +import type { + UserLockDetails, + SolverLockDetails, + BaseLockDetails, + EventDerivedData, +} from '@train-protocol/sdk' + +// Key derivation +import { deriveKeyMaterial, IDENTITY_SALT } from '@train-protocol/sdk' +``` + +--- + +## 10. Error Handling + +| Context | Pattern | +|---------|---------| +| Write operations | `try { ... } catch (error) { console.error('Error in {method}:', error); throw error }` | +| Signer guard | `private requireSigner(): Signer { if (!this.signer) throw new Error('Signer required'); return this.signer }` | +| Lock not found | Return `null` (never throw for missing locks) | +| Event decoding | Wrap in try-catch, skip non-matching events silently | +| Transaction revert | Check chain-specific revert indicator, throw with method name + error | + +--- + +## 11. Testing + +Each blockchain package must alias `@train-protocol/sdk` to source in `vitest.config.ts`: +```ts +resolve: { alias: { '@train-protocol/sdk': path.resolve(__dirname, '../../sdk/src/index.ts') } } +``` + +### resolveLock.test.ts — lock resolution tests (most critical) + +Tests `resolveUserLock` and `resolveSolverLock` directly — imported from `../client/public/getUserLockDetails` and `../client/public/getSolverLockDetails`: + +```ts +import { resolveUserLock } from '../client/public/getUserLockDetails' +import { resolveSolverLock } from '../client/public/getSolverLockDetails' + +describe('{Chain} resolveUserLock', () => { + // 1. resolves a basic user lock — check all BaseLockDetails fields + // 2. returns null for empty/zero sender (or status=0 for Aztec) + // 3. formats amount with correct decimals + // 4. maps status values correctly +}) + +describe('{Chain} resolveSolverLock', () => { + // 1. resolves solver lock with reward fields and index + // 2. returns null for empty/zero sender + // 3. includes correct index in result +}) +``` + +### helpers.test.ts — pure helper functions + +Test chain-specific helpers: `pickEventDerivedData`, `mapLockStatus` (Starknet), `parseSecret` (Solana/Aztec), `pickStarknetEventData`, etc. + +### register{Chain}Sdk.test.ts — registration smoke test + +Verify `register{Chain}Sdk()` registers the namespace and creates clients with expected methods. + +--- + +## 12. Constants + +Define chain-specific constants in `constants.ts` (preferred) or at the top of `client.ts`: + +```ts +export const TX_TIMEOUT = 120000 // Transaction confirmation timeout (ms) +export const ZERO_ADDRESS = '0x000...' // Chain's empty/zero address representation +``` + +--- + +## Summary Checklist for New Chain SDK + +- [ ] Create `packages/{chain}/` with the modular directory structure above +- [ ] In `types.ts`: + - [ ] Define `{Chain}Signer` interface + - [ ] Define `{Chain}HTLCPublicClientConfig` (rpcUrl only) and `{Chain}HTLCWalletClientConfig` (extends public + required signer) + - [ ] Define `{Chain}WalletSignConfig` type + - [ ] Add `declare module` augmentations for SDK and auth registries +- [ ] Implement `client/PublicClient.ts` — delegates to `client/public/*.ts` files +- [ ] Implement `client/WalletClient.ts` — delegates to `client/wallet/*.ts` files +- [ ] Each read method in its own file under `client/public/`: + - [ ] `getUserLockDetails.ts` — includes exported `resolveUserLock()` + `pickEventDerivedData()` + - [ ] `getSolverLockDetails.ts` — includes exported `resolveSolverLock()` + - [ ] `getTransaction.ts` + - [ ] `recoverSwap.ts` +- [ ] Each write method split into a paired builder + executor under `client/wallet/`: + - [ ] `userLock.ts` (executor) + `buildUserLockTx.ts` (pure builder) + - [ ] `refund.ts` (executor) + `buildRefundTx.ts` (pure builder) + - [ ] `redeemSolver.ts` (executor) + `buildRedeemSolverTx.ts` (pure builder) + - [ ] `buildApproveTx.ts` + `getErc20Allowance.ts` (under `client/public/`) if the chain has ERC20-like allowances + - [ ] Expose all builders as public methods on the wallet client +- [ ] Count-then-loop pattern in `getSolverLockDetails` (1-indexed) +- [ ] `getTransaction(txHash)` — non-blocking, try/catch returning `null`, all three statuses +- [ ] Set `this.consensusOptions` in constructor if chain needs non-default quorum +- [ ] Validate `txHash` format at the top of `recoverSwap` before any RPC calls +- [ ] Shared utilities in `src/utils.ts`, constants in `src/constants.ts` +- [ ] Login: `login/wallet-sign.ts` using `deriveKeyMaterial` + `IDENTITY_SALT` +- [ ] Registration: idempotent `register{Chain}Sdk()` in `index.ts` +- [ ] Exports: registration fn, both client classes, both config types, signer type, key derivation fn +- [ ] Tests: + - [ ] `resolveLock.test.ts` — test `resolveUserLock` + `resolveSolverLock` (imported from public files) + - [ ] `helpers.test.ts` — test chain-specific pure helpers + - [ ] `register{Chain}Sdk.test.ts` — registration smoke test + - [ ] `vitest.config.ts` with SDK source alias +- [ ] Add contract ABI/artifacts in `abis/` or `artifacts/` diff --git a/.claude/skills/next-best-practices/SKILL.md b/.claude/skills/next-best-practices/SKILL.md new file mode 100644 index 00000000..437896b4 --- /dev/null +++ b/.claude/skills/next-best-practices/SKILL.md @@ -0,0 +1,153 @@ +--- +name: next-best-practices +description: Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling +user-invocable: false +--- + +# Next.js Best Practices + +Apply these rules when writing or reviewing Next.js code. + +## File Conventions + +See [file-conventions.md](./file-conventions.md) for: +- Project structure and special files +- Route segments (dynamic, catch-all, groups) +- Parallel and intercepting routes +- Middleware rename in v16 (middleware → proxy) + +## RSC Boundaries + +Detect invalid React Server Component patterns. + +See [rsc-boundaries.md](./rsc-boundaries.md) for: +- Async client component detection (invalid) +- Non-serializable props detection +- Server Action exceptions + +## Async Patterns + +Next.js 15+ async API changes. + +See [async-patterns.md](./async-patterns.md) for: +- Async `params` and `searchParams` +- Async `cookies()` and `headers()` +- Migration codemod + +## Runtime Selection + +See [runtime-selection.md](./runtime-selection.md) for: +- Default to Node.js runtime +- When Edge runtime is appropriate + +## Directives + +See [directives.md](./directives.md) for: +- `'use client'`, `'use server'` (React) +- `'use cache'` (Next.js) + +## Functions + +See [functions.md](./functions.md) for: +- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams` +- Server functions: `cookies`, `headers`, `draftMode`, `after` +- Generate functions: `generateStaticParams`, `generateMetadata` + +## Error Handling + +See [error-handling.md](./error-handling.md) for: +- `error.tsx`, `global-error.tsx`, `not-found.tsx` +- `redirect`, `permanentRedirect`, `notFound` +- `forbidden`, `unauthorized` (auth errors) +- `unstable_rethrow` for catch blocks + +## Data Patterns + +See [data-patterns.md](./data-patterns.md) for: +- Server Components vs Server Actions vs Route Handlers +- Avoiding data waterfalls (`Promise.all`, Suspense, preload) +- Client component data fetching + +## Route Handlers + +See [route-handlers.md](./route-handlers.md) for: +- `route.ts` basics +- GET handler conflicts with `page.tsx` +- Environment behavior (no React DOM) +- When to use vs Server Actions + +## Metadata & OG Images + +See [metadata.md](./metadata.md) for: +- Static and dynamic metadata +- `generateMetadata` function +- OG image generation with `next/og` +- File-based metadata conventions + +## Image Optimization + +See [image.md](./image.md) for: +- Always use `next/image` over `` +- Remote images configuration +- Responsive `sizes` attribute +- Blur placeholders +- Priority loading for LCP + +## Font Optimization + +See [font.md](./font.md) for: +- `next/font` setup +- Google Fonts, local fonts +- Tailwind CSS integration +- Preloading subsets + +## Bundling + +See [bundling.md](./bundling.md) for: +- Server-incompatible packages +- CSS imports (not link tags) +- Polyfills (already included) +- ESM/CommonJS issues +- Bundle analysis + +## Scripts + +See [scripts.md](./scripts.md) for: +- `next/script` vs native script tags +- Inline scripts need `id` +- Loading strategies +- Google Analytics with `@next/third-parties` + +## Hydration Errors + +See [hydration-error.md](./hydration-error.md) for: +- Common causes (browser APIs, dates, invalid HTML) +- Debugging with error overlay +- Fixes for each cause + +## Suspense Boundaries + +See [suspense-boundaries.md](./suspense-boundaries.md) for: +- CSR bailout with `useSearchParams` and `usePathname` +- Which hooks require Suspense boundaries + +## Parallel & Intercepting Routes + +See [parallel-routes.md](./parallel-routes.md) for: +- Modal patterns with `@slot` and `(.)` interceptors +- `default.tsx` for fallbacks +- Closing modals correctly with `router.back()` + +## Self-Hosting + +See [self-hosting.md](./self-hosting.md) for: +- `output: 'standalone'` for Docker +- Cache handlers for multi-instance ISR +- What works vs needs extra setup + +## Debug Tricks + +See [debug-tricks.md](./debug-tricks.md) for: +- MCP endpoint for AI-assisted debugging +- Rebuild specific routes with `--debug-build-paths` + diff --git a/.claude/skills/next-best-practices/async-patterns.md b/.claude/skills/next-best-practices/async-patterns.md new file mode 100644 index 00000000..dce8d8cc --- /dev/null +++ b/.claude/skills/next-best-practices/async-patterns.md @@ -0,0 +1,87 @@ +# Async Patterns + +In Next.js 15+, `params`, `searchParams`, `cookies()`, and `headers()` are asynchronous. + +## Async Params and SearchParams + +Always type them as `Promise<...>` and await them. + +### Pages and Layouts + +```tsx +type Props = { params: Promise<{ slug: string }> } + +export default async function Page({ params }: Props) { + const { slug } = await params +} +``` + +### Route Handlers + +```tsx +export async function GET( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params +} +``` + +### SearchParams + +```tsx +type Props = { + params: Promise<{ slug: string }> + searchParams: Promise<{ query?: string }> +} + +export default async function Page({ params, searchParams }: Props) { + const { slug } = await params + const { query } = await searchParams +} +``` + +### Synchronous Components + +Use `React.use()` for non-async components: + +```tsx +import { use } from 'react' + +type Props = { params: Promise<{ slug: string }> } + +export default function Page({ params }: Props) { + const { slug } = use(params) +} +``` + +### generateMetadata + +```tsx +type Props = { params: Promise<{ slug: string }> } + +export async function generateMetadata({ params }: Props): Promise { + const { slug } = await params + return { title: slug } +} +``` + +## Async Cookies and Headers + +```tsx +import { cookies, headers } from 'next/headers' + +export default async function Page() { + const cookieStore = await cookies() + const headersList = await headers() + + const theme = cookieStore.get('theme') + const userAgent = headersList.get('user-agent') +} +``` + +## Migration Codemod + +```bash +npx @next/codemod@latest next-async-request-api . +``` diff --git a/.claude/skills/next-best-practices/bundling.md b/.claude/skills/next-best-practices/bundling.md new file mode 100644 index 00000000..ac5e814c --- /dev/null +++ b/.claude/skills/next-best-practices/bundling.md @@ -0,0 +1,180 @@ +# Bundling + +Fix common bundling issues with third-party packages. + +## Server-Incompatible Packages + +Some packages use browser APIs (`window`, `document`, `localStorage`) and fail in Server Components. + +### Error Signs + +``` +ReferenceError: window is not defined +ReferenceError: document is not defined +ReferenceError: localStorage is not defined +Module not found: Can't resolve 'fs' +``` + +### Solution 1: Mark as Client-Only + +If the package is only needed on client: + +```tsx +// Bad: Fails - package uses window +import SomeChart from 'some-chart-library' + +export default function Page() { + return +} + +// Good: Use dynamic import with ssr: false +import dynamic from 'next/dynamic' + +const SomeChart = dynamic(() => import('some-chart-library'), { + ssr: false, +}) + +export default function Page() { + return +} +``` + +### Solution 2: Externalize from Server Bundle + +For packages that should run on server but have bundling issues: + +```js +// next.config.js +module.exports = { + serverExternalPackages: ['problematic-package'], +} +``` + +Use this for: +- Packages with native bindings (sharp, bcrypt) +- Packages that don't bundle well (some ORMs) +- Packages with circular dependencies + +### Solution 3: Client Component Wrapper + +Wrap the entire usage in a client component: + +```tsx +// components/ChartWrapper.tsx +'use client' + +import { Chart } from 'chart-library' + +export function ChartWrapper(props) { + return +} + +// app/page.tsx (server component) +import { ChartWrapper } from '@/components/ChartWrapper' + +export default function Page() { + return +} +``` + +## CSS Imports + +Import CSS files instead of using `` tags. Next.js handles bundling and optimization. + +```tsx +// Bad: Manual link tag + + +// Good: Import CSS +import './styles.css' + +// Good: CSS Modules +import styles from './Button.module.css' +``` + +## Polyfills + +Next.js includes common polyfills automatically. Don't load redundant ones from polyfill.io or similar CDNs. + +Already included: `Array.from`, `Object.assign`, `Promise`, `fetch`, `Map`, `Set`, `Symbol`, `URLSearchParams`, and 50+ others. + +```tsx +// Bad: Redundant polyfills + + +// Good: Next.js Script component +import Script from 'next/script' + + +``` + +## Don't Put Script in Head + +`next/script` should not be placed inside `next/head`. It handles its own positioning. + +```tsx +// Bad: Script inside Head +import Head from 'next/head' +import Script from 'next/script' + + + + +// Good: Next.js component +import { GoogleAnalytics } from '@next/third-parties/google' + +export default function Layout({ children }) { + return ( + + {children} + + + ) +} +``` + +## Google Tag Manager + +```tsx +import { GoogleTagManager } from '@next/third-parties/google' + +export default function Layout({ children }) { + return ( + + + {children} + + ) +} +``` + +## Other Third-Party Scripts + +```tsx +// YouTube embed +import { YouTubeEmbed } from '@next/third-parties/google' + + + +// Google Maps +import { GoogleMapsEmbed } from '@next/third-parties/google' + + +``` + +## Quick Reference + +| Pattern | Issue | Fix | +|---------|-------|-----| +| `