Conventions & patterns

These are the recurring patterns in the codebase. Follow them when you change the system — most bugs come from breaking one of these.

Prepare → sign → confirm (custody)

On‑chain actions are never signed by the backend. The flow is always:

  1. Prepare — the service returns the call data / ABI for the action (POST …/actions/:intent).
  2. Sign — the user signs client‑side via wagmi/Privy.
  3. Confirm — the client posts the txHash; the service verifies the transaction on‑chain (right contract, function, sender, args) before mutating the database (POST …/confirm).

The chain is the source of truth. Don't add a code path that updates custody state without an on‑chain verification.

Every cross‑cutting operation carries an operationHash = a UUID encoded as bytes32, byte‑identical to the contract's offChainId. Use the helpers in @axon/db (operationHashFromId) so DB rows and on‑chain events always join. Never generate it ad‑hoc.

Money is BigInt, transported as strings

Use packages/db/src/money.ts for all arithmetic (micro‑USDC BigInt, contract‑parity). Amounts cross service/HTTP boundaries as decimal strings, never JS numbers (ethers.parseUnits throws on float artifacts). feePct, bps, totals — all through money.ts.

Dual‑mode & profile scoping

  • The acting profile is sent as X-AXON-Profile-Id; services validate the token's user owns that profile (mismatch → 403).
  • Resolve the active wallet through useActiveWallet() (web/mobile) — never hard‑code wallets[0] (it isn't HD‑ordered; you'd sign with the wrong key).
  • canSign reflects whether the wallet can sign in this browser; gate signing UI on it.

No cross-service foreign keys

Links across service boundaries are plain string IDs, not Prisma relations (e.g. FundingCoverage.settlementId). Integrity is enforced in code. Don't add an FK across a boundary — it couples deploys.

Idempotency

Side‑effectful ingestion must be idempotent:

  • coverage on inbound transfers is keyed on sourceTxId (unique) — re‑scans are harmless;
  • settlement recording is idempotent on source + sourceId;
  • webhooks dedupe on WebhookEvent (provider, externalEventId).

Background workers

Workers are plain interval loops started in a module's onModuleInit (e.g. CoverageWatcher 30 s, settlements reconcile 60 s / anchor 30 min, reconciliation 30 s). They are:

  • single‑flight (a running guard so passes don't stack),
  • stateless / re‑entrant where possible (wide look‑back windows + idempotency instead of cursors),
  • fail‑soft (a failed pass logs and retries next tick; it never crashes the service).

Auth & secrets

  • Verify JWTs with verifyBearer / verifyHs256Jwt from @axon/shared-config — services re‑verify even though the gateway already did.
  • Read config through getSecret() (fail‑closed in prod). Don't read process.env directly for secrets.
  • NEXT_PUBLIC_* are build‑time — adding one means rebuilding the frontend image.

Notifications

Notify the counterparty, never the actor who triggered the event. Use the typed NotificationType values.

Reports

PDF/CSV reports go through packages/shared-reports. Endpoints return base64 (PDF) or text (CSV); the gateway passes binary through verbatim (don't JSON‑wrap it).

Gotchas worth knowing

  • Prisma CLI lives at packages/db/node_modules/.bin/prisma (pnpm doesn't hoist it to root).
  • SERVICE_SETTLEMENTS_HOST must be correct or cross‑service settlement recording silently fails.
  • Services compile with strictNullChecks: false → discriminated‑union narrowing doesn't work in shared types; prefer optional fields.
  • The reconciliation service mounts health at /api/v1/health (not /api/v1/reconciliation/health).
  • Legacy shell + mfe-* are gated behind the legacy-mfe compose profile (they OOM the default build) and are superseded by apps/web.

results matching ""

    No results matching ""