Sandbox mode lets contributors run the primary Trellis payment workflow
(create → sign → submit → status → refund) locally against deterministic fake
adapters — no production credentials, no real wallets, no network calls, and no
irreversible records.
It is opt-in via SANDBOX_MODE=true and is refused when
NODE_ENV=production.
| External dependency | Sandbox stand-in |
|---|---|
| Payment / chain backend (Stellar Horizon, Grantfox) | SandboxPaymentAdapter — implements the same IPaymentProcessor contract, serves canned fixtures |
| Ids and transaction hashes | Seeded SHA-256 — the same request always returns the same value |
| Failure modes | Named fixtures for success, failed payment, insufficient funds and a transient timeout |
The payment-processor plugin system is the seam: the sandbox adapter is
registered in the shared PaymentProcessorRegistry only while sandbox mode is
on (see SandboxProcessorRegistrar). Production never lists or routes to it.
# 1. Install deps (once)
npm install
# 2. Run the sandbox workflow demo — needs no .env and no credentials
npm run sandbox:demo
# 3. Run the sandbox test suite
npm test -- src/sandboxnpm run sandbox:demo runs the full lifecycle and prints a JSON trace for the
success, insufficient_funds and horizon_timeout scenarios. It does not
boot the Nest app, so it does not need PostgreSQL, Redis or any secret.
Add to your local .env:
SANDBOX_MODE=true
# Optional: default scenario and the seed that makes ids reproducible
SANDBOX_SCENARIO=success
SANDBOX_SEED=trellis-local-sandboxThen start the server and pin the processor per request:
npm run start:dev
curl -X POST http://localhost:3000/api/v1/payments \
-H 'Authorization: Bearer <jwt>' \
-H 'X-Payment-Processor: sandbox' \
-H 'Content-Type: application/json' \
-d '{"amount":"10","currency":"XLM","destination":"GBSANDBOX","idempotencyKey":"sandbox-1"}'You can also set PAYMENTS_DEFAULT_PROCESSOR=sandbox. The sandbox processor is
never selected implicitly while other processors are enabled — select it
explicitly so it can never be reached by accident.
All variables are optional and validated in src/config/env.validation.ts.
| Variable | Default | Purpose |
|---|---|---|
SANDBOX_MODE |
false |
Master switch. Must be exactly true to enable. |
SANDBOX_SCENARIO |
success |
Fixture used when a request does not name one. |
SANDBOX_SEED |
trellis-local-sandbox |
Seed for all generated ids/hashes. Change it to get a fresh id space. |
A request can override the scenario per call with
metadata.sandboxScenario on POST /payments.
| Scenario | Fails at | Behaviour |
|---|---|---|
success |
— | CONFIRMED on submit, full REFUNDED refund. |
payment_failed |
refund |
Submission settles FAILED; refunding raises 400. |
insufficient_funds |
create |
Creation raises 400 with a fixed balance message. |
horizon_timeout |
submit |
Submission raises a retryable 503. |
Fixtures live in src/sandbox/fixtures/sandbox.fixtures.ts and are the single
place to add a new scenario.
- Every id, payload and hash is derived from
SANDBOX_SEEDplus stable request fields (scenario, idempotency key, payment id) via SHA-256. - Calling any sandbox method twice with the same input returns the same value.
- Sandbox ids are prefixed
sbx:<scenario>:<kind>:<hex>so they are obviously fake and can never be mistaken for a Stellar hash or a UUID. They must never be sent to a real network. - The scenario travels inside the unsigned/signed payloads, so the stateless create → sign → submit HTTP flow keeps its scenario across requests, exactly like a real XDR round-trips.
src/sandbox/adapters/sandbox-payment.adapter.spec.ts asserts determinism, the
full lifecycle, every failure scenario, and that the adapter works with only
SANDBOX_MODE set.
- Sandbox mode covers the payment workflow only. Booting the full API still
requires
DATABASE_URL,JWT_SECRETandREDIS_URL; the sandbox demo and tests are intentionally standalone and need none of them. - Fixture state is in-memory and resets on restart; nothing is persisted and no ledger record is ever written.
- The sandbox processor is registered only while
SANDBOX_MODE=true, and never whenNODE_ENV=production. Every adapter entry point also fails closed with412 Precondition Failedif the flag is missing. - This is not a replacement for the real-network suite. Use
npm run test:testnetwhen you need to validate against Stellar testnet. - Sandbox ids are not valid ledger hashes by design; do not assert on their
shape beyond the
sbx:prefix.
- Add the value to
SandboxScenarioinsrc/sandbox/sandbox.scenarios.ts. - Add the matching fixture entry to
SANDBOX_FIXTURESinsrc/sandbox/fixtures/sandbox.fixtures.ts. - Cover it in
src/sandbox/adapters/sandbox-payment.adapter.spec.ts.
No adapter code changes are needed — the adapter is a thin interpreter over the fixture table.