Backend SDK for ephemeral Stellar account management
MVP Stubs
🚧 MVP — Active Development: encryptSecret() — base64, not real encryption, must be replaced before any production deployment 🚧 The expiresIn → expiry_ledger conversion — needs verification or explicit documentation of where it happens 🚧 Webhook coverage gaps..
The Bridgelet SDK is a NestJS-based backend service that manages the lifecycle of ephemeral Stellar accounts. It handles account creation, claim authentication, webhook notifications, and integration with the bridgelet-core smart contracts.
PLEASE READ THIS SECTION BEFORE DEVELOPMENT
The following services/imports are currently commented out to allow npm run start:dev to run without errors. These are NOT removed and MUST be restored once proper implementations exist.
-
Search the codebase for comments containing
TEMPORARY:to locate all commented-out code that needs restoration.. -
Every
TEMPORARY:comment MUST reference a linked GitHub issue using the formatTEMPORARY: <reason> (issue #NNN). This is enforced by CI (see.github/workflows/ci.yml), so a temporary workaround can never silently become permanent. Track restoration in the linked issue. There are currently zeroTEMPORARY:comments in the codebase (issue #458). -
Secret Encryption (
src/modules/accounts/accounts.service.ts)- Current: Base64 encoding (NOT encryption)
- Impact: Ephemeral secret keys are not protected at rest
- Required: AES-256-GCM or KMS-backed encryption before any deployment with real funds
-
Ledger Expiry Conversion
CreateAccountDto.expiresInis converted to an absoluteexpiresAtDate and then mapped to the contractexpiry_ledgerduring initialization.expiresAtis the API-level source of truth; the ledger number is derived from that deadline at the time the account is initialized.- Conversion formula:
expiry_ledger = current_ledger + ceil(remaining_seconds / 5) + 10
-
Sweep Authorization Signature (
src/modules/sweeps/providers/contract.provider.ts)- Current:
generateAuthSignature()produces a fake 64-byte stub signature - Works because:
EphemeralAccount.verify_sweep_authorization()inbridgelet-coreis also a stub that accepts any signature (documented in bridgelet-core README) - Impact: Sweep authorization is not cryptographically enforced in development
- Guard: Method throws if called outside
developmentortestenvironments - Required: Real Ed25519 signing against the
SweepController'sauthorized_signeroncebridgelet-coreimplements real verification.
- Current:
This is a temporary stabilization to enable local development and onboarding until missing implementations are complete. No code was deleted - all logic remains in place as comments.
- Framework: NestJS (Node.js + TypeScript)
- Database: PostgreSQL
- ORM: TypeORM
- Blockchain: Stellar SDK + Soroban RPC
- API: REST api
@stellar/stellar-sdk is pinned to an exact version (14.6.1) in package.json — no caret or tilde range — for the following reasons:
- The SDK exposes raw Stellar XDR and Soroban RPC types. A minor or patch bump can change serialization behaviour, breaking transaction building or contract call encoding in ways that are difficult to detect without end-to-end tests against a live network.
- Exact pinning makes the dependency tree fully reproducible across developer machines and CI without relying on lock-file-only guarantees.
Upgrade process:
- Update the version in
package.jsonto the new exact version. - Run
npm installto updatepackage-lock.json. - Run the full test suite:
npm test. - Manually test account creation, sweep, and expiry flows against testnet before merging.
- Only promote to production after all testnet checks pass.
- Account lifecycle management (create, claim, expire)
- Claim authentication via signed tokens
- Webhook system for payment events
- Integration with bridgelet-core contracts
- Admin dashboard API endpoints
src/
├── modules/
│ ├── accounts/ # Ephemeral account management
│ ├── claims/ # Claim authentication & processing
│ ├── sweeps/ # Fund sweep orchestration
│ ├── webhooks/ # Event notification system
│ └── stellar/ # Stellar/Soroban integration
├── common/
│ ├── guards/ # Auth guards
│ ├── interceptors/ # Logging, transform
│ └── filters/ # Exception filters
├── config/ # Environment configuration
└── database/ # Migrations, entities
scripts/
└── generate-migrations.sh # Regenerates src/database/migrations/ from scratch
# Install dependencies
npm install
# Setup environment
cp .env.example .env
# Edit .env with your configuration
# Run database migrations
# DataSource config : src/config/typeorm.config.ts
# Migrations : src/database/migrations/
# 1718100000000-CreateAccountsTable (accounts table, status enum, expiredAt, metadata)
# 1718100001000-CreateClaimsTable (claims table + FK to accounts)
# 1718100002000-AddInitializingToAccountStatus (adds INITIALIZING to account_status enum)
# 1718100003000-CreateWebhooksTable (webhooks table)
# 1718100004000-AddClaimingToAccountStatus (adds CLAIMING to account_status enum)
# 1718100005000-CreateWebhookDeliveriesTable (webhook delivery attempt log table)
# 1718100006000-AddHighTrafficIndexes (accounts composite and range indexes)
# 1718100007000-CreateContractEventsTable (Soroban contract event index table)
# 1718100008000-AddDeletedAtToAccountsTable (soft-delete column + index on accounts)
# 1718100008000-AddPartialSweepToAccountStatus (adds PARTIAL_SWEEP to account_status enum)
# 1718100008000-CreateClaimAuditLogTable (claim attempt audit log table)
npm run migration:run
# Start development server
npm run start:devsrc/database/migrations/ is scripted rather than hand-maintained. scripts/generate-migrations.sh deletes the folder and rewrites every file listed above verbatim, so the folder on disk is always reproducible from the script instead of relying on files someone created by hand.
# Recreate src/database/migrations/ from the script (prompts for confirmation)
./scripts/generate-migrations.sh
# Same, but skip the confirmation prompt (useful in CI)
./scripts/generate-migrations.sh --yesThis does not apply migrations to a database — it only (re)writes the .ts files. Run npm run migration:run afterwards as usual. See CONTRIBUTING.md for the workflow to follow when adding a new migration.
npm test
## or to run specific tests
npm test -- test_Service_File_Name
## e.g
npm test -- sweeps.service.spec.tsRun the full coverage report (enforces 80% minimum threshold):
npm run test:covCoverage reports are generated in the coverage/ directory. The build will fail if any metric (branches, functions, lines, statements) falls below 80%.
Generate API keys for the concurrent account load test:
npm run load:accounts:seed -- 50Then run the burst test against a locally running server:
npm run load:accountsThis test sends 50 concurrent POST /accounts requests using unique integrator API keys and verifies the endpoint remains responsive under burst load.
The claim-redemption (sweep) path — a much heavier operation that triggers
on-chain sweep transactions — has its own burst test. Seed ephemeral accounts
in PENDING_CLAIM with valid claim tokens:
npm run load:claims:seed -- 50Then run the burst test against a locally running server:
npm run load:claimsThis sends 50 concurrent POST /claims/redeem requests and reports the
success/failure rate and latency distribution. Note that POST /claims/redeem
is throttled at 5 requests/min/IP, so simultaneous arrivals exercise the
throttle as well as the reconcile/locking path under burst; for a pure
un-throttled sweep burst run the requests from distinct clients (or disable the
throttle in a load-test profile).
The repository includes a local sandbox integration test for the Bridgelet contract flow in test/accounts-local-sandbox.e2e-spec.ts.
Set the required Stellar environment variables and run:
npm run test:local-sandboxRequired env vars:
STELLAR_HORIZON_URLSTELLAR_SOROBAN_RPC_URLEPHEMERAL_ACCOUNT_CONTRACT_IDSWEEP_CONTROLLER_CONTRACT_IDFUNDING_ACCOUNT_SECRETRECOVERY_ACCOUNT_PUBLICSWEEP_SIGNING_KEY_SEEDSTELLAR_LOCAL_SANDBOX=true
The repository also includes an embedded-Postgres integration test in src/database/migrations.integration.spec.ts that starts a fresh PostgreSQL instance, runs all current migrations, verifies the resulting schema matches the TypeORM entities, checks the account_status_enum values, confirms the claims.accountId -> accounts.id and webhook_deliveries.subscription_id -> webhooks.id foreign keys are enforced, verifies the high-traffic accounts indexes added by migration 1718100006000, and verifies the contract_events table shape.
To check coverage for a specific file:
npm test -- sweeps.service.spec.ts --coverage# Database
DATABASE_HOST=localhost
DATABASE_PORT=5432
DATABASE_NAME=bridgelet
DATABASE_USER=postgres
DATABASE_PASSWORD=postgres
# Stellar
STELLAR_NETWORK=testnet
STELLAR_HORIZON_URL=https://horizon-testnet.stellar.org
STELLAR_SOROBAN_RPC_URL=https://soroban-testnet.stellar.org
# Security
JWT_SECRET=your-secret-key
CLAIM_TOKEN_EXPIRY=2592000 # 30 days
API_RATE_LIMIT=100 # General API rate limit (requests/minute)
# Scheduler
INITIALIZING_TIMEOUT_MS=600000 # INITIALIZING -> FAILED cleanup timeout
SWEEP_RECONCILIATION_TIMEOUT_MS=600000 # Stale CLAIMING -> PARTIAL_SWEEP reconciliation (#475)
# Stellar funding / sweep secrets (high privilege — see Deployment Guide)
FUNDING_ACCOUNT_SECRET=...
RECOVERY_ACCOUNT_PUBLIC=G...
SWEEP_SIGNING_KEY_SEED=...
ENCRYPTION_KEY=your-64-char-hex-string
# Application
PORT=3000
NODE_ENV=development
# Swagger UI (/api/docs) — disabled by default in production. Set to 'true' to expose it.
# Never enable in production unless the endpoint is behind authentication.
ENABLE_SWAGGER=falseHigh-privilege secrets:
FUNDING_ACCOUNT_SECRET,SWEEP_SIGNING_KEY_SEED,JWT_SECRET, andENCRYPTION_KEYcontrol accounts that can move or authorise real funds. In production these must be supplied from a secrets manager (AWS Secrets Manager / SSM, Vault, KMS-backed injection, etc.) — not a plain.envvalue — and must never be committed or logged. See the Deployment Guide for secure storage, rotation, and balance-monitoring guidance for the funding account.
JWT_SECRETmust be a strong, random value of at least 32 characters. In non-development environments (NODE_ENV=production,staging, etc.) the application refuses to start ifJWT_SECRETis empty, shorter than 32 characters, or a known placeholder such asyour-secret-key.
Once running, access API docs at:
- Swagger:
http://localhost:3000/api/docs
Production note: the Swagger UI (
/api/docs) is disabled by default whenNODE_ENV=production. A publicly reachable Swagger UI would expose the full REST surface, request/response schemas and potential internal field names to anyone who finds the URL. To opt in explicitly (e.g. for a private, authenticated staging deployment) setENABLE_SWAGGER=true.
POST /accounts # Create ephemeral account (also generates the claim token) GET /accounts/:id # Get account details POST /claims/initiate # Generate claim token POST /claims/redeem # Redeem claim and sweep GET /webhooks # List webhook subscriptions POST /webhooks # Subscribe to events PUT /webhooks/:id # Update webhook subscription (e.g. URL, events) DELETE /webhooks/:id # Delete webhook subscription
POST /accounts accepts an optional metadata JSON object for integration
bookkeeping (e.g. userId, orderId). Constraints:
- Must be a plain JSON object (arrays and primitives are rejected).
- Must serialise to at most 4 KB (4096 bytes) — larger payloads are rejected.
- Any top-level key matching a known PII identifier (
email,phone,name,address,ssn,dob,passport,taxid, etc.) is stripped before storage. PII is never persisted.
Validation is applied on write only, so existing metadata is unaffected retroactively.
Claim tokens gate real fund movement, so the endpoints that generate and
consume them are rate-limited per API key AND per IP (see the
getTracker configuration in src/app.module.ts), stricter than the general
API limit. Exceeding a limit returns HTTP 429 with a Retry-After header.
| Endpoint | Limit (per API key + IP) | Justification |
|---|---|---|
POST /accounts |
10 / min | Generates a claim token; limit prevents mass token generation / account-existence probing (#473) |
POST /claims/verify |
20 / min | Cheap token check |
POST /claims/redeem |
5 / min | Moves funds; aggressive limit slows token-guessing (#474) |
| General API (all routes) | 100 / min | Baseline throttle |
In addition to the redeem rate limit, repeated failed redemption attempts
against the same token raise a brute-force alert log line (see
ClaimRedemptionProvider). This is defense-in-depth layered on top of the
high-entropy claim token generation (see SECURITY_AUDIT.md), not a
substitute for it.
See Database Schema Documentation
# Run tests
npm run test
# Run e2e tests
npm run test:e2e
# Lint
npm run lint
# Format
npm run formatAll pull requests are validated automatically for branch naming and PR title format.
- During the initial rollout, checks ran in warning mode until 2026-02-27.
- Since then, enforcement is active: pull requests are blocked until naming issues are fixed (verified 2026-08-27, and the workflow below still blocks on non-conforming names/titles).
Accepted pattern:
(<conventional-type>)/brief-description
Regex used by CI:
^(fix|feature|feat|test|chore|docs|refactor|ref|hotfix|release|ci|build|revert)/[a-z0-9-]+$
Examples:
fix/jwt-error-handlingfeature/webhook-service
main and develop are exempt for release/hotfix workflows.
Accepted pattern:
<conventional-type>: Brief description
Regex used by CI:
^(fix|feature|feat|test|chore|docs|refactor|ref|hotfix|release|ci|build|revert): .+$
Examples:
fix: Handle JWT errors in TokenVerificationProvidertest: Add unit tests for ClaimLookupProviderfeature: Implement WebhooksService
Rename your local branch and push the new branch:
git branch -m fix/jwt-error-handling
git push origin -u fix/jwt-error-handlingThen update the PR to use the renamed branch. If needed, close the old PR and open a new one from the renamed branch.
Edit the PR title directly in GitHub:
- Open the pull request.
- Click the title field.
- Update it to the required format.
- Save changes.
See Deployment Guide for production setup.
Visit http://localhost:3000/api/docs for API documentation.
See Getting Started Guide for full setup instructions.
(Nest)https://nestjs.com
UNLICENSED