Skip to content

bitmap 3D PFP: simplify pass after /simplify review #63

bitmap 3D PFP: simplify pass after /simplify review

bitmap 3D PFP: simplify pass after /simplify review #63

name: E2E (regtest mint)
# Full-stack mint round-trip on Bitcoin regtest:
# bitcoind + electrs + mariadb (from ordpool-sdk's consumer-environment)
# + ordpool-backend (this repo) + ordpool/frontend serving locally
# + Xverse extension headed in xvfb
# + Playwright drives /cat21-mint end-to-end through to a real mint
#
# Iteration ladder (matches PLAN-cat21-mint-port.md):
# 1. bring the backend stack up, confirm /api/v1/fees/recommended +
# /api/v1/blocks/tip/height answer [done]
# 2. add frontend serve + Playwright smoke (page loads, wallet picker
# visible) [done]
# 3. add Xverse extension + full mint click-through + 1-block mine +
# pending feed transition assertion [done]
#
# Subsequent iterations extend the same workflow file rather than
# creating new ones — keeps the CI surface stable.
on:
workflow_dispatch:
schedule:
- cron: '17 3 * * *' # nightly at 03:17 UTC
push:
paths:
- 'e2e/docker-compose.ordpool-backend.yml'
- 'e2e/mempool-config.regtest.json'
- '.github/workflows/e2e-regtest-mint.yml'
permissions:
contents: read
concurrency:
group: ordpool-e2e-regtest-mint-${{ github.ref }}
# Don't cancel an in-flight run when a newer push / dispatch
# arrives. Cancelled runs show up on the commit page as gray X
# marks that read as "CI broken" even though they just got
# superseded by a newer dispatch. Letting them complete keeps the
# commit's check history clean.
cancel-in-progress: false
jobs:
regtest-mint:
name: Bring up stack + smoke-test backend
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout ordpool
uses: actions/checkout@v4
- name: Setup Node
# Match the working build-frontend.yml exactly — setup-node@v6
# + plain `npm ci`. Earlier runs of this workflow used
# `--legacy-peer-deps` which drops the optional peer deps
# (@popperjs/core, sats-connect) that the build needs; v6 +
# plain ci sidesteps that.
uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
cache-dependency-path: 'frontend/package-lock.json'
- name: Install frontend npm deps (brings ordpool-sdk into node_modules)
run: npm ci
working-directory: frontend
- name: Make SDK consumer-environment scripts executable
# Git on Linux preserves the bit, but defensive against any
# cross-platform clone that lost it.
run: chmod +x node_modules/ordpool-sdk/e2e/consumer-environment-bootstrap.sh
working-directory: frontend
- name: Bring up consumer-environment (bitcoind + electrs + mariadb)
run: |
frontend/node_modules/ordpool-sdk/e2e/consumer-environment-bootstrap.sh \
> stack.json
cat stack.json
- name: Start fees + electrs HTTP+WS shim on :8999
# We bring up just the SDK consumer-environment (bitcoind +
# electrs + mariadb). For the mempool-style API surface the
# frontend needs (/api/v1/fees/recommended + Esplora-shaped
# /api/address/.../utxo + /api/tx/...) we use the tiny zero-
# dep Node shim cat21-indexer uses for the same purpose. The
# upstream mempool/backend Docker image only serves /api/v1/*
# — it lacks ordpool's custom Express middleware that proxies
# /api/* to electrs, so the SDK's getUtxos 404s against it.
#
# Run with cwd=frontend so the stub's dynamic `import('ws')`
# resolves against frontend/node_modules/ws (a transitive dep
# of the mempool fork). ordpool's StateService.recommendedFees$
# is fed from a WebSocket — without one, the cat21-mint empty
# state never renders because the `(recommendedFees$ | async)`
# gate is null. The stub responds to the WS upgrade on
# /api/v1/ws with a single snapshot frame containing `fees`,
# `mempool-blocks`, `da`, `backendInfo` so the state-service
# marks itself "connected" (drops the "Reconnecting…" badge
# the trace showed pinned on a failed run) and the fee
# signal fires.
run: |
cd frontend
ELECTRS_URL=http://localhost:3000 PORT=8999 WS_ENABLED=1 \
nohup node ../e2e/fees-electrs-stub.mjs > ../fees-stub.log 2>&1 &
echo $! > ../fees-stub.pid
cd ..
for i in $(seq 1 30); do
if curl -fsS http://localhost:8999/healthz >/dev/null 2>&1; then
echo "fees-stub up"; break
fi
if [ "$i" = "30" ]; then
echo "fees-stub never came up"; cat fees-stub.log || true; exit 1
fi
sleep 1
done
- name: Smoke-test stub endpoints
run: |
echo "--- fees/recommended"
curl -fsS http://localhost:8999/api/v1/fees/recommended
echo
echo "--- blocks/tip/height (electrs)"
curl -fsS http://localhost:8999/api/blocks/tip/height
echo
# ── Iter 2 + 3 — Playwright + Xverse + the page-driven mint ─────
#
# We checkout the SDK as a sibling repo because its Xverse
# globalSetup (onboards the vault on Regtest, dumps the seeded
# user-data-dir) needs the SDK's own dev deps + harness bundle.
# The SDK's package install in node_modules ships only the
# runtime; running its playwright config requires the full repo.
- name: Checkout ordpool-sdk (for Xverse extension + seeded vault)
uses: actions/checkout@v4
with:
repository: ordpool-space/ordpool-sdk
path: sdk
ref: main
- name: Setup Node (sdk)
uses: actions/setup-node@v4
with:
node-version: '24.13.0'
cache: 'npm'
cache-dependency-path: 'sdk/package-lock.json'
- name: Install SDK deps + build (harness needs the dist/ output)
working-directory: sdk
run: |
npm ci
npm run build
- name: Install Playwright system deps + Chromium
working-directory: sdk
run: npx playwright install --with-deps chromium
# Cache the unpacked Xverse extension keyed by the version pinned
# in the SDK's playwright-bootstrap.sh. First run downloads .crx
# from the SDK repo's release mirror; subsequent runs are a hit.
- name: Cache unpacked Xverse extension
id: xverse-cache
uses: actions/cache@v4
with:
path: sdk/e2e/extensions/xverse
key: xverse-extension-v2.3.2
- name: Download + unpack Xverse from SDK release mirror
if: steps.xverse-cache.outputs.cache-hit != 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
working-directory: sdk
run: bash e2e/playwright/playwright-bootstrap.sh xverse
- name: Bootstrap regtest funds (101 blocks, dump WIF)
working-directory: sdk
run: |
chmod +x e2e/regtest-bootstrap.sh
./e2e/regtest-bootstrap.sh > regtest-bootstrap.json
cat regtest-bootstrap.json
- name: Run SDK's Xverse onboarding (produces seeded user-data-dir)
working-directory: sdk
run: |
# globalSetup.ts is the side-effect target — running any
# xverse spec triggers it. `xverse-loads.spec.ts` is the
# cheapest one and exits in ~10s once the vault is seeded.
xvfb-run --auto-servernum --server-args='-screen 0 1280x900x24' \
npx playwright test e2e/playwright/specs/xverse-loads.spec.ts \
--config=e2e/playwright/playwright.config.ts
- name: Verify seed user-data-dir landed
run: |
test -d sdk/test-results/xverse-seed-user-data-dir/Default \
|| { echo "no seed user-data-dir at expected path"; ls sdk/test-results/; exit 1; }
# ── Patch environment.ts + serve the frontend ──────────────────
#
# The local-esplora proxy config is bound to upstream mempool's
# webpack-dev-server + http-proxy-middleware contract that's
# diverged in newer Angular CLI versions ("Invalid context"
# errors fire on every request even with valid globs). Skip the
# proxy entirely: patch environment.ts so apiBaseUrl +
# websocketBaseUrl are absolute localhost URLs, then run plain
# `ng serve` with no -c flag (the default has no proxyConfig).
# The frontend talks directly to ordpool-backend on :8999, which
# in turn proxies /api/* to electrs.
- name: Generate frontend config (Ordpool defaults)
working-directory: frontend
run: npm run config:defaults:ordpool
- name: Patch environment.ts to localhost absolute URLs
working-directory: frontend
# Three URL knobs that have to be rewritten in-place to land on
# the regtest stub:
# apiBaseUrl '' → http://localhost:8999 (mempool API + electrs proxy)
# websocketBaseUrl '' → ws://localhost:8999 (mempool WS)
# cat21BaseUrl http://localhost:3333 → http://localhost:8999
# (the wallet asset-scanner calls /api/status + /api/cats/numbers/...
# to identify cat sats among UTXOs; without redirecting these the
# scanner blocks on ECONNREFUSED and the page never reaches a
# connected state — observed as a "Reconnecting…" banner that
# never clears in trace logs of failed runs)
run: |
sed -i \
-e "s|apiBaseUrl: ''|apiBaseUrl: 'http://localhost:8999'|" \
-e "s|websocketBaseUrl: ''|websocketBaseUrl: 'ws://localhost:8999'|" \
-e "s|cat21BaseUrl: 'http://localhost:3333'|cat21BaseUrl: 'http://localhost:8999'|" \
src/environments/environment.ts
echo "--- patched environment.ts ---"
head -32 src/environments/environment.ts
- name: Patch bitcoinNetwork to Regtest (matches the seeded Xverse vault)
working-directory: frontend
# Frontend ships hardcoded Network.Mainnet in app.module.ts; the
# SDK's globalSetup seeded Xverse on Bitcoin Regtest. Without
# this swap Xverse refuses to connect (network mismatch) so the
# approval popup never opens and the spec times out at 60s on
# waitForApprovalPopup.
run: |
sed -i \
-e 's|useValue: Network\.Mainnet|useValue: Network.Regtest|' \
src/app/app.module.ts
grep -n bitcoinNetwork src/app/app.module.ts | head -2
- name: Build static frontend bundle
# Using `ng build` (default = base options, not the production
# config) over `ng serve` because ng-serve in CI emits HTTP 200
# for / before compile finishes — the spec then hits an empty
# shell. Static build + `npx serve` removes the compile race.
# Pipe through tee so the error surfaces in the workflow log
# AND lands in build.log for the artifact upload.
working-directory: frontend
run: |
set -o pipefail
npm run ng -- build --output-path ../dist-regtest 2>&1 | tee ../build.log
- name: Locate the built browser output
run: |
# Angular CLI's output layout depends on builder + configuration:
# could be dist-regtest/index.html (mempool's classic webpack
# builder), dist-regtest/browser/index.html (esbuild/application
# builder), or dist-regtest/<project>/browser/index.html
# (workspace-shape variants). Try each.
for candidate in \
dist-regtest \
dist-regtest/browser \
$(find dist-regtest -maxdepth 3 -type d -name browser); do
if [ -f "$candidate/index.html" ]; then
BROWSER_DIR="$candidate"
break
fi
done
if [ -z "${BROWSER_DIR:-}" ]; then
echo "could not find any index.html under dist-regtest"
ls -la dist-regtest || true
tail -200 build.log || true
exit 1
fi
echo "BROWSER_DIR=$BROWSER_DIR" >> "$GITHUB_ENV"
ls "$BROWSER_DIR" | head -8
- name: Serve the static bundle on :4200
run: |
# `serve -s` enables SPA fallback so /cat21-mint resolves
# to index.html (no real /cat21-mint file exists in the
# build, the Angular router owns that path).
nohup npx -y serve@14 -s "$BROWSER_DIR" -l 4200 --no-clipboard > serve.log 2>&1 &
echo $! > serve.pid
- name: Wait for static server ready on :4200
run: |
for i in $(seq 1 30); do
code=$(curl -s -o /dev/null -w '%{http_code}' http://localhost:4200/ 2>/dev/null || echo 000)
if [ "$code" = "200" ]; then
echo "static server up"
break
fi
if [ "$i" = "30" ]; then
echo "static server never came up (last HTTP $code)"
tail -200 serve.log || true
exit 1
fi
sleep 1
done
- name: Install Playwright in the frontend tree
working-directory: frontend
run: npx playwright install --with-deps chromium
- name: Lift SDK e2e helpers out of node_modules (Node 24 strip-types refuses .ts there)
working-directory: frontend
run: |
mkdir -p playwright/specs/regtest/sdk-lib
cp node_modules/ordpool-sdk/e2e/regtest/regtest-helpers.ts playwright/specs/regtest/sdk-lib/
cp node_modules/ordpool-sdk/e2e/playwright/approval-popup.ts playwright/specs/regtest/sdk-lib/
ls -la playwright/specs/regtest/sdk-lib/
- name: Run the page-driven cat21-mint regtest spec
working-directory: frontend
env:
# Point the spec at the SDK's seeded vault + unpacked .crx
# (the workflow's cache populated these one level up).
XVERSE_EXT_PATH: ${{ github.workspace }}/sdk/e2e/extensions/xverse
XVERSE_SEED_USER_DATA_DIR: ${{ github.workspace }}/sdk/test-results/xverse-seed-user-data-dir
FRONTEND_URL: http://localhost:4200
run: |
xvfb-run --auto-servernum --server-args='-screen 0 1280x900x24' \
npx playwright test playwright/specs/regtest/cat21-mint-regtest.spec.ts \
--config=playwright.regtest.config.ts
- name: Upload Playwright artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: regtest-mint-playwright-${{ github.run_id }}
path: |
frontend/test-results
frontend/playwright-report-regtest
build.log
serve.log
fees-stub.log
retention-days: 14
if-no-files-found: warn
- name: Dump container logs on failure
if: failure()
run: |
for c in ordpool-e2e-consumer-bitcoind ordpool-e2e-consumer-electrs \
ordpool-e2e-consumer-mariadb; do
echo "=== $c ==="
docker logs "$c" 2>&1 | tail -200 || true
done
- name: Tear down stack
if: always()
run: |
docker compose \
-f frontend/node_modules/ordpool-sdk/e2e/docker-compose.consumer-environment.yml \
down -v || true
if [ -f fees-stub.pid ]; then
kill "$(cat fees-stub.pid)" 2>/dev/null || true
fi
if [ -f frontend/serve.pid ]; then
kill "$(cat frontend/serve.pid)" 2>/dev/null || true
fi