Skip to content

Latest commit

 

History

History
467 lines (385 loc) · 19.6 KB

File metadata and controls

467 lines (385 loc) · 19.6 KB

Glossary

Audience: contributors adding or changing features in the RemitWise Frontend. Operators and downstream integrators have their own entry points (README.md → "API Versioning", docs/openapi.json) — keep those separate.

This document defines the internal jargon used throughout the RemitWise codebase and docs. Every entry is one paragraph for the definition, followed by a short concrete example that compiles against the current source and a link to the canonical doc or source file for the whole story.

When you introduce a new in-codebase term, add it here in alphabetical position rather than inventing a new top-level doc — orphaned docs rot fastest.


Anchor

In the Stellar ecosystem, an anchor is the regulated service that exchanges fiat (USD, EUR, etc.) for the on-chain USDC/XLM that RemitWise sends. In this repo "anchor" refers to the same concept plus the small set of HTTP endpoints that talk to one: /api/anchor/rates, /api/anchor/deposit, /api/anchor/withdraw, plus the inbound /api/webhooks/anchor callback. Anchors are configured via ANCHOR_API_BASE_URL and authenticated either through SEP-10 web auth for outbound requests or via HMAC for inbound webhooks.

# Cache miss on the server: hit the anchor's /quote through our wrapper.
curl -s http://localhost:3000/api/anchor/rates
# → { "USDC": { "USD": 1.0, "MXN": 17.4 }, "XLM": { ... } }

See also: docs/Anchor_Webhooks.md, docs/ANCHOR_ADMIN_SHUTDOWN.md.

Anchor webhook

An anchor webhook is the asynchronous callback an anchor posts to POST /api/webhooks/anchor after a deposit or withdrawal changes state. The handler at app/api/webhooks/anchor/route.ts returns 200 OK immediately after verifying the HMAC signature and persisting the raw payload, then runs the actual reconciliation in the background via runBackgroundJob('anchor_webhook_event', …) so a slow anchor never blocks the response. Every event goes through the same retry path; events that exceed WEBHOOK_MAX_RETRIES end up in the Dead-Letter Queue.

// app/api/webhooks/anchor/route.ts
import { verifySignature } from '@/lib/webhooks/verify'
import { saveWebhookEvent, processWebhookEvent } from '@/lib/webhooks/processor'

export async function POST(request: NextRequest) {
  const rawBody = await request.text()
  const secret = process.env.ANCHOR_WEBHOOK_SECRET!
  if (!verifySignature(rawBody, request.headers.get('x-signature'), secret)) {
    return new Response('Invalid signature', { status: 401 })
  }
  const payload = JSON.parse(rawBody)
  const eventId = await saveWebhookEvent('anchor', payload.event_type, rawBody)
  runBackgroundJob('anchor_webhook_event', () => processWebhookEvent(eventId, handleAnchorEvent))
  return NextResponse.json({ received: true, eventId })
}

See also: docs/Anchor_Webhooks.md, docs/WEBHOOK_RETRY_AND_DLQ.md.

Approvals queue

The approvals queue is a client-side reducer + hook pair that drives the multi-signer flow on Family Wallet actions (buildAddMemberTx, buildUpdateSpendingLimitTx). Each item starts in building, advances to pending once the unsigned XDR is produced, then to awaiting-signature → signed | rejected | expired as co-signers act. The hook useApprovalsQueue is shared by lib/hooks/useApprovalsQueue.ts and consumed from components/family/ApprovalsQueue.tsx. Signing is delegated to signTransaction from the wallet-kit (useWallet) — there is no bespoke signing primitive.

// lib/hooks/useApprovalsQueue.ts (reducer in pseudo-shape)
type QueueItem = {
  id: string
  kind: 'add-member' | 'update-limit'
  status: 'building' | 'pending' | 'awaiting-signature' | 'signed' | 'rejected' | 'expired'
  xdr?: string
  signatures: string[]
}

See also: docs/family-multisig-approvals.md.

CSP nonce

A CSP nonce in this repo is a per-request opaque string emitted by middleware.ts → applySecurityHeaders(response, nonce, isApiRoute) and embedded into the Content-Security-Policy header (script-src 'self' 'nonce-<value>' 'strict-dynamic') plus every authorised inline <script> tag. Without the matching nonce attribute the browser refuses to run the script, which neutralises injected <script> blocks. Always read headers().get('x-nonce') from a Server Component when adding a new inline script (see How Nonces Work in the security doc). This is a different string from the auth nonce produced by /api/auth/nonce — they are generated by different code paths and have different lifetimes.

// app/layout.tsx
const nonce = headersList.get('x-nonce') || ''
return <script nonce={nonce} dangerouslySetInnerHTML={{ __html: themeScript }} />

See also: docs/SECURITY.md.

Dashboard widget deep-link

