Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion docs/rfq-demo-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,13 @@ Router settlement path.
## One-command demo

```sh
scripts/e2e-anvil.sh --profile buidl-like --mode rfq
scripts/demo.sh --profile buidl-like
```

The launcher starts Anvil, deploys the selected profile, starts the RFQ backend,
starts the read-only Operator API and serves the dashboard. Open the printed URL
and press Ctrl-C to stop all local services.

The command exits non-zero on failure. A successful run proves, in order:

1. the selected asset profile is valid for the deployed stack;
Expand Down
99 changes: 99 additions & 0 deletions scripts/demo.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
set -euo pipefail

ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$ROOT_DIR"

PROFILE=buidl-like
MODE=rfq
ANVIL_PORT=8545
BACKEND_PORT=8787
OPERATOR_API_PORT=8788
DASHBOARD_PORT=8790

while [ $# -gt 0 ]; do
case "$1" in
--profile) PROFILE="$2"; shift 2 ;;
--profile=*) PROFILE="${1#*=}"; shift ;;
--mode) MODE="$2"; shift 2 ;;
--mode=*) MODE="${1#*=}"; shift ;;
--port) ANVIL_PORT="$2"; shift 2 ;;
--backend-port) BACKEND_PORT="$2"; shift 2 ;;
--operator-api-port) OPERATOR_API_PORT="$2"; shift 2 ;;
--dashboard-port) DASHBOARD_PORT="$2"; shift 2 ;;
-h|--help)
echo "Usage: scripts/demo.sh [--profile buidl-like|reg-d] [--mode rfq|full]"
echo " [--port N] [--backend-port N] [--operator-api-port N] [--dashboard-port N]"
exit 0
;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done

STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/corner-store-demo.XXXXXX")
PID_FILE="$STATE_DIR/pids"
OPERATOR_API_PID=""
DASHBOARD_PID=""

cleanup() {
if [ -n "$DASHBOARD_PID" ] && kill -0 "$DASHBOARD_PID" 2>/dev/null; then
kill "$DASHBOARD_PID" 2>/dev/null || true
wait "$DASHBOARD_PID" 2>/dev/null || true
fi
if [ -n "$OPERATOR_API_PID" ] && kill -0 "$OPERATOR_API_PID" 2>/dev/null; then
kill "$OPERATOR_API_PID" 2>/dev/null || true
wait "$OPERATOR_API_PID" 2>/dev/null || true
fi
if [ -s "$PID_FILE" ]; then
mapfile -t DEMO_PIDS < "$PID_FILE"
for pid in "${DEMO_PIDS[@]}"; do
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill "$pid" 2>/dev/null || true
fi
done
fi
rm -rf "$STATE_DIR"
}
trap cleanup EXIT INT TERM

echo "==> Starting Corner Store local demo"
scripts/e2e-anvil.sh --profile "$PROFILE" --mode "$MODE" \
--port "$ANVIL_PORT" --backend-port "$BACKEND_PORT" --pid-file "$PID_FILE" --keep

echo "==> Starting read-only Operator API"
npm run build --prefix services/operator-api >/dev/null
CORNER_STORE_CONFIG="services/toolkit/examples/corner-store.config.json"
if [ "$PROFILE" = "reg-d" ]; then
CORNER_STORE_CONFIG="services/toolkit/examples/corner-store.reg-d.config.json"
fi
CORNER_STORE_CONFIG="$CORNER_STORE_CONFIG" \
CORNER_STORE_ARTIFACT="deployments/anvil-e2e.json" \
PORT="$OPERATOR_API_PORT" \
node services/operator-api/dist/src/index.js >"$STATE_DIR/operator-api.log" 2>&1 &
OPERATOR_API_PID=$!

echo "==> Starting dashboard"
CORNER_STORE_OPERATOR_API="http://127.0.0.1:${OPERATOR_API_PORT}" \
PORT="$DASHBOARD_PORT" \
npm run start --prefix services/operator-dashboard >"$STATE_DIR/dashboard.log" 2>&1 &
DASHBOARD_PID=$!

DASHBOARD_READY=0
for _ in $(seq 1 25); do
if curl -fsS "http://127.0.0.1:${DASHBOARD_PORT}/health" >/dev/null 2>&1; then
DASHBOARD_READY=1
break
fi
sleep 0.2
done
if [ "$DASHBOARD_READY" -ne 1 ]; then
cat "$STATE_DIR/dashboard.log" >&2 || true
exit 1
fi

