Run the full Moonlight stack locally: Stellar network, smart contracts, privacy provider, consoles, and dashboards.
| Tool | Install |
|---|---|
| Docker | docker.com |
Stellar CLI and Deno are auto-installed by up.sh if missing.
Clone all repos to ~/repos/:
~/repos/
├── local-dev/ # This repo (setup scripts, E2E infrastructure)
├── provider-platform/ # Privacy provider server
├── provider-console/ # Provider dashboard
├── council-platform/ # Council backend
├── council-console/ # Council dashboard
├── pay-platform/ # Moonlight Pay backend (accounts, transactions, POS)
├── moonlight-pay/ # Moonlight Pay frontend (wallet sign-in, POS checkout)
└── network-dashboard/ # Network monitoring dashboard
If your repos live somewhere other than ~/repos/, set BASE_DIR:
BASE_DIR=~/projects ./up.shYou can also override individual repo paths:
PROVIDER_PLATFORM_PATH=~/other/provider-platform ./up.shEvery port and the postgres container name are env-overridable, so you can run a second stack alongside an existing one without conflict. Setup scripts read URLs (not ports), so both must be set.
# Override file (lives outside the repo so it never gets committed)
cat > ~/my-stack.env <<'EOF'
# Postgres (private)
export PG_PORT=5542
export PG_CONTAINER=my-stack-db
# Platform service ports (used by infra-up.sh)
export PROVIDER_PORT=3110
export COUNCIL_PLATFORM_PORT=3115
export PAY_PLATFORM_PORT=3125
export PROVIDER_CONSOLE_PORT=3120
export COUNCIL_CONSOLE_PORT=3130
export MOONLIGHT_PAY_PORT=3150
export NETWORK_DASHBOARD_PORT=3140
# Setup-script URLs (used by setup-c.sh / setup-pp.sh / setup-pay.sh)
# These do NOT derive from *_PORT — they must be set explicitly.
export PROVIDER_URL=http://localhost:3110
export COUNCIL_URL=http://localhost:3115
export PAY_PLATFORM_URL=http://localhost:3125
EOF
set -a; source ~/my-stack.env; set +a
./up.sh
./setup-c.sh && ./setup-pp.sh && ./setup-pay.shPlaywright's playwright/helpers/urls.ts reads the same *_PORT env vars, so the override file works for tests too.
Stellar and Jaeger are shared between stacks. STELLAR_RPC_PORT (8000) and JAEGER_OTLP_PORT (4318) default to the same values across stacks, and infra-up.sh reuses an already-running container at those ports instead of starting a new one. That means parallel stacks share one Stellar ledger and one Jaeger trace store. To isolate them, override JAEGER_CONTAINER, JAEGER_UI_PORT, and start your own Stellar quickstart on a different STELLAR_RPC_PORT.
Full list of overrides: infra-up.sh:42-54 (ports + container names) and the jsdoc headers of setup-c.ts, setup-pp.ts, setup-pay.ts (URLs).
setup-c.sh deploys two compiled contracts that must exist before the script runs:
e2e/wasms/channel_auth_contract.wasme2e/wasms/privacy_channel.wasm
These are produced by soroban-core's release pipeline and are not checked into local-dev. Download them from the soroban-core releases page and place them under e2e/wasms/. The released WASMs are the same artifacts deployed on testnet/mainnet — they must match the release, not a local build.
local-dev cleanly separates infrastructure (long-lived services) from application setup (contracts, councils, PPs).
| Layer | Script | Owns | When to run |
|---|---|---|---|
| Keys | ./setup-keys.sh |
Deterministic keypairs for all roles (deployer, pay-admin, pay-service) | Called automatically by up.sh |
| Infra | ./infra-up.sh |
Stellar quickstart, PostgreSQL, Jaeger, platform services, consoles, Moonlight Pay | Called automatically by up.sh |
| App: Council | ./setup-c.sh |
Admin keypair, contract deploys (channel-auth, privacy-channel, XLM SAC), council registered via council-platform API | After up.sh, before any flow that needs a council |
| App: Privacy Provider | ./setup-pp.sh |
PP keypair, registered in provider-platform, joined to the council via the production join flow, on-chain add_provider |
After setup-c.sh, before any flow that needs a working PP |
| App: Pay Platform | ./setup-pay.sh |
Seeds pay-platform with council config (channels, jurisdictions, PP), funds the service keypair | After setup-pp.sh, before any POS flow |
This split exists for three reasons:
- Infra restarts shouldn't silently rebuild app state. Before this split, every
down/upcycle quietly redeployed contracts and re-funded accounts as a side effect. Nowup.shis idempotent infra and the app state is explicit. - The setup scripts exercise the production API surface.
setup-c.shandsetup-pp.shmake the same HTTP calls that council-console and provider-console make. If a platform release breaks the public surface, these scripts break too — that's the point. No DB seeding, no shortcuts. - Skipping app setup is supported. You can run
up.shalone if you only want to develop console UI or hit infra directly without a populated stack.
./up.shThis is the single entry point. It calls setup-keys.sh (generates deterministic keypairs) then infra-up.sh (starts all services). The infra script runs through 11 sections:
- Checks prerequisites (Docker, Stellar CLI, Deno) and auto-installs missing ones
- Starts Jaeger (OTLP on
:4318, UI on:16686) - Starts the Stellar quickstart container (
:8000) and waits for Friendbot - Starts PostgreSQL (
:5442) and createsprovider_platform_db,council_platform_db,pay_platform_db - Generates provider-platform
.env(infra-only), runs migrations, starts on:3010 - Generates council-platform
.env(infra-only), runs migrations, starts on:3015 - Generates pay-platform
.env(withADMIN_WALLETSandPAY_SERVICE_SKfrom keypairs), runs migrations, starts on:3025 - Builds and serves provider-console on
:3020 - Builds and serves council-console on
:3030 - Builds and serves Moonlight Pay on
:3050 - Builds and serves network-dashboard on
:3040
After up.sh finishes the services are healthy and reachable, but the protocol state is empty: no contracts deployed, no councils, no PPs. Run the setup scripts to populate it.
If you only need to restart infra without regenerating keys, run ./infra-up.sh directly.
./setup-c.shSteps (all production-like — every API call is the one council-console makes):
- Generate ephemeral admin keypair, fund via Friendbot
- Deploy Channel Auth contract → councilId
- Deploy native XLM SAC (Stellar Asset Contract)
- Deploy Privacy Channel contract → channelContractId
- Admin authenticates to council-platform via SEP-43/53 challenge → JWT
PUT /council/metadatato create the councilPOST /council/channelsto add the XLM channel- Write admin SK + contract IDs to
.local-dev-state(gitignored) forsetup-pp.shand other followups to consume
./setup-pp.shRequires setup-c.sh to have run first. Steps (also production-like):
- Load admin SK + council ID from
.local-dev-state - Generate fresh PP operator keypair, fund via Friendbot
- PP operator authenticates to provider-platform dashboard → JWT
POST /dashboard/pp/registerto register the PP- Sign a join envelope,
POST /dashboard/council/join(provider-platform forwards to council-platform) - Admin authenticates to council-platform → JWT
- List join requests, find ours,
POST /council/provider-requests/:id/approve - Admin calls
add_provideron-chain against the channel-auth contract - Poll provider-platform until the membership flips ACTIVE (event watcher sees
provider_added) - Append PP keys to
.local-dev-state
Each setup-pp.sh run registers a fresh PP. Re-running adds a second PP to the same council (multi-PP). To reset: down.sh → up.sh → setup-c.sh → setup-pp.sh.
./setup-pay.shRequires setup-c.sh and setup-pp.sh to have run first. Steps:
- Load PAY_ADMIN keypair from
.local-dev-keys - Load council + PP info from
.local-dev-state - Fund PAY_ADMIN via Friendbot
- PAY_ADMIN authenticates to pay-platform → JWT
- Create council with channels (XLM) and jurisdictions via
POST /admin/councils - Create PP via
POST /admin/councils/:id/pps - Fund PAY_SERVICE keypair via Friendbot (for provider-platform auth)
- Verify council config
./up.sh # Keys + infra (single command)
./setup-c.sh # Deploy contracts + create council
./setup-pp.sh # Register privacy provider
./setup-pay.sh # Seed pay-platform councils
./setup-accounts-extension.sh # Fund browser extension wallets./down.shTears down all containers, kills all services, and removes generated files (.env, *.log, .local-dev-state). After this you're back to a clean machine. Re-running up.sh gives you a fresh Stellar ledger and empty databases — you'll need to re-run the setup scripts to repopulate the application state.
./test.sh e2e # Payment flow (deposit, send, receive, withdraw)
./test.sh otel # Payment flow + OTEL trace verification
./test.sh governance # UC2 governance flows (approve, reject, multi-PP)
./test.sh lifecycle # Full lifecycle (deploy → payment → remove)
./test.sh pos-instant # UC4 POS crypto instant payment (temp P256 hop)
./test.sh all # All suites in parallelEach run spins up its own Stellar node, PostgreSQL, provider, council, and (for POS suites) pay-platform in Docker — fully isolated, no shared state, no dependency on up.sh. Uses your current local repo source code (mounted read-only). Set BASE_DIR if your repos aren't in ~/repos/.
Each suite has its own Docker Compose file (docker-compose.<suite>.yml), its own setup script, and its own test runner. No conditional branching — every suite is fully explicit about what it needs.
The POS tests import and call the actual moonlight-pay frontend functions (executeInstantPayment, executeSelfCustodialPayment) with a mock signer that replaces Freighter. This ensures the tests exercise the same code paths the browser UI uses. Shared test helpers live in e2e/pos-helpers.ts.
See e2e/README.md for the Docker compose setup that runs E2E tests in CI without any host dependencies.
cd e2e && docker compose up --abort-on-container-exitEvery test suite exports OTEL traces to Jaeger, and Jaeger persists them to disk at .traces/<suite>/. After a test run (pass or fail), you can inspect the traces by starting a Jaeger instance against the persisted data:
# Start Jaeger in read-only mode against a suite's traces
docker run --rm -d --name jaeger-inspect \
-e SPAN_STORAGE_TYPE=badger \
-e BADGER_EPHEMERAL=false \
-e BADGER_DIRECTORY_KEY=/badger/keys \
-e BADGER_DIRECTORY_VALUE=/badger/values \
-v "$(pwd)/.traces/lifecycle:/badger" \
-p 16687:16686 \
jaegertracing/all-in-one:latest
# Open the UI
open http://localhost:16687
# When done
docker rm -f jaeger-inspectReplace lifecycle with any suite name (e2e, otel, governance).
| Symptom | Where to look |
|---|---|
| Bundle FAILED | Service: provider-platform, Operation: Executor.submitTransactionToNetwork — check submission_failed event for error.message |
| Auth fails | Service: provider-platform, Operation: P_VerifyChallenge — check signature verification events |
| Event watcher not firing | Service: provider-platform, Operation: EventWatcher.poll — check for poll_error events |
| Slow transactions | Service: provider-platform, compare Executor.submitTransactionToNetwork durations across bundles |
| Missing distributed traces | Service: moonlight-e2e — check that SDK spans have provider-platform children (CHILD_OF references) |
.traces/
├── e2e/ # Payment flow traces
├── otel/ # Same as e2e (used for OTEL verification)
├── governance/ # UC2 governance flow traces
├── lifecycle/ # Full lifecycle traces (deploy → payment → remove)
└── pos-instant/ # POS crypto instant payment traces
Each directory contains Jaeger's badger storage (keys/ and values/). Delete a directory to clear its traces. The .traces/ directory is gitignored.
scripts/verify-deploy.sh is a one-shot check that confirms every deployed Moonlight app is running its latest tagged release. It probes 14 endpoints (6 backend /api/v1/health + 8 frontend /health.json, testnet + mainnet × {council-platform, pay-platform, provider-platform, council-console, provider-console, network-dashboard, moonlight-pay}), compares each app's reported version against its repo's latest GitHub tag, and prints a status table.
bash scripts/verify-deploy.shExit codes:
0— every app on its latest tag.1— at least one app drifts (older deployed version than latest tag).2— at least one endpoint is unreachable / returned no valid JSON.
Sample happy-path output:
APP ENV DEPLOYED LATEST_TAG STATUS
council-platform mainnet 0.4.20 v0.4.20 OK
council-platform testnet 0.4.20 v0.4.20 OK
pay-platform mainnet 0.5.16 v0.5.16 OK
...
moonlight-pay testnet 0.5.17 v0.5.17 OK
Requires gh authenticated (for tag lookup), curl, jq. No Docker / no stack startup needed — the script only hits public deployed endpoints.
See RELEASES.md for the versioning strategy and release workflows across all modules.
- Friendbot timeout: The local Stellar node can take a few minutes on first start. Re-run
./up.sh, it will pick up where it left off. - Provider connection fails: Check
provider.login this directory for errors. - Contract deployment fails: Make sure the local Stellar container is running (
docker ps). - No traces in Jaeger: Check
jaeger.logand ensureOTEL_DENO=trueis set (automatic withdeno task e2e).