The dashboard widget deep-link is a small URL convention used to send a support user (or yourself) directly to a specific dashboard panel: /dashboard?widget=six-month-trends. The four stable widget IDs live in lib/config/widgets.ts → WIDGET_IDS and must not be renamed: they are part of the public URL surface and any rename is a breaking change. useWidgetDeepLink(id) (from lib/hooks/useWidgetDeepLink.ts) reads the query string, scrolls the matching element into view, and pulses a brand-red outline (widget-highlight class in app/globals.css); the pulse is suppressed for prefers-reduced-motion: reduce users.

import { useWidgetDeepLink } from '@/lib/hooks/useWidgetDeepLink'
import { WIDGET_IDS }         from '@/lib/config/widgets'

export function SixMonthTrendsWidget() {
  const ref = useWidgetDeepLink(WIDGET_IDS.SIX_MONTH_TRENDS)
  return <section id={WIDGET_IDS.SIX_MONTH_TRENDS} ref={ref}>{/* … */}</section>
}

Supported values: six-month-trends, money-distribution, recent-transactions, savings-by-goal. See also: docs/architecture.md → "Deep-link support for dashboard widgets".

Dead-Letter Queue (DLQ)

The DLQ is the terminal holding pen for webhook events that failed processing enough times to exhaust WEBHOOK_MAX_RETRIES (default 5, see lib/webhooks/processor.ts → WEBHOOK_RETRY_CONFIG). Failed events get status dlq plus a lastError message and surface in the admin UI at /admin and through GET /api/v1/admin/webhooks/dlq. Operators replay them via POST /api/v1/admin/webhooks/dlq/[id]/replay once the underlying issue is fixed; until then they are durable via Prisma (WebhookEvent). Note: this is not a queue in the streaming sense — it is a status flag on a persisted row.

# Admin: list failed events from the Anchor pipeline.
curl -H "X-Admin-Key: $ADMIN_SECRET" \
  "http://localhost:3000/api/v1/admin/webhooks/dlq?source=anchor&limit=20"

See also: docs/WEBHOOK_RETRY_AND_DLQ.md.

Idempotency key

An idempotency key is an opaque, client-generated string (typically a UUID v4) the caller sends in the idempotency-key header on money-moving POSTs (/api/remittance/send, /api/bills, /api/insurance, etc.) so that a network retry, a double-click, or a client crash does not produce two transfers. The middleware at lib/idempotency/middleware.ts stores the response keyed by the string plus a hash of the body, replays the cached body on hit (X-Idempotent-Replay: true), and returns 409 Conflict if the same key arrives with a different body. TTL is 24h and the store is in-memory — duplicate the write path in production with Redis. Generate one key per logical operation, never per request.

const idempotencyKey = crypto.randomUUID()
await fetch('/api/remittance/send', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'idempotency-key': idempotencyKey, // reuse on retry
  },
  body: JSON.stringify({ recipient: 'G...ABC', amount: 100 }),
})

See also: docs/IDEMPOTENCY.md.

iron-session cookie

The iron-session cookie (default name SESSION_COOKIE_NAME, conventionally remitwise-session) is an encrypted, httpOnly cookie that ties a session to the user's Stellar address. lib/session.ts uses the sealData/unsealData helpers from the iron-session library with SESSION_PASSWORD as the keying material; on login, the iron-session helper is invoked with the verified Stellar address and the cookie is set for SESSION_MAX_AGE (default 86 400 s / 24 h). The library uses iron-session (no next-auth/next-iron-session), which is why Sentry scrubbers (sentry.server.config.ts, instrumentation.ts) redact anything matching the "iron-session": "..." field pattern.

import { sealData } from 'iron-session'
// After /api/auth/login verifies the signed nonce…
const sealed = await sealData(
  { address: 'G...PUBLIC', createdAt: Date.now() },
  { password: process.env.SESSION_PASSWORD!, ttl: 60 * 60 * 24 },
)
response.cookies.set('remitwise-session', sealed, {
  httpOnly: true, sameSite: 'lax', secure: true,
})

See also: docs/architecture.md → "Session & Auth", lib/session.ts.

manageData operation

A Stellar ManageData operation is the on-chain instruction we use to encode RemitWise "application state" actions (create / pay / cancel a bill, create / pay / deactivate an insurance policy) without deploying a new contract method per action. The server assembles an unsigned TransactionBuilder whose sole op is manageData({ name: 'rw:<kind>', value: JSON.stringify(payload) }), then returns the XDR for the user to sign and submit. This keeps the contract surface stable across app releases.

// lib/contracts/bill-payments.ts (simplified)
const op = Operation.manageData({
  name: 'rw:create-bill',
  value: JSON.stringify({ name, amount, dueDate }),
})
const tx = new TransactionBuilder(source, { fee, networkPassphrase })
  .addOperation(op)
  .setTimeout(30)
  .build()