echo ""
echo "Demo is ready: http://127.0.0.1:${DASHBOARD_PORT}"
echo "RFQ backend: http://127.0.0.1:${BACKEND_PORT}"
echo "Press Ctrl-C to stop all demo services."
wait "$DASHBOARD_PID"

13 changes: 10 additions & 3 deletions scripts/e2e-anvil.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@
# and tears the node down on exit. Runs fully offline.
#
# Usage:
# scripts/e2e-anvil.sh [--port N] [--backend-port N] [--profile buidl-like|reg-d] [--mode full|rfq] [--keep]
# scripts/e2e-anvil.sh [--port N] [--backend-port N] [--profile buidl-like|reg-d] [--mode full|rfq] [--pid-file PATH] [--keep]
#
# --port N Anvil port (default 8545).
# --backend-port N RFQ demo backend port (default 8787).
# --profile Asset profile (default buidl-like; alternative reg-d).
# --mode full runs the 7-scenario suite plus RFQ; rfq runs the concise
# backend/CLI/Router RFQ walkthrough only (default full).
# --pid-file write Anvil and RFQ backend PIDs for a supervising launcher.
# --keep Leave Anvil running after the suite (attach a UI / continue the
# demo interactively). Otherwise Anvil is killed on exit.
#
Expand All @@ -28,6 +29,7 @@ KEEP=0
ASSET_PROFILE=buidl-like
BACKEND_PORT=8787
DEMO_MODE=full
PID_FILE=""
while [ $# -gt 0 ]; do
case "$1" in
--port) PORT="$2"; shift 2 ;;
Expand All @@ -38,6 +40,8 @@ while [ $# -gt 0 ]; do
--backend-port=*) BACKEND_PORT="${1#*=}"; shift ;;
--mode) DEMO_MODE="$2"; shift 2 ;;
--mode=*) DEMO_MODE="${1#*=}"; shift ;;
--pid-file) PID_FILE="$2"; shift 2 ;;
--pid-file=*) PID_FILE="${1#*=}"; shift ;;
--keep) KEEP=1; shift ;;
-h|--help) grep '^#' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
Expand Down Expand Up @@ -65,6 +69,9 @@ CHECKPOINT_FILE="${TMPDIR:-/tmp}/corner-store-e2e-checkpoint.$$"

