Skip to content

Commit aa74251

Browse files
authored
Merge branch 'staging' into feature/508-approve-recovery-admin-action
2 parents 54642b8 + c169439 commit aa74251

284 files changed

Lines changed: 30774 additions & 7813 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ WALLET_ENCRYPTION_KEY=your-secret-encryption-key-min-32-chars
3131
# Mainnet: https://horizon.stellar.org
3232
# ------------------------------------------------------------
3333
STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
34+
STELLAR_NETWORK=TESTNET
35+
STELLAR_HORIZON_MAX_RETRIES=3
36+
STELLAR_HORIZON_RETRY_BACKOFF_MS=500
37+
STELLAR_HORIZON_RETRY_JITTER_MS=250
38+
BALANCE_STALE_THRESHOLD_MS=300000 # 5 minutes
39+
# Webhook Configuration
3440

3541
# ------------------------------------------------------------
3642
# Balance Indexer

.github/workflows/ci.yml

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- staging
8+
pull_request:
9+
branches:
10+
- main
11+
- staging
12+
13+
jobs:
14+
test:
15+
name: Build and test
16+
runs-on: ubuntu-latest
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Setup Node.js
23+
uses: actions/setup-node@v4
24+
with:
25+
node-version: '22'
26+
27+
- name: Setup pnpm
28+
uses: pnpm/action-setup@v4
29+
with:
30+
version: 9
31+
32+
- name: Setup Node.js (enable pnpm cache)
33+
uses: actions/setup-node@v4
34+
with:
35+
node-version: '22'
36+
cache: 'pnpm'
37+
38+
- name: Install dependencies
39+
run: pnpm install --frozen-lockfile
40+
41+
- name: Generate Prisma client
42+
run: pnpm prisma:generate
43+
44+
- name: Build
45+
run: pnpm run build
46+
47+
- name: Test
48+
run: pnpm test

.github/workflows/migrate.yml

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,12 @@ on:
88
paths:
99
- 'prisma/migrations/**'
1010
- 'prisma/schema.prisma'
11+
- '.github/workflows/migrate.yml'
1112
pull_request:
1213
paths:
1314
- 'prisma/migrations/**'
1415
- 'prisma/schema.prisma'
16+
- '.github/workflows/migrate.yml'
1517

1618
jobs:
1719
migrate:
@@ -40,12 +42,17 @@ jobs:
4042
- name: Checkout
4143
uses: actions/checkout@v4
4244

45+
- name: Setup Node.js
46+
uses: actions/setup-node@v4
47+
with:
48+
node-version: '22'
49+
4350
- name: Setup pnpm
4451
uses: pnpm/action-setup@v4
4552
with:
46-
version: latest
53+
version: 9
4754

48-
- name: Setup Node.js
55+
- name: Setup Node.js (enable pnpm cache)
4956
uses: actions/setup-node@v4
5057
with:
5158
node-version: '22'

README.md

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ It handles wallet creation, transaction orchestration, fee sponsorship, and on-c
3333

3434
## API Endpoints
3535

36+
All routes below are served under the `/v1` prefix (e.g. `GET /v1/health`). See [docs/API-VERSIONING.md](docs/API-VERSIONING.md) for the versioning strategy.
37+
3638
### Health & Monitoring
3739

3840
#### `GET /health`
@@ -391,8 +393,93 @@ The middleware is registered in `src/main.ts` and runs for all incoming requests
391393

392394
---
393395