return { xdr: tx.toXDR() }

See also: docs/bills-state-inventory.md, docs/SAVINGS_GOALS_IMPLEMENTATION.md.

Nonce (auth)

The auth nonce is the 32-byte random hex string the server returns from GET /api/auth/nonce?address=<G_PUBLIC_KEY> (the same handler also accepts POST /api/auth/nonce with { address | publicKey: "G…" } in the JSON body), and the client must sign with their Stellar wallet key as part of the challenge-response login flow. The server stores it in lib/auth-cache.ts against the caller's address with a 5-minute TTL; the same store is consulted atomically on POST /api/auth/login via getAndClearNonce(address) so a nonce cannot be consumed twice. The signed message is the byte representation of the hex nonce (NOT a human-readable prefix), and the address is checked with StrKey.isValidEd25519PublicKey first — bad addresses return 400 with a hint referencing the ?address=G… syntax. This nonce is unrelated to the CSP nonce emitted by middleware.ts.

# 1. Get nonce (GET-with-query OR POST-with-body are both supported)
curl -s -X POST http://localhost:3000/api/auth/nonce \
  -H 'content-type: application/json' -d '{"address":"G...PUBLIC"}'
# → { "nonce": "9f86d081…", "address": "G...PUBLIC", "expiresAt": "2026-…" }

# 2. Sign nonce with wallet; then:
curl -s -X POST http://localhost:3000/api/auth/login \
  -H 'content-type: application/json' \
  -d '{"address":"G...PUBLIC","nonce":"9f86...","signature":"<base64>"}'
# → 200 + Set-Cookie: remitwise-session=...

See also: docs/AUTH_IMPLEMENTATION.md, docs/AUTH_QUICK_REF.md, docs/CANONICALISATION.md → "Auth nonce bytes".

Non-custodial

A flow is non-custodial when RemitWise never holds the user's private keys and the server only ever returns an unsigned XDR for the client wallet to sign. Every contract-mutating route (/api/remittance/build, /api/v1/bills, /api/v1/insurance, family wallet, etc.) follows this rule; in practice this means: a server can be compromised in ways that leak metadata but cannot move funds. Sources of truth: docs/TRANSACTION_INTEGRATION.md, the relevant route file's docstring, and the ReviewStep.tsx warning copy.

// app/api/remittance/build/route.ts — what we return to the client
return NextResponse.json({ xdr: transaction.toXDR() })
// We do NOT call transaction.sign(...) and we do NOT submit to Horizon.

See also: docs/TRANSACTION_INTEGRATION.md, docs/REMITTANCE_FLOW.md.

Request ID (X-Request-ID)

Every request crossing the gateway is tagged with a request ID (header X-Request-ID, internal constant API_REQUEST_ID_HEADER). middleware.ts either adopts a valid client-supplied value or generates a fresh one with generateRequestId() from lib/requestId.ts, then echoes it on the response and threads it through every structured log line. Always include it in bug reports — the value is surfaced in error toasts (Toast.tsx → diagnostics.requestId) and pasted from the dev mode panel at the bottom-left of any page when ?dev=1 is appended to the URL.

curl -i http://localhost:3000/api/health \
  -H 'X-Request-ID: support-ticket-42'
# Response headers include:
#   X-Request-ID: support-ticket-42
# Server logs for this request are filterable by requestId="support-ticket-42".

See also: docs/logging.md, docs/infrastructure.md.

Sentry tunnel (/monitoring)

The Sentry tunnel is the URL path that all Sentry browser requests are proxied through so they bypass ad blockers: tunnelRoute: "/monitoring" is configured in next.config.js via the @sentry/nextjs webpack plugin and all *.sentry.io requests are rewritten to /monitoring?…. If you add a new environment that talks to Sentry directly, do not route through the canonical *.sentry.io hosts in the browser — your events will be silently dropped by uBlock / Brave Shields / corporate DNS blockers. The same next.config.js flag also turns on hideSourceMaps: true so source maps do not leak to the browser bundle.

// next.config.js (excerpt)
const sentryWebpackPluginOptions = {
  org: process.env.SENTRY_ORG,
  project: process.env.SENTRY_PROJECT,
  authToken: process.env.SENTRY_AUTH_TOKEN,
  silent: !process.env.CI,
  tunnelRoute: "/monitoring",
  hideSourceMaps: true,
  disableLogger: true,
}

See also: README.md → "Sentry".

SEP-1 / SEP-38

Stellar Ecosystem Proposals are the formal specs wallets and integrations implement against the network. RemitWise supports two of them in production:

  • SEP-1 — Stellar TOML. Served from /api/.well-known/stellar.toml with the required Access-Control-Allow-Origin: * header so wallets can discover our anchor / signing key / endpoints.
  • SEP-38 — Anchor quote API. When ANCHOR_API_BASE_URL is set, /api/remittance/quote delegates to the anchor's GET /quote to return a compliant price quote for a (sell_asset, buy_asset, amount) triple.