cleanup() {
if [ "$KEEP" -eq 1 ]; then
if [ -n "$PID_FILE" ]; then
printf '%s\n%s\n' "$ANVIL_PID" "$BACKEND_PID" > "$PID_FILE"
fi
if [ -n "$ANVIL_PID" ]; then
echo ""
echo "==> --keep set: Anvil left running (pid ${ANVIL_PID}) on ${RPC}"
Expand Down Expand Up @@ -180,10 +187,10 @@ DEMO_SETTLED=$(curl -fsS -X POST "http://127.0.0.1:${BACKEND_PORT}/demo/trade" \
-H "content-type: application/json" \
-d '{"amountIn":"5000000000000000000000000","action":"settle"}')
node -e 'const value=JSON.parse(process.argv[1]); if (!value.transaction?.hash || BigInt(value.transaction.rwaDelta) <= 0n) process.exit(1); console.log(` PASS: dashboard trade settled in block ${value.transaction.blockNumber}`);' "$DEMO_SETTLED"
DEMO_REJECTED=$(curl -fsS -X POST "http://127.0.0.1:${BACKEND_PORT}/demo/trade" \
DEMO_REJECTED=$(curl -sS -X POST "http://127.0.0.1:${BACKEND_PORT}/demo/trade" \
-H "content-type: application/json" \
-d '{"amountIn":"5000000000000000000000000","action":"revoked-maker"}')
node -e 'const value=JSON.parse(process.argv[1]); if (!value.rejection) process.exit(1); console.log(" PASS: dashboard maker-revocation was rejected");' "$DEMO_REJECTED"
node -e 'const value=JSON.parse(process.argv[1]); if (!value.rejection) { console.error(value); process.exit(1); } console.log(" PASS: dashboard maker-revocation was rejected");' "$DEMO_REJECTED"

echo "==> Requesting and filling a backend-signed RFQ quote through the Router"
"${CLI[@]}" rfq-quote --backend "http://127.0.0.1:${BACKEND_PORT}" \
Expand Down
18 changes: 7 additions & 11 deletions services/operator-dashboard/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,17 @@ This dashboard has two modes:
- Governance changes must be prepared as an external multisig proposal and reviewed
against the deployment artifact before execution.

For the RFQ-first MVP demo:
For the RFQ-first MVP demo, use the one-command launcher:

```sh
# terminal 1
scripts/e2e-anvil.sh --profile buidl-like --mode rfq --keep

# terminal 2
npm run start --prefix services/operator-dashboard
scripts/demo.sh --profile buidl-like
```

Open `http://127.0.0.1:8790` and select a view. The dashboard never has a
private key and does not submit transactions. Start Operator API on port 8788,
or set `CORNER_STORE_OPERATOR_API` to its URL. If the API uses authentication,
set `CORNER_STORE_API_TOKEN` on the dashboard process; the token stays server
side and is never exposed to the browser.
Open the printed URL and select a view. Press Ctrl-C to stop all local services.
For manual composition, start Operator API on port 8788 or set
`CORNER_STORE_OPERATOR_API` to its URL. If the API uses authentication, set
`CORNER_STORE_API_TOKEN` on the dashboard process; the token stays server side
and is never exposed to the browser.

Production hosting, authentication, CSRF policy and multisig provider
integration remain deployment-specific work.
4 changes: 4 additions & 0 deletions services/operator-dashboard/test/smoke.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,8 @@ const server = fs.readFileSync(path.join(__dirname, "..", "server.js"), "utf8");
for (const marker of ["CORNER_STORE_OPERATOR_API", "CORNER_STORE_API_TOKEN", "proxyOperatorApi", "operator_api_unavailable"]) {
if (!server.includes(marker)) throw new Error(`dashboard proxy marker missing: ${marker}`);
}
const launcher = fs.readFileSync(path.join(__dirname, "..", "..", "..", "scripts", "demo.sh"), "utf8");
for (const marker of ["scripts/e2e-anvil.sh", "CORNER_STORE_ARTIFACT", "Press Ctrl-C to stop all demo services"]) {
if (!launcher.includes(marker)) throw new Error(`demo launcher marker missing: ${marker}`);
}
console.log("corner-store operator dashboard smoke ok");
19 changes: 15 additions & 4 deletions services/rfq-demo-backend/src/settlement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,10 @@ export interface DemoTradeResult {
export class DemoSettlementService {
private readonly provider: JsonRpcProvider;
private readonly investor: NonceManager;
private readonly operator: NonceManager;
private readonly operator: HDNodeWallet;
private readonly investorAddress: string;
private nextRouterNonce = BigInt(Date.now());
private operatorNonce?: number;

constructor(private readonly config: DemoBackendConfig, private readonly quotes: RFQBackendSDK) {
if (!config.demoSettlement.enabled) throw new Error("demo settlement is disabled");
Expand All @@ -49,7 +50,7 @@ export class DemoSettlementService {

this.provider = new JsonRpcProvider(config.rpcUrl, config.chainId);
this.investor = new NonceManager(investor.connect(this.provider));
this.operator = new NonceManager(operator.connect(this.provider));
this.operator = operator.connect(this.provider);
}

async trade(amountIn: string, action: DemoTradeAction): Promise<DemoTradeResult> {
Expand Down Expand Up @@ -122,9 +123,19 @@ export class DemoSettlementService {
}

private async setMakerApproval(approved: boolean): Promise<void> {
// The operator account also performed deployment/onboarding transactions.
// Refresh before each policy toggle so a revoke/finally-restore pair cannot
// reuse a stale cached nonce in the long-lived demo backend.
const nonce = this.operatorNonce ?? await this.operator.getNonce("pending");
this.operatorNonce = nonce + 1;
const adapter = new Contract(this.config.artifact.rfqAdapter, RFQ_ADAPTER_ABI, this.operator);
const tx = await adapter.setMakerApproved(this.config.artifact.maker, approved);
await tx.wait();
try {
const tx = await adapter.setMakerApproved(this.config.artifact.maker, approved, {nonce});
await tx.wait();
} catch (error) {
this.operatorNonce = undefined;
throw error;
}
}
}

Expand Down
Loading