396+
## Balance Indexer
397+
398+
The balance indexer provides fast, cached balance reads without hitting Stellar Horizon on every request.
399+
400+
### Architecture
401+
402+
```
403+
┌─────────────────────────────────────────────────────────┐
404+
│ BalanceIndexerService │
405+
│ │
406+
│ getBalance() → cached read from DB │
407+
│ getAllBalances() → cached reads from DB │
408+
│ syncWalletBalances() → fetch Horizon → upsert DB │
409+
│ reconcileBalance() → compare DB vs Horizon │
410+
│ reconcileAllBalances()→ full sweep across active wallets│
411+
│ syncAllWallets() → manual full sync trigger │
412+
└──────────┬──────────────────────┬───────────────────────┘
413+
│ │
414+
┌────────▼────────┐ ┌────────▼──────────────┐
415+
│ PrismaService │ │ StellarHorizonService │
416+
│ (PostgreSQL) │ │ (Horizon REST API) │
417+
└─────────────────┘ └────────────────────────┘
418+
```
419+
420+
### Stale Detection
421+
422+
Balances older than `BALANCE_STALE_THRESHOLD_MS` (default 5 minutes) trigger an async background refresh on the next read. The stale value is still returned immediately so callers are never blocked.
423+
424+
### Mismatch Handling
425+
426+
On reconciliation, if the indexed balance differs from the on-chain balance, the indexed value is corrected and `mismatchDetectedAt` / `reconciliationAttempts` are updated for observability.
427+
428+
### Sync Job Tracking
429+
430+
All sync and reconciliation operations create a `BalanceSyncJob` record for audit and observability.
431+
432+
### API Endpoints
433+
434+
| Method | Path | Description |
435+
|--------|------|-------------|
436+
| `GET` | `/balances/wallet/:walletId` | Get cached balances (add `?assetType=NATIVE` for single asset) |
437+
| `POST` | `/balances/wallet/:walletId/sync` | Manually trigger sync for a single wallet |
438+
| `POST` | `/balances/sync-all` | Manually trigger full sync for all active wallets (admin) |
439+
| `POST` | `/balances/wallet/:walletId/reconcile` | Reconcile wallet balance with on-chain state |
440+
| `POST` | `/balances/reconcile-all` | Reconcile all balances (admin) |
441+
442+
### Environment Variables
443+
444+
| Variable | Default | Description |
445+
|----------|---------|-------------|
446+
| `BALANCE_STALE_THRESHOLD_MS` | `300000` | Age (ms) after which a balance is considered stale |
447+
| `STELLAR_HORIZON_URL` | `https://horizon-testnet.stellar.org` | Stellar Horizon API URL |
448+
449+
---
450+
451+
## Webhooks
452+
453+
Webhooks allow your application to receive real-time notifications when events occur in Mux Protocol.
454+
455+
### Endpoint CRUD
456+
457+
| Method | Path | Description |
458+
|--------|------|-------------|
459+
| `POST` | `/webhooks/endpoints` | Register a new webhook endpoint |
460+
| `GET` | `/webhooks/endpoints/project/:projectId` | List endpoints for a project |
461+
| `GET` | `/webhooks/endpoints/:id` | Get a specific endpoint |
462+
| `PUT` | `/webhooks/endpoints/:id` | Update an endpoint |
463+
| `DELETE` | `/webhooks/endpoints/:id` | Delete an endpoint |
464+
| `POST` | `/webhooks/endpoints/:id/rotate-secret` | Rotate signing secret |
465+
| `GET` | `/webhooks/endpoints/:id/deliveries` | Get delivery history |
466+
| `POST` | `/webhooks/process-deliveries` | Manually process pending deliveries (admin) |
467+
468+
### Payload Signing
469+
470+
All webhook payloads are signed with HMAC-SHA256. The `X-Webhook-Signature` header has format `t=<timestamp>,v1=<signature>`. Verify with the secret returned at endpoint creation.
471+
472+
### Supported Events
473+
474+
`wallet.created`, `wallet.activated`, `wallet.suspended`, `wallet.rotated`, `transaction.created`, `transaction.pending`, `transaction.confirmed`, `transaction.failed`, `balance.updated`, `balance.low`, `user.created`, `user.updated`
475+
476+
---
477+
394478
## Wallets API
395479