# /.well-known/stellar.toml (excerpt)
VERSION = "2.0.0"
NETWORK_PASSPHRASE = "Test SDF Network ; September 2015"
SIGNING_KEY = "G...SIGNING_KEY"
TRANSFER_SERVER = "https://anchor.example.com"
WEB_AUTH_ENDPOINT = "https://auth.example.com/auth"

See also: lib/contracts/network-resolution.ts, docs/REMITTANCE_FLOW.md.

Smart Money Split

The Smart Money Split ("Split") is the percentage-based auto-allocation of an incoming remittance across the four RemitWise buckets — Spending, Savings, Bills, and Insurance. Until per-user overrides ship, the in-repo default is DEFAULT_SPLIT_CONFIG = { spending: 50, savings: 30, bills: 15, insurance: 5 } (must sum to 100) from lib/remittance/split.ts; once the Soroban remittance_split contract is wired in, live values are read via resolveContractId('REMITTANCE_SPLIT') and the contract getter. Integer rounding can leave a 1-unit remainder per allocation, which computeAllocation absorbs into the Spending bucket to avoid silent loss for any other bucket. The frontend surfaces the planned allocation as a preview on the Send screen; on confirmation it returns the unsigned XDR using the same manageData pattern as bills/insurance.

import { DEFAULT_SPLIT_CONFIG, computeAllocation } from '@/lib/remittance/split'
const amounts = computeAllocation(1000, DEFAULT_SPLIT_CONFIG)
// → { spending: 500, savings: 300, bills: 150, insurance: 50 }

See also: docs/CONTRACT_INTEGRATION.md, docs/REMITTANCE_FLOW.md.

Soroban

Soroban is Stellar's smart-contract platform. In the RemitWise frontend, "the Soroban client" is the server-only SorobanRpc.Server instance returned by getServer() from lib/soroban/client.ts — that is the canonical client. lib/soroban-client.ts still exists but is deprecated and bundled into the browser via NEXT_PUBLIC_SOROBAN_RPC_URL; never import it in new server code. RPC URL and network passphrase resolve from SOROBAN_RPC_URL and getSorobanNetworkPassphrase() respectively; contract IDs resolve from per-network env vars (REMITTANCE_SPLIT_CONTRACT_ID_TESTNET, etc.) via resolveContractId('REMITTANCE_SPLIT').

// lib/soroban/client.ts (real shape)
import { SorobanRpc } from '@stellar/stellar-sdk'

export function getServer(): SorobanRpc.Server {
  if (!_server) {
    const rpcUrl = process.env.SOROBAN_RPC_URL ?? 'https://soroban-testnet.stellar.org'
    _server = new SorobanRpc.Server(rpcUrl, {
      allowHttp: rpcUrl.startsWith('http://'), // only allow plain HTTP for local dev
    })
  }
  return _server
}

See also: docs/CACHE_STRATEGY.md, lib/contracts/README.md.

v1 namespace

The v1 namespace is the URL-prefix versioning convention for all externally consumed API routes — /api/v1/remittance/history, /api/v1/bills/[id]/pay, /api/v1/insurance/[id]/deactivate, etc. New versioned routes live under app/api/v1/. The legacy /api/* paths are rewritten to /api/v1/* at the Next.js layer (next.config.js) so existing callers keep working unchanged. When a new major version is cut (v2), the rewrite flips and v1 stays published for ≥6 months for migration.

# Both URLs reach the same handler today.
curl http://localhost:3000/api/remittance/history
curl http://localhost:3000/api/v1/remittance/history

See also: next.config.jsrewrites, docs/architecture.md → "API Routes".

XDR

XDR (External Data Representation) is Stellar's base64-encoded binary format for transactions and operation sets. Contract-mutating handlers in RemitWise build an unsigned XDR server-side, return it as { xdr: string }, and expect the client to pipe it into the wallet (signTransaction(xdr)) before submission. Building helpers live in lib/contracts/*.ts and follow the same shape: TransactionBuilderaddOperation(...)setTimeout(30)build()tx.toXDR(). Type bytes are documented in Stellar's XDR reference.

import { TransactionBuilder, Operation, Networks } from '@stellar/stellar-sdk'

const tx = new TransactionBuilder(sourceAccount, {
  fee: '100',
  networkPassphrase: Networks.TESTNET,
})
  .addOperation(Operation.payment({ destination, asset, amount }))
  .setTimeout(30)
  .build()
return { xdr: tx.toXDR() }

See also: docs/TRANSACTION_INTEGRATION.md, docs/api/savings-goals-transactions.md.