480+
Endpoint semantics, idempotency, lifecycle events, dependency retries, and
481+
metrics are documented in [docs/WALLET-API.md](docs/WALLET-API.md).
482+
396483
- `POST /wallets` - create wallet
397484
- `GET /wallets` - list all wallets
398485
- `GET /wallets/user/:userId` - list wallets by userId (#189)
@@ -433,4 +520,3 @@ Testing
433520

434521
- Unit tests are under `src/**/*spec.ts`.
435522
- E2E tests are under `test/` and use Jest + Supertest.
436-

TEST_VERIFICATION_GUIDE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ These test files were updated to use the new `/v1` prefix:
3333

3434
4. **test/wallets.e2e-spec.ts**
3535
- Tests wallet endpoint: `GET /v1/wallets/protected`
36+
- Tests wallet creation and wallet status paths
37+
- Verifies `x-request-id` propagation in headers
3638
- Verifies API key authentication with prefix
3739

3840
### New Test File

docs/API-VERSIONING.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# API Versioning Strategy
2+
3+
This document describes how the Mux backend versions its public HTTP API.
4+
5+
## Current approach: URI path versioning
6+
7+
All routes are served under a global `/v1` prefix, applied once in `src/main.ts`:
8+
9+
```ts
10+
app.setGlobalPrefix('v1');
11+
```
12+
13+
Individual controllers (e.g. `@Controller('auth')`, `@Controller('wallets')`)
14+
declare their resource path only; the version prefix is applied globally so
15+
every route is automatically namespaced (`/v1/auth/authenticate`,
16+
`/v1/wallets`, etc.). Requests made without the `/v1` prefix return `404 Not
17+
Found` — there is no unversioned fallback.
18+
19+
## Why URI versioning
20+
21+
- **Explicit and cache-friendly**: the version is visible in the URL, in logs,
22+
and in reverse-proxy/CDN routing rules, without relying on a header that
23+
intermediaries may strip.
24+
- **Simple for consumers**: partners and the frontend hard-code a base URL
25+
(e.g. `https://api.mux.dev/v1`) rather than needing to set a custom header
26+
on every request.
27+
- **Matches existing NestJS conventions** in this repo — a single
28+
`setGlobalPrefix` call versions every controller without per-route
29+
decorators.
30+
31+
## Introducing a breaking change (`/v2`)
32+
33+
When a change is not backwards compatible:
34+
35+
1. Add the new/changed controllers under a `v2` path (NestJS's built-in
36+
[URI versioning](https://docs.nestjs.com/techniques/versioning) via
37+
`app.enableVersioning({ type: VersioningType.URI })` can be adopted at that
38+
point to run `v1` and `v2` controllers side by side).
39+
2. Keep `/v1` serving the previous behavior until consumers have migrated.
40+
3. Announce the deprecation window for `/v1` in release notes before removal.
41+
42+
## Non-breaking changes
43+
44+
Additive changes (new endpoints, new optional request/response fields) ship
45+
directly under the current `/v1` prefix — no new version is required.
46+
47+
## Health and monitoring endpoints
48+
49+
`/v1/health` and `/v1/ready` follow the same prefix as every other route, so
50+
uptime checks and readiness probes must be configured with the `/v1` path.

docs/AUTH-FEATURE-FLAGS.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Auth Feature Flags
2+
3+
This document summarizes feature flags added for the auth and session endpoints.
4+
5+
- `FEATURE_AUTH_API` (boolean, default: false)
6+
- When `true`, the auth endpoints (`POST /auth/authenticate`, `GET /auth/sessions`, `GET /auth/validate/:authId`) are enabled.
7+
- When `false` or unset, the endpoints return HTTP 403 (Forbidden) with message: "Feature is not available at this time. (Flag: auth_api)".
8+
9+
Notes:
10+
- The flag is implemented via the existing `FeatureFlagGuard` and the `@FeatureFlag('auth_api')` decorator on the `AuthOrchestratorController`.
11+
- The guard reads environment variables using the existing pattern: `FEATURE_<FLAG_NAME>=true|false` (e.g. `FEATURE_AUTH_API=true`).
12+
- Existing unit tests for `FeatureFlagGuard` cover enabled/disabled behavior. The auth controller tests were adjusted to override the guard for isolation.
13+
14+
Operational guidance:
15+
- To enable auth in runtime, set `FEATURE_AUTH_API=true` in the configuration used by the service (env, k8s secret, etc.).
16+
- Ensure any API gateway or routing changes are coordinated when toggling this flag in production to avoid unexpected client errors.

docs/WALLET-API.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Wallet API behavior
2+
3+
All Wallet API routes require a valid API key and are rate-limited. The API
4+
never returns encrypted key material on read endpoints. The only operation
5+
that returns a `privateKey` is a successful first wallet-creation response;
6+
clients must consume it immediately and must not expect it to be replayed.
7+
8+
## Endpoints
9+
10+
| Method | Route | Behavior |
11+
| --- | --- | --- |
12+
| `POST` | `/wallets` | Creates one active wallet per user/network pair. Duplicate user/network requests return `409`. |
13+
| `GET` | `/wallets` | Lists wallets. Supports `userId`, `network`, `status` filters and `limit`/`offset` pagination (default `limit=20`, max `100`). Returns `{ data, total, limit, offset, hasMore }`. |
14+
| `GET` | `/wallets/:id` | Returns a wallet or `404`. |
15+
| `GET` | `/wallets/:id/status` | Returns lifecycle status without decrypting the private key. |
16+
| `PATCH` | `/wallets/:id` | Updates wallet lifecycle status. |
17+
| `PATCH` | `/wallets/:id/activate` | Activates a `PROVISIONING` wallet. Any other current state is rejected. |
18+
| `DELETE` | `/wallets/:id` | Removes a wallet record. |
19+
| `POST` | `/wallets/orchestration/create` | Runs the provisioning flow and accepts an optional `idempotencyKey`. |
20+
| `GET` | `/wallets/orchestration/user/:userId/:network` | Returns the wallet for a user/network pair or `404`. |
21+
| `GET` | `/wallets/orchestration/validate/:userId/:network` | Reports whether a new wallet may be created. |
22+
23+
`network` is `TESTNET` or `MAINNET`. `POST /wallets/orchestration/create`
24+
creates a wallet as `PROVISIONING`, then promotes it to `ACTIVE` in the same
25+
database transaction. Testnet funding is best effort: a disconnected or
26+
failed Friendbot call is logged and does not undo a committed wallet.
27+
28+
## Idempotency
29+
30+
For orchestration creation, an `idempotencyKey` is scoped to one
31+
`userId`/`network` operation for 24 hours.
32+
33+
- Repeating the same operation returns the cached wallet result with
34+
`privateKey: ""`.
35+
- Reusing the key for another user or network returns `409`.
36+
- Expired keys are treated as new requests.
37+
38+
## Lifecycle events
39+
40+
The API emits webhook domain events after state has been durably persisted:
41+
`wallet.created`, `wallet.activated`, `wallet.suspended`, and
42+
`wallet.rotated`. Event dispatch is asynchronous; a webhook outage is logged
43+
but never changes the response or rolls back wallet state. Creation events
44+
from the orchestration endpoint are emitted only after its database
45+
transaction commits, and are not repeated for idempotency replays.
46+
47+
## Dependency retries and metrics
48+
49+
Before any wallet write, transient key-management and testnet-funding failures
50+
are retried with capped exponential backoff. Invalid requests and non-transient
51+
4xx responses are not retried. Configure this behavior with:
52+
53+
| Variable | Default |
54+
| --- | --- |
55+
| `WALLET_API_RETRY_MAX_ATTEMPTS` | `3` |
56+
| `WALLET_API_RETRY_BASE_DELAY_MS` | `100` |
57+
| `WALLET_API_RETRY_MAX_DELAY_MS` | `2000` |
58+
59+
Wallet operations write structured `[wallet-api-metrics]` log records with
60+
operation, outcome, duration, and network. Metrics intentionally exclude user
61+
and wallet identifiers so they are safe to aggregate as low-cardinality
62+
telemetry.

0 commit comments

Comments
 (0)