diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..cac08cac --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +.git +.github +.agents +.codex + +**/node_modules +**/dist +**/.astro +**/coverage +**/.daml +**/*.log + +.env +**/.env +**/.env.local + +website +docs +trading +trading-tests +vendor +scripts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ebdf9ef1..39d4b93d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,6 +53,8 @@ jobs: run: npm ci - name: Typecheck run: npm run typecheck + - name: Typecheck live driver sources (no Canton connection) + run: npm run typecheck:live-scripts - name: Test run: npm test @@ -77,6 +79,12 @@ jobs: run: npm test - name: Build run: npm run build + - name: Verify production bundle excludes development authority adapters + run: | + if grep -R -E '/v1/wallet/(submit|execute)|VITE_CANTON_AUTH_TOKEN|Direct Canton|Operator Relay' dist; then + echo "development-only wallet authority leaked into the production bundle" >&2 + exit 1 + fi docs: name: Documentation build @@ -97,7 +105,7 @@ jobs: run: npm run build docker: - name: Docker build smoke + name: Container build + backend runtime smoke runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -108,6 +116,30 @@ jobs: else echo "Dockerfile.backend not present on this branch; skipping" fi + - name: Runtime-smoke backend image + run: | + cid="$(docker run --rm -d -p 127.0.0.1:18082:8080 \ + -e DEX_READ_ONLY=1 \ + -e CANTON_LEDGER_URL=http://127.0.0.1:1 \ + -e CANTON_LEDGER_TOKEN=ci-ledger-token \ + -e CANTON_OPERATOR=Operator::ci \ + -e CANTON_LP_REGISTRAR=LpRegistrar::ci \ + -e CANTON_ADMIN=Admin::ci \ + -e 'CANTON_DEX_PACKAGE_ID=#canton-dex-trading' \ + canton-dex-backend:ci)" + trap 'docker rm -f "$cid" >/dev/null 2>&1 || true' EXIT + for _ in $(seq 1 30); do + if curl -fsS http://127.0.0.1:18082/v1/status > /tmp/dex-status.json; then + break + fi + sleep 1 + done + grep -q '"synced":false' /tmp/dex-status.json + test "$(curl -sS -o /dev/null -w '%{http_code}' -X POST \ + -H 'Content-Type: application/json' -d '{}' \ + http://127.0.0.1:18082/v1/admin/pairs)" = 401 + docker exec "$cid" node -e \ + 'if (process.getuid?.() === 0) throw new Error("backend runs as root"); const Database=require("better-sqlite3"); const db=new Database(":memory:"); db.exec("select 1"); db.close()' - name: Build frontend image run: | if [ -f Dockerfile.frontend ]; then diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3d640f24..ddb7ce2b 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -40,8 +40,10 @@ builder evaluating Canton + Token Standard V2. and explain any public contract-surface change. - Backend changes: TypeScript typecheck clean; include a `curl` example for any new endpoint in the PR description. -- UI changes: at minimum a screenshot of the affected page; if the - change touches data flow, also confirm against testnet. +- UI changes: at minimum a screenshot of the affected page. If the change + touches AMM data flow, run the self-contained DPM sandbox proof documented in + `docs/guides/localnet.md`; controlled-testnet validation is an additional + check when the contributor has access to such an environment. - Avoid committing secrets, `.env` files, SQLite databases, private keys, or generated build output. The Token Standard DARs already pinned under `vendor/splice/dars/` are the intentional exception for binary dependencies. diff --git a/Dockerfile.backend b/Dockerfile.backend index cbec422b..6cc4d553 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -1,35 +1,43 @@ -FROM node:20-alpine AS builder +FROM node:24-alpine AS builder WORKDIR /app -COPY services/registry-client/package.json services/registry-client/ +# better-sqlite3 is a native dependency. Prebuilt binaries are used when one +# exists for the current architecture; these tools provide the deterministic +# node-gyp fallback instead of silently skipping its install script. +RUN apk add --no-cache python3 make g++ + +COPY services/registry-client/package.json services/registry-client/package-lock.json services/registry-client/ COPY services/operator-backend/package.json services/operator-backend/package-lock.json services/operator-backend/ WORKDIR /app/services/registry-client -RUN npm install --ignore-scripts +RUN npm ci --ignore-scripts WORKDIR /app/services/operator-backend -RUN npm install --ignore-scripts +RUN npm ci COPY services/registry-client/ /app/services/registry-client/ COPY services/operator-backend/ /app/services/operator-backend/ RUN npx tsc --noEmit -FROM node:20-alpine +FROM node:24-alpine WORKDIR /app -RUN mkdir -p /app/data +RUN mkdir -p /app/data && chown node:node /app/data -COPY --from=builder /app/services/registry-client /app/services/registry-client -COPY --from=builder /app/services/operator-backend /app/services/operator-backend +COPY --chown=node:node --from=builder /app/services/registry-client /app/services/registry-client +COPY --chown=node:node --from=builder /app/services/operator-backend /app/services/operator-backend WORKDIR /app/services/operator-backend ENV NODE_ENV=production ENV PORT=8080 +ENV HOST=0.0.0.0 EXPOSE 8080 +USER node + CMD ["node", "--import", "tsx", "src/testnet-server.ts"] diff --git a/Dockerfile.frontend b/Dockerfile.frontend index bf515c73..86eac1f1 100644 --- a/Dockerfile.frontend +++ b/Dockerfile.frontend @@ -1,11 +1,53 @@ -FROM node:20-alpine AS builder +FROM node:24-alpine AS builder WORKDIR /app COPY app/web/package.json app/web/package-lock.json ./ -RUN npm install --ignore-scripts +RUN npm ci --ignore-scripts COPY app/web/ . + +# Vite reads configuration at build time. Declare every supported safe/public +# build argument explicitly; Docker otherwise ignores Compose's `args:` values. +# A ledger bearer token is intentionally absent because VITE_* is public. +ARG VITE_API_BASE= +ARG VITE_DOCS_URL=https://srikanth-bitdynamics.github.io/Canton-Dex-Reference-Implementation/ +ARG VITE_APP_VERSION=v0.6.0 +ARG VITE_WC_PROJECT_ID= +ARG VITE_CANTON_NETWORK_ID=canton:devnet +ARG VITE_CANTON_SYNCHRONIZER= +ARG VITE_CANTON_DEX_PACKAGE_ID=#canton-dex-trading +ARG VITE_ENABLE_SDK=0 +ARG VITE_WALLET_GATEWAY_URL= +ARG VITE_WALLET_GATEWAY_NAME= +ARG VITE_WALLET_SHOW_FULL_CATALOG=0 +ARG VITE_ENABLE_PARTYLAYER=0 +ARG VITE_ENABLE_HOSTED_RFQ=0 +ARG VITE_PARTYLAYER_APP_NAME=Canton DEX +ARG VITE_PARTYLAYER_NETWORK=canton:devnet +ARG VITE_PARTYLAYER_WALLET_IDS=console,nightly,send +ARG VITE_PARTYLAYER_CONNECT_TIMEOUT_MS=180000 +ARG VITE_PARTYLAYER_REGISTRY_URL= +ARG VITE_PARTYLAYER_REGISTRY_CHANNEL=stable +ENV VITE_API_BASE=$VITE_API_BASE \ + VITE_DOCS_URL=$VITE_DOCS_URL \ + VITE_APP_VERSION=$VITE_APP_VERSION \ + VITE_WC_PROJECT_ID=$VITE_WC_PROJECT_ID \ + VITE_CANTON_NETWORK_ID=$VITE_CANTON_NETWORK_ID \ + VITE_CANTON_SYNCHRONIZER=$VITE_CANTON_SYNCHRONIZER \ + VITE_CANTON_DEX_PACKAGE_ID=$VITE_CANTON_DEX_PACKAGE_ID \ + VITE_ENABLE_SDK=$VITE_ENABLE_SDK \ + VITE_WALLET_GATEWAY_URL=$VITE_WALLET_GATEWAY_URL \ + VITE_WALLET_GATEWAY_NAME=$VITE_WALLET_GATEWAY_NAME \ + VITE_WALLET_SHOW_FULL_CATALOG=$VITE_WALLET_SHOW_FULL_CATALOG \ + VITE_ENABLE_PARTYLAYER=$VITE_ENABLE_PARTYLAYER \ + VITE_ENABLE_HOSTED_RFQ=$VITE_ENABLE_HOSTED_RFQ \ + VITE_PARTYLAYER_APP_NAME=$VITE_PARTYLAYER_APP_NAME \ + VITE_PARTYLAYER_NETWORK=$VITE_PARTYLAYER_NETWORK \ + VITE_PARTYLAYER_WALLET_IDS=$VITE_PARTYLAYER_WALLET_IDS \ + VITE_PARTYLAYER_CONNECT_TIMEOUT_MS=$VITE_PARTYLAYER_CONNECT_TIMEOUT_MS \ + VITE_PARTYLAYER_REGISTRY_URL=$VITE_PARTYLAYER_REGISTRY_URL \ + VITE_PARTYLAYER_REGISTRY_CHANNEL=$VITE_PARTYLAYER_REGISTRY_CHANNEL RUN npm run build FROM nginx:1.27-alpine diff --git a/README.md b/README.md index 4b4d146e..88a210c6 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,16 @@ # Canton DEX Reference Implementation -### A full-stack Token Standard V2 DEX reference for Canton. +## A full-stack code reference for a Token Standard V2 DEX on Canton Daml contracts, an operator backend, a React frontend, wallet handoff, tests, and runbooks for RFQs, prefunded orders, pools, swaps, and LP tokens. +> **New to Canton but comfortable with AMMs?** Follow the single +> [newcomer learning path](docs/README.md#canonical-newcomer-learning-path). It +> starts with the Canton/Daml mental model, runs each proof mode, traces one +> swap, and ends with a tested first code change. +

License: Apache 2.0 Daml SDK 3.5.2 @@ -22,6 +27,7 @@ and runbooks for RFQs, prefunded orders, pools, swaps, and LP tokens.

Quick Start · + Documentation Site · Features · Architecture · Workflows · @@ -35,8 +41,10 @@ and runbooks for RFQs, prefunded orders, pools, swaps, and LP tokens. git clone https://github.com/srikanth-bitdynamics/Canton-Dex-Reference-Implementation.git cd Canton-Dex-Reference-Implementation -(cd services/operator-backend && npm install && npm run dev) -(cd app/web && npm install && npm run dev) +(cd services/operator-backend && npm ci) +(cd app/web && npm ci && cp .env.example .env.local) + +# Then use the split-terminal Quick Start below. ``` @@ -52,10 +60,16 @@ Canton. It shows how market state, wallet-authorized funding, registry-defined holdings, V2 allocations, and atomic settlement batches fit together in one application. +In AMM terms: a **holding** is a token balance, an **allocation** locks a +trader's funds for a single trade, and a **settlement batch** is the one atomic +step that exchanges them. The RFQs, orders, pools, swaps, and LP tokens named +above are those Canton pieces assembled into an exchange. + It is designed to be: - **Readable**: Daml templates and docs explain the workflow boundaries. -- **Runnable**: local demo mode works without a Canton participant. +- **Runnable**: the browser preview and Daml-engine tests run without a Canton + participant; real wallet settlement uses a configured participant. - **Verifiable**: Daml tests and TypeScript tests cover the reference flows. - **Forkable**: builders can reuse the Daml, backend, frontend, or docs. @@ -64,6 +78,17 @@ It is designed to be: > exchange. Production adopters should perform their own security review, > operational hardening, compliance work, and version-compatibility checks. +The repository has three deliberately different run modes: + +| Mode | Best for | Honest boundary | +|---|---|---| +| Browser preview | screens, seeded reads, quotes, wallet-intent UI | TypeScript in-memory ledger and Mock Wallet; no Daml or value settlement | +| Daml-engine tests | choices, authorization, atomicity, conservation | Daml Script runner; no browser, backend, or Canton participant | +| Live Canton proof | real Canton process, JSON Ledger API, package upload, distinct LP/swapper parties, and add → quote-bound swap → partial remove value movement | direct-ledger driver only; no backend HTTP, browser, external wallet, or persistent state | + +See [Getting started](docs/getting-started.md) for the commands and expected +results for each mode. + ## Why Canton DEX? Token Standard V2 gives Canton applications a shared way to represent holdings, @@ -82,7 +107,7 @@ workflows, not just diagrams. -### Full-Stack Reference +### Full-Stack Code Reference The repo includes Daml contracts, backend orchestration, wallet handoff, frontend screens, tests, and operator runbooks. @@ -108,10 +133,10 @@ factories instead of a custom off-ledger balance model. | Prefunded orders | Implemented | Orders are backed by V2 allocations | | Order matching | Implemented | Reference price-time-priority matcher in the backend | | Constant-product pools | Implemented | Pool state plus committed allocation slices | -| Add/remove liquidity | Implemented | DvP request, wallet allocation, and operator settle flow | +| Add/remove liquidity | Implemented | DvP (delivery-versus-payment) request, wallet allocation, and operator settle flow | | LP token | Implemented | LP token is identified by a V2 `InstrumentId` and issued through the registry used by the reference | | Single-hop swaps | Implemented | Trader allocation plus `PoolRules_Swap` settlement | -| Wallet handoff | Implemented | Token Standard, PartyLayer, CIP-0103-style, WalletConnect, Direct Canton, and Mock providers | +| Wallet handoff | Implemented with explicit boundaries | External-wallet adapters for the Canton dapp SDK (CIP-0103), PartyLayer, and WalletConnect; DEV-only operator relay and Mock; unsafe Direct Canton experiment disabled | | Operator backend | Implemented | HTTP API, JSON Ledger API driver, idempotency, indexing, and recovery | ## Who Should Use It? @@ -124,32 +149,43 @@ factories instead of a custom off-ledger balance model. | Operator | You get deployment, observability, cleanup, and recovery patterns | | Auditor or evaluator | You can inspect authority boundaries and settlement choreography | -## New To DEX, AMM, Or Token Standard V2? +## New To Canton Or Daml? -Knowing Daml is enough to start. Use this path to build the exchange context in -small steps: +If you know AMMs but not Canton, follow this order. Each step assumes only the +steps before it: -1. Read [Understand the design in 15 minutes](docs/concepts/design-tour.md) for - the actors, contracts, authority boundaries, and four settlement flows. -2. Keep the [Glossary](docs/concepts/glossary.md) open for terms such as holding, - allocation, DvP, committed funding, and iterated settlement. -3. Read [Workflow Design](docs/concepts/workflows.md) for the active choices and - state transitions behind swaps, liquidity, orders, and RFQs. -4. Follow the [Quick Start](#quick-start) and repeat those flows with the seeded - local data. -5. Use the [Builder Guide](docs/guides/builder-guide.md) when you are ready to - change contracts or add a workflow. +1. [Canton and Daml primer](docs/concepts/canton-daml-primer.md) +2. [Overview](docs/concepts/overview.md) +3. [Getting started: run the three proof modes](docs/getting-started.md) +4. [Trace one AMM swap](docs/tutorials/amm-first-walkthrough.md) +5. [Understand the design in 15 minutes](docs/concepts/design-tour.md) +6. [Architecture](docs/concepts/architecture.md) +7. [Workflow design](docs/concepts/workflows.md) +8. [Make your first AMM code change](docs/tutorials/make-your-first-amm-change.md) +9. [Builder guide](docs/guides/builder-guide.md) + +Keep the [Glossary](docs/concepts/glossary.md) open as a companion. The same +canonical path, including the outcome of every step, is maintained in the +[documentation index](docs/README.md#canonical-newcomer-learning-path). ## Quick Start -You can run the app locally without a Canton participant. The local backend uses -an in-memory ledger and seeded demo data. +The quickest local experience is a **browser preview**, not a settled Canton +DEX. The backend uses seeded TypeScript state and Mock Wallet returns fake +contract IDs. Use the Daml tests below to exercise real contract semantics. ### Prerequisites -- Node.js 24 or newer. -- npm. -- Daml SDK 3.5.2 (managed by `dpm`) for Daml builds and tests. +- [Node.js 24 or newer](https://nodejs.org/en/download) and its bundled npm. +- [Git](https://git-scm.com/downloads/). +- [Eclipse Temurin JDK 17 or newer](https://adoptium.net/temurin/releases/?version=17), + [DPM](https://archived.docs.digitalasset.com/build/3.5/dpm/manual-install.html), and + Daml SDK 3.5.2 for Daml builds/tests and the default live-Canton proof. + +See the [full prerequisites](docs/getting-started.md#prerequisites). If the +language is new to you, start with Digital Asset's official +[Get started with Daml](https://archived.docs.digitalasset.com/build/3.5/tutorials/get-started/index.html) +tutorial. ### 1. Install @@ -157,18 +193,20 @@ an in-memory ledger and seeded demo data. git clone https://github.com/srikanth-bitdynamics/Canton-Dex-Reference-Implementation.git cd Canton-Dex-Reference-Implementation -(cd services/operator-backend && npm install) -(cd app/web && npm install) +(cd services/operator-backend && npm ci) +(cd app/web && npm ci && cp .env.example .env.local) ``` ### 2. Start The Local Backend ```bash cd services/operator-backend -npm run dev +ALLOWED_ORIGINS=http://localhost:5173 npm run dev ``` -The backend listens on . +Leave this foreground process running. Success includes +`dev server listening at http://127.0.0.1:8080`. The explicit origin is +required because browser CORS access is denied by default. ### 3. Start The Frontend @@ -176,26 +214,87 @@ In another terminal: ```bash cd app/web -cp .env.example .env.local npm run dev ``` -Open . +Open the URL Vite prints, normally . + +The app header and warning banner identify this mode as **In-memory preview — +no Canton participant**. If that banner is absent, verify which backend URL the +dApp is using before treating any displayed state as ledger-backed. ### 4. Explore -1. Click **Connect Wallet**. -2. Select **Mock Wallet (dev)**. -3. Open **Trade** and review a swap. -4. Open **Pools** to add or remove liquidity. -5. Open **Orders** to place a prefunded order. -6. Open **RFQ** to inspect the bilateral block-trade flow. -7. Open **Portfolio** and **Admin** to see user and operator views. +1. Confirm Trade and Pools show the seeded `BTC/USDC` market. +2. Select **Mock Wallet (dev)** and inspect the `trader-demo` holdings. +3. Change a swap amount and inspect the quote, fee, and price impact. +4. Browse Pools, Orders, RFQ, Portfolio, and Admin to learn the surfaces. + +Mock Wallet logs an intent and returns a `#mock-…:0` placeholder; it does not +sign or submit a Canton transaction. Writes are `401` by default. The explicit +local-only `DEX_DEV_OPEN=1` bypass opens the non-admin operator-write gate, but +allocation-backed flows can still return `501 not_supported` because this +ledger does not implement them. Admin routes still require their admin token. +These are expected preview boundaries. + +### Prove The Daml Swap + +Install DPM, JDK 17+, and SDK 3.5.2 as described in +[Getting started](docs/getting-started.md#additional-tools-for-daml-builds-and-tests), +then run: + +```bash +bash scripts/run-local-daml-tests.sh +``` + +A successful run builds `canton-dex-trading-0.1.4.dar`, reports every Daml +Script test as `ok`, and exits 0. At this revision there are 111 test +declarations. This proves Daml behavior, including real-holding settlement +fixtures, but still does not run a Canton participant or browser integration. + +### Prove Live Canton Value Movement + +The default live path needs no Canton DevKit or Docker: + +```bash +bash scripts/run-dpm-sandbox-proof.sh +``` + +It builds the DAR, starts a throwaway `dpm sandbox`, uploads that DAR (including +its embedded Token Standard dependency closure), creates distinct LP/trader and +swapper parties, and runs the live JSON Ledger API DvP driver. The driver adds +liquidity, executes a quote-bound swap, and removes half the LP position. +Success ends with: + +```text +==> PASS: portable live-Canton proof completed + The throwaway sandbox is now stopping; no persistent ledger state remains. +``` + +This proves real ledger value movement between distinct counterparties in an +authentication-disabled local sandbox. It checks exact balances and reserves, +reserve-slice reconciliation after every phase, LP holding/supply/policy +consistency, `x*y` nondecrease, reserve-per-LP, and total value conservation. +Operator, admin, and LP registrar still share the bootstrap party and the +sandbox user has unrestricted throwaway rights. It does not start the operator +HTTP server, browser, or an external wallet. See +[Local Canton from a clean clone](docs/guides/localnet.md) for its phases, +limitations, and optional persistent environments. + +The complete split-terminal procedure, exact outputs, CORS explanation, and +three-mode capability table are in [Getting started](docs/getting-started.md). ## Run Against Canton -For a real Canton participant or testnet validator, start with: +Start with the DPM sandbox proof above. It is the repository's default live +path and has no dependency on Canton DevKit. DevKit is an optional, separately +distributed helper for a persistent Splice LocalNet; it is not a runtime +dependency of the DEX application or its DARs. + +For a persistent local network, bring-your-own participant, or testnet, use: +- [`docs/guides/localnet.md`](docs/guides/localnet.md) for the default DPM + sandbox proof and optional persistent DevKit LocalNet. - [`docs/guides/run-on-testnet.md`](docs/guides/run-on-testnet.md) for the testnet setup flow. - [`docs/guides/deployment.md`](docs/guides/deployment.md) for Docker Compose and production environment variables. @@ -204,11 +303,16 @@ For a real Canton participant or testnet validator, start with: - [`docs/guides/validator-test-plan.md`](docs/guides/validator-test-plan.md) for a full live-validation checklist. +A live **browser** flow additionally needs deployed/vetted packages, separated +parties and ledger rights, long-lived registry factories and holdings, a funded +pool, backend credentials, explicit CORS, and a compatible wallet. Passing the +DPM sandbox proof does not establish those browser/wallet boundaries. + Self-custodial value flows keep trader authority in the wallet: order funding, swap allocation creation, and LP add/remove allocations never require the -operator to act as the trader. The hosted demo's RFQ routes are an explicit +operator to act as the trader. The included operator-mediated RFQ routes are an explicit exception: they require the backend's ledger user to be authorized for the -hosted trader party, and `Rfq_Accept` also requires the operator. External +configured trader party, and `Rfq_Accept` also requires the operator. External deployments should provide those authorities through their own wallet, delegation, or co-submission design. @@ -257,7 +361,8 @@ The boundary is intentionally strict: require a configuration template. - Wallets own trader-authored allocation submissions in self-custodial flows. - The operator backend submits administrative and settlement commands. Its - hosted RFQ relay is a documented demo exception, not a self-custodial path. + operator-mediated RFQ path is a documented authority exception, not a + self-custodial path or a public relay service. The Daml package separates LP-token policy, venue workflows, and the reference registry by module/template. It implements upstream Token Standard V2 @@ -281,14 +386,24 @@ Read [`docs/concepts/architecture.md`](docs/concepts/architecture.md) and ## Wallet Support -The frontend has a wallet-provider abstraction. Current providers include: - -- Token Standard V2 provider for Canton-native local and testnet flows. -- PartyLayer provider for supported Canton wallets. -- CIP-0103 SDK-style provider. -- WalletConnect provider. -- Direct Canton provider for advanced testnet sessions. -- Mock provider for local development. +The frontend separates external-wallet integrations from local conveniences: + +- **External-wallet adapters:** the Canton dapp SDK (CIP-0103), PartyLayer, and + WalletConnect. The SDK path is marked DvP-ready, PartyLayer remains marked + unproven until a selected wallet passes the live validator plan, and the + current WalletConnect adapter is marked no-DvP and rejects LP add/remove. +- **Development-only adapters:** the `token-standard` provider is + accurately labelled **Operator Relay (dev only)** because the backend submits + with configured ledger rights; Mock Wallet returns placeholders and performs + no ledger write. Neither is registered in production builds. +- **Disabled experiment:** Direct Canton is not registered. Its former + `/v1/wallet/execute` target is not a participant Ledger API endpoint, and the + app does not retain a participant bearer token in browser storage. + +No external wallet is connected automatically. When several production-facing +adapters are enabled, one row gets a recommendation badge, but the user still +chooses and authorizes the wallet. If none is configured, production does not +fall back to the operator relay. PartyLayer live-validation steps are documented in [`docs/guides/run-on-testnet.md`](docs/guides/run-on-testnet.md). @@ -315,7 +430,10 @@ bash scripts/run-local-daml-tests.sh ./scripts/deploy-testnet.sh # Run the smoke test script -./scripts/e2e-smoke.sh +bash scripts/backend-http-smoke.sh + +# Run the portable live-Canton add -> swap -> partial-remove proof +bash scripts/run-dpm-sandbox-proof.sh ``` ## Token Standard V2 @@ -344,10 +462,22 @@ intend to run. ## Documentation -The full documentation set is in **[`docs/README.md`](docs/README.md)** — organized as -Getting Started, Concepts, Guides, and Reference. Good entry points: - -- **[Getting Started](docs/getting-started.md)** — run the whole stack locally. +Read the rendered **[documentation site](https://srikanth-bitdynamics.github.io/Canton-Dex-Reference-Implementation/)** +or the repository index at **[`docs/README.md`](docs/README.md)**. The site is +published from `main`; documentation on an unmerged branch appears only after +that branch is merged and the Pages workflow completes. Good entry points: + +- **[Getting Started](docs/getting-started.md)** — choose browser preview, + Daml-engine proof, or live integration and see the boundary of each. +- **[Canton and Daml primer](docs/concepts/canton-daml-primer.md)** — the minimum + ledger mental model for a newcomer. +- **[AMM-first walkthrough](docs/tutorials/amm-first-walkthrough.md)** — trace one + swap from `x*y=k` through allocation and settlement tests. +- **[Make your first AMM code change](docs/tutorials/make-your-first-amm-change.md)** — + make a small test-first Daml refactor, check the off-ledger impact, and rerun + the live-Canton proof. +- **[Local Canton](docs/guides/localnet.md)** — run the default DPM sandbox + live proof; understand the optional DevKit path and exact proof boundaries. - **[Overview](docs/concepts/overview.md)** — what it is and the trust model. - **[Builder Guide](docs/guides/builder-guide.md)** — extend the reference. - **[HTTP API](docs/reference/http-api.md)** — backend endpoints. diff --git a/SECURITY.md b/SECURITY.md index 57c929f0..24982048 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -5,9 +5,10 @@ Only the `main` branch is supported. We do not backport fixes to older tags or releases. -The `canton-dex-trading` Daml package version on the public testnet -is the deployed surface; older versions remain queryable via Daml -smart-upgrade but are not supported for new contracts. +This repository does not provision or promise a public testnet deployment. An +operator's supported ledger surface is the exact DAR/package id they deploy from +`main`; the committed upgrade baseline exists only to validate Daml package +compatibility and is not evidence that a public service is currently running. ## Reporting a vulnerability diff --git a/app/web/.env.example b/app/web/.env.example index ad96c3ca..f811d342 100644 --- a/app/web/.env.example +++ b/app/web/.env.example @@ -1,12 +1,13 @@ # Canton DEX — Frontend Environment Variables # Copy this file to .env.local and fill in the values. -# Default wallet provider selection: production builds never default to -# the operator relay. The default is, in order: PartyLayer (if -# VITE_ENABLE_PARTYLAYER=1), then WalletConnect (if VITE_WC_PROJECT_ID is set), -# then SDK (if VITE_ENABLE_SDK=1); otherwise the user must pick a provider -# explicitly. The operator relay (token-standard) is the default only in dev -# builds and is labelled "dev only". +# Wallet recommendation badge: production builds never recommend or expose the +# operator relay. The badge follows capability readiness: SDK (if +# VITE_ENABLE_SDK=1), then PartyLayer (if VITE_ENABLE_PARTYLAYER=1), then +# WalletConnect (if VITE_WC_PROJECT_ID is set). The user always makes the +# connection choice. +# The operator relay (provider id: token-standard) exists only in DEV builds, +# is labelled "dev only", and is never given the recommendation badge. # Reown / WalletConnect Cloud project ID (required for WalletConnect provider). # Get one at https://cloud.reown.com @@ -31,19 +32,26 @@ VITE_WALLET_SHOW_FULL_CATALOG=0 # CAIP network identifier for Canton (default: canton:devnet). VITE_CANTON_NETWORK_ID=canton:devnet -# Canton JSON Ledger API URL for direct ledger reads and Token Standard wallet. -VITE_CANTON_LEDGER_URL=http://localhost:7575 - -# Bearer token for the dev-only Token Standard relay / canton-direct provider. -# DEV BUILDS ONLY: this is a long-lived credential. It is read only -# when `import.meta.env.DEV` is true; in a production build the app refuses to -# use it (logs an error), and the relay/direct providers that depend on it are -# disabled. Do NOT set this in any production environment. -VITE_CANTON_AUTH_TOKEN= - # Operator backend API base URL (default: http://localhost:8080). VITE_API_BASE=http://localhost:8080 +# DEV-only operator relay identity. This is not a wallet: the backend submits +# with its own configured ledger credential. The backend must also set +# DEX_DEV_WALLET_RELAY=1 and include this exact party in +# DEX_DEV_RELAY_PARTIES. Production builds do not register the relay. +VITE_CANTON_DEFAULT_PARTY= +VITE_CANTON_USER_ID=ledger-api-user + +# Documentation link in the application header. Set this to your deployment's +# published docs. The default in source points at this repository's GitHub +# Pages site; `/docs/` is not served by the Vite/nginx application itself. +VITE_DOCS_URL=https://srikanth-bitdynamics.github.io/Canton-Dex-Reference-Implementation/ + +# Backend write tokens are intentionally NOT VITE_* variables. Vite values are +# public bundle contents. Enter short-lived operator/admin/caller credentials +# at Admin -> API session credentials; they remain in sessionStorage for that +# tab. Use an authenticated BFF/session service for a public multi-user dApp. + # Enable the PartyLayer wallet connector in the Connect Wallet menu. # Requires an installed submit-capable wallet. The DEX tries Console, Nightly, # then Send by default; include loop to test 5N Loop explicitly. @@ -59,3 +67,8 @@ VITE_PARTYLAYER_CONNECT_TIMEOUT_MS=180000 # Optional PartyLayer registry overrides. VITE_PARTYLAYER_REGISTRY_URL= VITE_PARTYLAYER_REGISTRY_CHANNEL=stable + +# Custodial RFQ UI writes. Production defaults OFF. Set this to 1 only when the +# backend also has DEX_HOSTED_RFQ_RELAY=1, mandatory caller JWT binding, and +# deliberately provisioned actAs rights for each hosted trader. +VITE_ENABLE_HOSTED_RFQ=0 diff --git a/app/web/package-lock.json b/app/web/package-lock.json index ce1ffea6..629302a0 100644 --- a/app/web/package-lock.json +++ b/app/web/package-lock.json @@ -19,7 +19,7 @@ "@walletconnect/types": "^2.23.9", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.26.0", + "react-router-dom": "^7.18.2", "zustand": "^4.5.0" }, "devDependencies": { @@ -27,21 +27,21 @@ "@testing-library/react": "^16.3.2", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "@vitest/ui": "^1.6.1", + "@vitejs/plugin-react": "^6.1.0", + "@vitest/ui": "^4.1.11", "autoprefixer": "^10.4.19", "jsdom": "^29.1.1", "postcss": "^8.4.39", "tailwindcss": "^3.4.6", "typescript": "^5.5.3", - "vite": "^5.3.4", - "vitest": "^1.6.1" + "vite": "^8.2.2", + "vitest": "^4.1.11" } }, "node_modules/@adobe/css-tools": { - "version": "4.4.4", - "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", - "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", "dev": true, "license": "MIT" }, @@ -164,13 +164,14 @@ "license": "MIT" }, "node_modules/@babel/code-frame": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", - "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.28.5", + "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -178,299 +179,23 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.3.tgz", - "integrity": "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", - "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helpers": "^7.28.6", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.29.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", - "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", - "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", - "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", - "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", - "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", - "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", - "integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-self": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", - "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-source": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", - "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, + "peer": true, "engines": { "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" } }, "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", - "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.28.6", - "@babel/parser": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", - "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.29.0", - "@babel/generator": "^7.29.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.29.0", - "@babel/template": "^7.28.6", - "@babel/types": "^7.29.0", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", "dev": true, "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, "engines": { "node": ">=6.9.0" } @@ -493,6 +218,19 @@ "zustand": "5.0.3" } }, + "node_modules/@base-org/account/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@base-org/account/node_modules/zustand": { "version": "5.0.3", "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", @@ -524,14 +262,14 @@ } }, "node_modules/@biomejs/js-api": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@biomejs/js-api/-/js-api-4.0.0.tgz", - "integrity": "sha512-EOArR/6drRzM1/hwOIz1pZw90FL31Ud4Y7hEHGWVtMNmAwS9SrwZ8hMENGlLVXCeGW/kL46p8kX7eO6x9Nmezg==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@biomejs/js-api/-/js-api-6.0.0.tgz", + "integrity": "sha512-8HP7wexjQo5Np1J9h0B2x8L5G0GZpCacgjykxLHtvLPvF0hNqSG754oh8bxo+OSFDpVGMfSjmLO+ZY/5KbfjmQ==", "license": "MIT OR Apache-2.0", "peerDependencies": { - "@biomejs/wasm-bundler": "^2.3.0", - "@biomejs/wasm-nodejs": "^2.3.0", - "@biomejs/wasm-web": "^2.3.0" + "@biomejs/wasm-bundler": "^2.5.0", + "@biomejs/wasm-nodejs": "^2.5.0", + "@biomejs/wasm-web": "^2.5.0" }, "peerDependenciesMeta": { "@biomejs/wasm-bundler": { @@ -546,9 +284,9 @@ } }, "node_modules/@biomejs/wasm-nodejs": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@biomejs/wasm-nodejs/-/wasm-nodejs-2.4.16.tgz", - "integrity": "sha512-3BSGXHJ25Is+9gRgV+Gwz7YdMktrMOBP+Vw3RwXy3Y/CHqXbbNSyhaMt6VFk1J/WAGutdYfBy+NzhlvOftwd+g==", + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@biomejs/wasm-nodejs/-/wasm-nodejs-2.5.2.tgz", + "integrity": "sha512-B0r7jLdCmXhq4+jnx1oA0/SChLy5G283r35HJr276T+w6qgqAPAhqfnlKs+UobclAfidNDbfj9ZmcuCijaHDxQ==", "license": "MIT OR Apache-2.0" }, "node_modules/@bramus/specificity": { @@ -565,77 +303,22 @@ } }, "node_modules/@canton-network/core-ledger-client-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-ledger-client-types/-/core-ledger-client-types-1.3.1.tgz", - "integrity": "sha512-GwGmRlRI3G/7uSMP/rAldPK0t0DRiicKz+pbe5Hgf6VAGkXNeN5KdF5yOXt6ey1YxtbVIwB25Oyhf1YIIoyo1A==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-ledger-client-types/-/core-ledger-client-types-1.10.1.tgz", + "integrity": "sha512-SxiXlmNbF6Js4H/WJPF0WAr03cgNWLhXGPo39PMZ4EAD6CfFi15VgR8szzTDjtEvDO/AyZtYe6u8Pe1e7F+yXQ==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-types": "^1.3.1", + "@canton-network/core-types": "^1.10.1", "bignumber.js": "^10.0.2", - "dayjs": "^1.11.19", + "dayjs": "^1.11.21", "openapi-fetch": "^0.17.0", "pino": "^10.3.1" } }, - "node_modules/@canton-network/core-ledger-client-types/node_modules/dayjs": { - "version": "1.11.20", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", - "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", - "license": "MIT" - }, - "node_modules/@canton-network/core-ledger-client-types/node_modules/pino": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", - "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", - "license": "MIT", - "dependencies": { - "@pinojs/redact": "^0.4.0", - "atomic-sleep": "^1.0.0", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^3.0.0", - "pino-std-serializers": "^7.0.0", - "process-warning": "^5.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^4.0.1", - "thread-stream": "^4.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/@canton-network/core-ledger-client-types/node_modules/pino-abstract-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", - "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", - "license": "MIT", - "dependencies": { - "split2": "^4.0.0" - } - }, - "node_modules/@canton-network/core-ledger-client-types/node_modules/thread-stream": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", - "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", - "license": "MIT", - "dependencies": { - "real-require": "^1.0.0" - }, - "engines": { - "node": ">=20" - } - }, - "node_modules/@canton-network/core-ledger-client-types/node_modules/thread-stream/node_modules/real-require": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", - "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", - "license": "MIT" - }, "node_modules/@canton-network/core-ledger-proto": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-ledger-proto/-/core-ledger-proto-1.3.1.tgz", - "integrity": "sha512-nHRdx4ZnF+pHBQk9hBh0lyvpr/odTgxor7dTh9cx4aVwmpR4yQROHN3skQgfs5Tc5tjRbab/bhUHnVA7eC9O4g==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-ledger-proto/-/core-ledger-proto-1.9.1.tgz", + "integrity": "sha512-ol3eYkoMxUbXhPmNmI3Fqcrublo7nzJhu7uWHkNGhIG1FPgZqeEA8JXKyNB1btzcQ0QaJgzkhoNOkPurZv3njg==", "license": "Apache-2.0", "dependencies": { "@protobuf-ts/runtime": "^2.11.1", @@ -643,207 +326,170 @@ } }, "node_modules/@canton-network/core-provider-dapp": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-provider-dapp/-/core-provider-dapp-1.3.1.tgz", - "integrity": "sha512-9NIl/9UptPm0UCrEeGRZYts93FLUSnAbkSBJXspHHaSQjCeLUNGjxBQOO1I2Dj7qFhhVF/RzhFHye+zpCJCh2A==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-provider-dapp/-/core-provider-dapp-1.10.1.tgz", + "integrity": "sha512-K2hwUu9/EoJ6ClaTWYfVNbhgh2s1pZZxeVXJKie/diue3uBBT0ZezjdclEZM/EgaOr/xhh693DWv5OrebY4SJw==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-ledger-client-types": "^1.3.1", - "@canton-network/core-rpc-transport": "^1.3.1", - "@canton-network/core-splice-provider": "^1.3.1", - "@canton-network/core-types": "^1.3.1", - "@canton-network/core-wallet-dapp-remote-rpc-client": "^1.3.1", - "@canton-network/core-wallet-dapp-rpc-client": "^1.3.1" + "@canton-network/core-ledger-client-types": "^1.10.1", + "@canton-network/core-rpc-transport": "^1.10.1", + "@canton-network/core-splice-provider": "^1.10.1", + "@canton-network/core-types": "^1.10.1", + "@canton-network/core-wallet-dapp-remote-rpc-client": "^1.10.1", + "@canton-network/core-wallet-dapp-rpc-client": "^1.10.1" } }, "node_modules/@canton-network/core-rpc-errors": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-rpc-errors/-/core-rpc-errors-1.3.1.tgz", - "integrity": "sha512-aHC0UeqzKzrYXP9JpfCYjOHUHzouoNvt9RR9ai+hEv17Vc1OBkUDp1Mhab7rTcv7BtGuV+OpGnX0ABOoQejIOw==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-rpc-errors/-/core-rpc-errors-1.9.1.tgz", + "integrity": "sha512-BZwmE1gVDnPuBLkFuJ301W5bH6ZJNoVXKsCHtZh4L4pMPV91dPJJ/MiuPflFTVzHKfmjPL5xhCrQsW22WR1SUA==", "license": "Apache-2.0", "dependencies": { "@metamask/rpc-errors": "^7.0.3" } }, "node_modules/@canton-network/core-rpc-transport": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-rpc-transport/-/core-rpc-transport-1.3.1.tgz", - "integrity": "sha512-B+EtreaEvQ0sCW0JwPulDdC9MuloGQ+nZY9rfybyejOlTJKxBrduNNQoiTwC7LMJmXuArJYQdJPS1nhKiuxgtQ==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-rpc-transport/-/core-rpc-transport-1.10.1.tgz", + "integrity": "sha512-fkrV1JXnc3NUri2/y4WYV5k6R5CVgjinRoUoyP7VUNt+meSkXO8cZ/occ2lPi9cCyYDF/IikDQ/5DSKpoZzFlg==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-types": "^1.3.1", - "uuid": "^14.0.0" + "@canton-network/core-types": "^1.10.1", + "uuid": "^14.0.1" } }, "node_modules/@canton-network/core-splice-provider": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-splice-provider/-/core-splice-provider-1.3.1.tgz", - "integrity": "sha512-VhppKBkGR00vJ3LCUB3BGeoKZx7CrtslsGSyG4lRcSjYJF6KVJXU4DadsE5TZVFVTkG6E24S9lNVFLH2oBTorw==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-splice-provider/-/core-splice-provider-1.10.1.tgz", + "integrity": "sha512-xOfD/2r6rsDwxPRY2W2oB89GHImxPqEf25o8NsAx6eHVIy8UwkDUPRZ07CZpgtP/BPVZmQRHAMkpaA10YS+4sw==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-rpc-transport": "^1.3.1", - "@canton-network/core-types": "^1.3.1", - "@canton-network/core-wallet-dapp-remote-rpc-client": "^1.3.1", - "@canton-network/core-wallet-dapp-rpc-client": "^1.3.1" + "@canton-network/core-rpc-transport": "^1.10.1", + "@canton-network/core-types": "^1.10.1", + "@canton-network/core-wallet-dapp-remote-rpc-client": "^1.10.1", + "@canton-network/core-wallet-dapp-rpc-client": "^1.10.1" } }, "node_modules/@canton-network/core-tx-visualizer": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-tx-visualizer/-/core-tx-visualizer-1.3.1.tgz", - "integrity": "sha512-yc7UTEUucXzX89/Uono9CzWNdrRJDaB6BSY+29XlOSP8FVR9Ir/c8F3vFUWYM0W+pMcTHQG1RP9qrzoTEKnbaA==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-tx-visualizer/-/core-tx-visualizer-1.9.1.tgz", + "integrity": "sha512-yRjM+Q1GxzemExxHQM9B0KHgwS0OZ2lpKYECB/oY94Gs0fN1l34EXJ0NFGhnei20IKX2UuPDvS49HG0mGyp/Cg==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-ledger-proto": "^1.3.1", - "@types/node": "^25.3.3", + "@canton-network/core-ledger-proto": "^1.9.1", + "@types/node": "^25.9.4", "camelcase-keys": "^10.0.2" } }, "node_modules/@canton-network/core-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-types/-/core-types-1.3.1.tgz", - "integrity": "sha512-iG6PwUHMViWpIq3o8S/BZdkA8W/kxgMqYiI2wk5MZtpK01Vnko9Vm05XHuBpRF1MCXIHqM3ZmD2Eqgh/20KQBg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-types/-/core-types-1.10.1.tgz", + "integrity": "sha512-sX5bPnyn3xFPC2RhUPuSWNL7IknuwhF49iLLQpTWwKqCddLjfY6R2eFmi5qEvv/ahWotMEiGjg2ohYMbIf+6+A==", "license": "Apache-2.0", "dependencies": { - "@daml/types": "^3.5.0", - "uuid": "^14.0.0", - "zod": "^4.3.6" - } - }, - "node_modules/@canton-network/core-types/node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "@daml/types": "^3.5.2", + "uuid": "^14.0.1", + "zod": "^4.4.3" } }, "node_modules/@canton-network/core-wallet-auth": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-auth/-/core-wallet-auth-1.3.1.tgz", - "integrity": "sha512-DZsq8P0GmE7/vk0OKL4CcaZdGGeljoLV9/qKErOGe8yN5UYxJtnSXSZr5vrllM6M0N99lnCRwDPITtNQU+QYdg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-auth/-/core-wallet-auth-1.10.1.tgz", + "integrity": "sha512-iebwO5P+L1O8J3qU7Kzti5wrq4EZ/c8/d81Rgvb6XT6xadP55y0LFocGurIPp4fgFOr6j3gNl7XvJsLZKUVzUg==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-rpc-errors": "^1.3.1", - "@canton-network/core-types": "^1.3.1", - "jose": "^6.1.3", - "zod": "^4.3.6" - } - }, - "node_modules/@canton-network/core-wallet-auth/node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "@canton-network/core-rpc-errors": "^1.9.1", + "@canton-network/core-types": "^1.10.1", + "jose": "^6.2.3", + "zod": "^4.4.3" } }, "node_modules/@canton-network/core-wallet-dapp-remote-rpc-client": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-dapp-remote-rpc-client/-/core-wallet-dapp-remote-rpc-client-1.3.1.tgz", - "integrity": "sha512-Mhko8dgTK6KBQJHRQH4n1P16JKnANDXyOPy4BR9XzTrdOz814ZrP4vHR9E4x1868G3gXuB9MoA1zn4VjVIepwg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-dapp-remote-rpc-client/-/core-wallet-dapp-remote-rpc-client-1.10.1.tgz", + "integrity": "sha512-4m9oFUnErk4D4kFV/E6rv1dfGfTlBnMuZeDDacYvgZ0d1sgzr8OLAybLsq+u8nEV2ZrdhrONqcLsQcVqRTc0HQ==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-rpc-transport": "^1.3.1", - "@canton-network/core-types": "^1.3.1", + "@canton-network/core-rpc-transport": "^1.10.1", + "@canton-network/core-types": "^1.10.1", "lodash": "^4.18.1" } }, "node_modules/@canton-network/core-wallet-dapp-rpc-client": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-dapp-rpc-client/-/core-wallet-dapp-rpc-client-1.3.1.tgz", - "integrity": "sha512-Ej/mz4OZYXOhvjjOF9SPdV5HPMS+mJmVKqsbxlZ2r1CmePkuyRdcDk780kkOwV/MMOsFc1Uqpp1uN4ejAZjbog==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-dapp-rpc-client/-/core-wallet-dapp-rpc-client-1.10.1.tgz", + "integrity": "sha512-84T1JW3NgneyOOK8Y59zl39+hqFGKrnOAftxhepnzodMGWMjDYnG1A292PjmxWqTB5Nl/wImggcgJffqNENO0w==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-rpc-transport": "^1.3.1", - "@canton-network/core-types": "^1.3.1", + "@canton-network/core-rpc-transport": "^1.10.1", + "@canton-network/core-types": "^1.10.1", "lodash": "^4.18.1" } }, "node_modules/@canton-network/core-wallet-discovery": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-discovery/-/core-wallet-discovery-1.3.1.tgz", - "integrity": "sha512-f/slop+Idtcn3pKg5kC2lmEIBMtvnuQD5EBbxQM0LUTVREz1WizrnVcIcgIAd5DTpd8xJHsY611s1XWrAKtZng==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-discovery/-/core-wallet-discovery-1.10.1.tgz", + "integrity": "sha512-TFmfaAa+1+UmGUCv2ppqKbzgVxjEH4Xtw0+9clNYTFxj0+8iYHK8RQWbtnFQCkwEby2RljDYhFR3cQul7S0dMg==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-splice-provider": "^1.3.1", - "@canton-network/core-types": "^1.3.1", - "@canton-network/core-wallet-dapp-rpc-client": "^1.3.1" + "@canton-network/core-splice-provider": "^1.10.1", + "@canton-network/core-types": "^1.10.1", + "@canton-network/core-wallet-dapp-rpc-client": "^1.10.1" } }, "node_modules/@canton-network/core-wallet-store": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-store/-/core-wallet-store-1.3.1.tgz", - "integrity": "sha512-JccL41ZiiI580GQAOqcSDUCGcyIObRXmdA9uPj8umAA9fUKjA4swdEP2H6wz7XFmQ3Uf58DjRBQR4e7CEOD5nw==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-store/-/core-wallet-store-1.11.1.tgz", + "integrity": "sha512-Wf3A7eYydVnGBktjsEO3g4KPTu4rybyFy+N26oRy5QfZP9aLwA5rxUYih5+VAmF4lxVB2hkS+qAvOisopKgaDw==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-wallet-auth": "^1.3.1", - "zod": "^4.3.6" - } - }, - "node_modules/@canton-network/core-wallet-store/node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" + "@canton-network/core-wallet-auth": "^1.10.1", + "zod": "^4.4.3" } }, "node_modules/@canton-network/core-wallet-ui-components": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-ui-components/-/core-wallet-ui-components-1.3.1.tgz", - "integrity": "sha512-ULzjrIpxSScD53idzlC7DUFAL58vnvAYndSKybpL5FOy2Vd2G6PGH4ogRC+O6gtpryP+Nbcd/YY4OkPGS7Lw7Q==", + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-ui-components/-/core-wallet-ui-components-1.12.2.tgz", + "integrity": "sha512-sfHopeH/JsdYrnRK+OE/+m6fr4ZqZGYB0uozUbUUM5XOfEpaifY3osqIoRLpD7ew1Pae4WXx8zHQgUXneihN0Q==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-tx-visualizer": "^1.3.1", - "@canton-network/core-types": "^1.3.1", - "@canton-network/core-wallet-auth": "^1.3.1", - "@canton-network/core-wallet-store": "^1.3.1", - "@canton-network/core-wallet-user-rpc-client": "^1.3.1", + "@canton-network/core-tx-visualizer": "^1.9.1", + "@canton-network/core-types": "^1.10.1", + "@canton-network/core-wallet-auth": "^1.10.1", + "@canton-network/core-wallet-store": "^1.11.1", + "@canton-network/core-wallet-user-rpc-client": "^1.11.1", "@popperjs/core": "^2.11.8", "bootstrap": "^5.3.8", - "lit": "^3.3.2" - } - }, - "node_modules/@canton-network/core-wallet-ui-components/node_modules/lit": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", - "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", - "license": "BSD-3-Clause", - "dependencies": { - "@lit/reactive-element": "^2.1.0", - "lit-element": "^4.2.0", - "lit-html": "^3.3.0" + "lit": "^3.3.3" } }, "node_modules/@canton-network/core-wallet-user-rpc-client": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-user-rpc-client/-/core-wallet-user-rpc-client-1.3.1.tgz", - "integrity": "sha512-O/T+LG+wjdqDuplMxLLZ8LzLjX9x5NjfVuK+9DNIU3zbNv66T/aZ1MygzXIuFgG/ABQEtudYx4F/9rXQ5hRF8A==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@canton-network/core-wallet-user-rpc-client/-/core-wallet-user-rpc-client-1.11.1.tgz", + "integrity": "sha512-GnDrVYfdIcS7Kxvbb0VHkPfejjXDHBqIqjmMumJC15iqzyweyWVE/9k7XzPf5Ydys59uM7IL03tH+MvRl/KTYA==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-rpc-transport": "^1.3.1", - "@canton-network/core-types": "^1.3.1", + "@canton-network/core-rpc-transport": "^1.10.1", + "@canton-network/core-types": "^1.10.1", "lodash": "^4.18.1" } }, "node_modules/@canton-network/dapp-sdk": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@canton-network/dapp-sdk/-/dapp-sdk-1.1.0.tgz", - "integrity": "sha512-PUadCngcl82BIa6Y+xWVFgiJY8Gc7CHX7t5QuctnjG/DMy0QvCuTje8xnnV7kfou80+PVjqj3C+5cmYXMKhNow==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@canton-network/dapp-sdk/-/dapp-sdk-1.5.1.tgz", + "integrity": "sha512-AQYBu0QxCXTLgt7nb7LMx/W0YZUT98DL4IYk1Rqz0+H5+Hwlu+VtA/HGcHfllEjc79qti6qJae1uT4etJwm+Ig==", "license": "Apache-2.0", "dependencies": { - "@canton-network/core-provider-dapp": "^1.1.0", - "@canton-network/core-rpc-transport": "^1.1.0", - "@canton-network/core-splice-provider": "^1.1.0", - "@canton-network/core-types": "^1.1.0", - "@canton-network/core-wallet-dapp-remote-rpc-client": "^1.1.0", - "@canton-network/core-wallet-dapp-rpc-client": "^1.1.0", - "@canton-network/core-wallet-discovery": "^1.1.0", - "@canton-network/core-wallet-ui-components": "^1.1.0", - "qrcode": "^1.5.4" + "@canton-network/core-provider-dapp": "^1.10.0", + "@canton-network/core-rpc-transport": "^1.10.0", + "@canton-network/core-splice-provider": "^1.10.0", + "@canton-network/core-types": "^1.10.0", + "@canton-network/core-wallet-dapp-remote-rpc-client": "^1.10.0", + "@canton-network/core-wallet-dapp-rpc-client": "^1.10.0", + "@canton-network/core-wallet-discovery": "^1.10.0", + "@canton-network/core-wallet-ui-components": "^1.12.1", + "qrcode": "^1.5.4", + "uuid": "^14.0.1" }, "peerDependencies": { "@walletconnect/sign-client": "^2.23.8", @@ -858,23 +504,6 @@ } } }, - "node_modules/@canton-network/dapp-sdk/node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@cloudflare/json-schema-walker": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@cloudflare/json-schema-walker/-/json-schema-walker-0.1.1.tgz", @@ -882,9 +511,9 @@ "license": "BSD-3-Clause" }, "node_modules/@coinbase/cdp-sdk": { - "version": "1.49.2", - "resolved": "https://registry.npmjs.org/@coinbase/cdp-sdk/-/cdp-sdk-1.49.2.tgz", - "integrity": "sha512-QojjrkLG2mgo5Lq2ybu+k8Rk1NtklKQrroPG/1VCvMM62kGnF59B5re4B3XySY4etrzu60oqCnPuLSRcwuhI1g==", + "version": "1.55.0", + "resolved": "https://registry.npmjs.org/@coinbase/cdp-sdk/-/cdp-sdk-1.55.0.tgz", + "integrity": "sha512-5PbUg3n3Jk9nm8nEStskRv6jTrVZKkgwxMFjW+i/xUDDzK1fXksKwXdjgUiHB1hf0FZx1LiYBosrh4IULxFyPA==", "license": "MIT", "optional": true, "dependencies": { @@ -900,12 +529,124 @@ "uncrypto": "^0.1.3", "viem": "^2.47.0", "zod": "^3.25.76" + }, + "peerDependencies": { + "@x402/core": "^2.21.0", + "@x402/evm": "^2.21.0", + "@x402/extensions": "^2.21.0", + "@x402/svm": "^2.21.0" + }, + "peerDependenciesMeta": { + "@x402/core": { + "optional": true + }, + "@x402/evm": { + "optional": true + }, + "@x402/extensions": { + "optional": true + }, + "@x402/svm": { + "optional": true + } + } + }, + "node_modules/@coinbase/cdp-sdk/node_modules/abitype": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.6.tgz", + "integrity": "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@coinbase/cdp-sdk/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "optional": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@coinbase/wallet-sdk": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/@coinbase/wallet-sdk/-/wallet-sdk-4.3.6.tgz", + "integrity": "sha512-4q8BNG1ViL4mSAAvPAtpwlOs1gpC+67eQtgIwNvT3xyeyFFd+guwkc8bcX5rTmQhXpqnhzC4f0obACbP9CqMSA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@noble/hashes": "1.4.0", + "clsx": "1.2.1", + "eventemitter3": "5.0.1", + "idb-keyval": "6.2.1", + "ox": "0.6.9", + "preact": "10.24.2", + "viem": "^2.27.2", + "zustand": "5.0.3" + } + }, + "node_modules/@coinbase/wallet-sdk/node_modules/@noble/hashes": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@coinbase/wallet-sdk/node_modules/zustand": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.3.tgz", + "integrity": "sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } } }, "node_modules/@console-wallet/dapp-sdk": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@console-wallet/dapp-sdk/-/dapp-sdk-2.2.0.tgz", - "integrity": "sha512-1W2uWqlTdqDbSfg5AJ3ubs+s/P1GA601TMJH2ZssuOIMXPvsQ+tJlWgXnT/cnXMro1C8ixPsJCOKgVKi+I5t8Q==", + "version": "2.2.9", + "resolved": "https://registry.npmjs.org/@console-wallet/dapp-sdk/-/dapp-sdk-2.2.9.tgz", + "integrity": "sha512-9LfSxbOqFCEDoNFElCDogCToYxpH0nHV3Tgv9SSyjkiwDE80FIh1UjVpymCTiQpTYPCoQVt7UgPzzXh8MoDX9g==", "license": "ISC", "dependencies": { "axios": "^1.13.5", @@ -918,16 +659,10 @@ "tslib": "^2.8.1" } }, - "node_modules/@console-wallet/dapp-sdk/node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", "dev": true, "funding": [ { @@ -945,9 +680,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", - "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", "dev": true, "funding": [ { @@ -969,9 +704,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", - "integrity": "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.1.tgz", + "integrity": "sha512-YpAJZhaHplYQkG8ib+/Fx5Y0eF2lVWi3tIvMJA6i39TLyUNp2439cifzW8VMjhlqrBjHzK5hVGugRRm2zTKI/A==", "dev": true, "funding": [ { @@ -985,8 +720,8 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.1" + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" }, "engines": { "node": ">=20.19.0" @@ -1020,9 +755,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", - "integrity": "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.9.tgz", + "integrity": "sha512-iGGw4OsAYsS6pD29MdJ2bX/nJx65a04ZZiw6x+VwWlP2DdXf6f++Zmuv/OzALpdyfVhjbduIIF2cXM7HWBIe9A==", "dev": true, "funding": [ { @@ -1065,9 +800,9 @@ } }, "node_modules/@daml/types": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@daml/types/-/types-3.5.0.tgz", - "integrity": "sha512-KsXYCUwlFA5lbb19cSfQW86239S4PHWkYDXkNPYPk6gl1BEj2AJR7P7qKsl5uJo+tsLR+Av3raLUnfZd2iOKqQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@daml/types/-/types-3.5.2.tgz", + "integrity": "sha512-lWAXNOl1tdzodvDDw7XTv6dUZA0MeAQZtTklh+5kegNt1asKAhreppYkD+bogYU1DnMXUTeMgGmmMBZt1gZpJg==", "license": "Apache-2.0", "dependencies": { "@mojotech/json-type-validation": "^3.1.0", @@ -1075,446 +810,73 @@ "lodash": "^4.5" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], - "dev": true, + "node_modules/@ethereumjs/common": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", + "integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==", "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" + "dependencies": { + "@ethereumjs/util": "^8.1.0", + "crc-32": "^1.2.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@ethereumjs/rlp": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", + "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", + "license": "MPL-2.0", + "bin": { + "rlp": "bin/rlp" + }, "engines": { - "node": ">=12" + "node": ">=14" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "node_modules/@ethereumjs/tx": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-4.2.0.tgz", + "integrity": "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==", + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/common": "^3.2.0", + "@ethereumjs/rlp": "^4.0.1", + "@ethereumjs/util": "^8.1.0", + "ethereum-cryptography": "^2.0.0" + }, "engines": { - "node": ">=12" + "node": ">=14" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], + "node_modules/@ethereumjs/util": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", + "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", + "license": "MPL-2.0", + "dependencies": { + "@ethereumjs/rlp": "^4.0.1", + "ethereum-cryptography": "^2.0.0", + "micro-ftch": "^0.3.1" + }, "engines": { - "node": ">=12" + "node": ">=14" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/@ethereumjs/common": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/common/-/common-3.2.0.tgz", - "integrity": "sha512-pksvzI0VyLgmuEF2FA/JR/4/y6hcPq8OUail3/AvycBaW1d5VSauOZzqGvJ3RTmR4MU35lWE8KseKOsEhrFRBA==", - "license": "MIT", - "dependencies": { - "@ethereumjs/util": "^8.1.0", - "crc-32": "^1.2.0" - } - }, - "node_modules/@ethereumjs/rlp": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@ethereumjs/rlp/-/rlp-4.0.1.tgz", - "integrity": "sha512-tqsQiBQDQdmPWE1xkkBq4rlSW5QZpLOUJ5RJh2/9fug+q9tnUhuZoVLk7s0scUIKTOzEtR72DFBXI4WiZcMpvw==", - "license": "MPL-2.0", - "bin": { - "rlp": "bin/rlp" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@ethereumjs/tx": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/tx/-/tx-4.2.0.tgz", - "integrity": "sha512-1nc6VO4jtFd172BbSnTnDQVr9IYBFl1y4xPzZdtkrkKIncBCkdbgfdRV+MiTkJYAtTxvV12GRZLqBFT1PNK6Yw==", - "license": "MPL-2.0", - "dependencies": { - "@ethereumjs/common": "^3.2.0", - "@ethereumjs/rlp": "^4.0.1", - "@ethereumjs/util": "^8.1.0", - "ethereum-cryptography": "^2.0.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/@ethereumjs/util": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@ethereumjs/util/-/util-8.1.0.tgz", - "integrity": "sha512-zQ0IqbdX8FZ9aw11vP+dZkKDkS+kgIvQPHnSAXzP9pLu+Rfu3D3XEeLbicvoXJTYnhZiPmsZUxgdzXwNKxRPbA==", - "license": "MPL-2.0", - "dependencies": { - "@ethereumjs/rlp": "^4.0.1", - "ethereum-cryptography": "^2.0.0", - "micro-ftch": "^0.3.1" - }, - "engines": { - "node": ">=14" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, "node_modules/@exodus/schemasafe": { @@ -1535,36 +897,6 @@ "typescript": "^5" } }, - "node_modules/@fivenorth/loop-sdk/node_modules/qrcode": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", - "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", - "license": "MIT", - "dependencies": { - "dijkstrajs": "^1.0.1", - "pngjs": "^5.0.0", - "yargs": "^15.3.1" - }, - "bin": { - "qrcode": "bin/qrcode" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -1576,17 +908,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1654,9 +975,9 @@ } }, "node_modules/@metamask/superstruct": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@metamask/superstruct/-/superstruct-3.2.1.tgz", - "integrity": "sha512-fLgJnDOXFmuVlB38rUN5SmU7hAFQcCjrg3Vrxz67KTY7YHFnSNEKvX4avmEBdOI0yTCxZjwMCFEqsC8k2+Wd3g==", + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@metamask/superstruct/-/superstruct-3.4.1.tgz", + "integrity": "sha512-caTaaBUcwBGbUNf3r0uT48upX4nECRbKhQ9pPOfW4sIkfcIUUDV4S9DZxq/5fuNPVt5KWpyd5xIIz0sP+iWLlg==", "license": "MIT", "engines": { "node": ">=16.0.0" @@ -1685,17 +1006,16 @@ } }, "node_modules/@metamask/utils/node_modules/uuid": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", - "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", - "deprecated": "uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028).", + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { - "uuid": "dist/bin/uuid" + "uuid": "dist/esm/bin/uuid" } }, "node_modules/@mojotech/json-type-validation": { @@ -1759,12 +1079,12 @@ } }, "node_modules/@noble/hashes": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", - "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", "license": "MIT", "engines": { - "node": ">= 16" + "node": "^14.21.3 || >=16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -1808,60 +1128,116 @@ "node": ">= 8" } }, + "node_modules/@oxc-project/types": { + "version": "0.147.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz", + "integrity": "sha512-IJ3s6ltHLp45S0bh7phkX+gJO7A1Wuz2EaqpAhb8WjqDwbzMiWKHhyyT42tskaWjEYXtHtVCPpnBJVT9+dcRLg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@partylayer/adapter-bron": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@partylayer/adapter-bron/-/adapter-bron-0.2.10.tgz", - "integrity": "sha512-QYQv/Fu4uNSrLVakg1lYMXJVt4im0TwuRU9KVNdDNEnfl7Qhlj8VSqkZeqtLjvjqma/H7LO7d6Ztx+l7PsxFSA==", + "version": "0.2.21", + "resolved": "https://registry.npmjs.org/@partylayer/adapter-bron/-/adapter-bron-0.2.21.tgz", + "integrity": "sha512-EDv7xfiqqYCOyr7W2EJdXEgQ0JkMTVd8gE/Lr+0ybNLaRo4bv57SDVsJIh+dS+HXltDAkptfT4VZX/CcRJ5vdw==", "license": "MIT", "dependencies": { - "@partylayer/core": "^0.3.0" + "@partylayer/core": "^0.12.1" } }, + "node_modules/@partylayer/adapter-bron/node_modules/@partylayer/core": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@partylayer/core/-/core-0.12.1.tgz", + "integrity": "sha512-IsXwB5FWMPbh5iUDAm0dnsOE7P7GpsAPzG6zm4opxd/Gn+ieimiVITAowu6VyGts9m/FIEgWYtN3SwkEtAxUJg==", + "license": "MIT" + }, "node_modules/@partylayer/adapter-cantor8": { - "version": "0.2.10", - "resolved": "https://registry.npmjs.org/@partylayer/adapter-cantor8/-/adapter-cantor8-0.2.10.tgz", - "integrity": "sha512-CGt3e8XpcZIqNy5MANK52pTrrK34aQ8ndKFeGcs/DoaMNSZ9oJliAqHbHop+bKco94VOps9MY68ZvdybuzL2kg==", + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/@partylayer/adapter-cantor8/-/adapter-cantor8-0.2.20.tgz", + "integrity": "sha512-wcAZuzK+g7mGSgncnYgTPl2ukfIuRZc5tYeS5DXNbw0lSqr1ekof5tj/rbRBklsToSH3tOLavKDeULVQwpz7Mg==", "license": "MIT", "dependencies": { - "@partylayer/core": "^0.3.0" + "@partylayer/core": "^0.12.1" } }, + "node_modules/@partylayer/adapter-cantor8/node_modules/@partylayer/core": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@partylayer/core/-/core-0.12.1.tgz", + "integrity": "sha512-IsXwB5FWMPbh5iUDAm0dnsOE7P7GpsAPzG6zm4opxd/Gn+ieimiVITAowu6VyGts9m/FIEgWYtN3SwkEtAxUJg==", + "license": "MIT" + }, "node_modules/@partylayer/adapter-console": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@partylayer/adapter-console/-/adapter-console-0.3.4.tgz", - "integrity": "sha512-ExmfzeX+IO8lQVkGr7awlrsDZ9EwoMkplNzm8ludYNofLvaCDNImijvVkiP2+M8oEyy993gkldC3TkcdktG9HQ==", + "version": "0.3.19", + "resolved": "https://registry.npmjs.org/@partylayer/adapter-console/-/adapter-console-0.3.19.tgz", + "integrity": "sha512-m+5pRH07SRha4epoLftEy6kw77qgCs1RuGRA8iTJx4hsE4FsdnyIWCvgMS80m2+J6t+m4rDEPlyzzNFlSoNojg==", "license": "MIT", "dependencies": { - "@console-wallet/dapp-sdk": "^2.1.5", - "@partylayer/core": "^0.3.0" + "@console-wallet/dapp-sdk": "^2.2.8", + "@partylayer/core": "^0.13.0" } }, + "node_modules/@partylayer/adapter-console/node_modules/@partylayer/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@partylayer/core/-/core-0.13.0.tgz", + "integrity": "sha512-lj6irpqCWft+F5H8Xz6FSzCEJ3Fr5/pAdRPFwJjxaNToMhifKWV2sr7fT/PQgODbdcWAE9CisYNtQxCVre6hJw==", + "license": "MIT" + }, "node_modules/@partylayer/adapter-loop": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@partylayer/adapter-loop/-/adapter-loop-0.3.7.tgz", - "integrity": "sha512-KNohs3qkt3/3bivKySqHvlnItFGMMc5UzrbPC6xNsmwge+viMa9QtqaVMfYtnV1win2gjzKaEJmQfaPkQAgevg==", + "version": "0.3.15", + "resolved": "https://registry.npmjs.org/@partylayer/adapter-loop/-/adapter-loop-0.3.15.tgz", + "integrity": "sha512-Su9lbdCyLOL64Hiit3mucC+9W2tjFbpfymh88ZXli+QttsE1bTrJqFiC47A4PhyOhRHFt8gtlQI4fp3Zxfo8UA==", "license": "MIT", "dependencies": { "@fivenorth/loop-sdk": "^0.10.0", - "@partylayer/core": "^0.3.0" + "@partylayer/core": "^0.10.0" } }, + "node_modules/@partylayer/adapter-loop/node_modules/@partylayer/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@partylayer/core/-/core-0.10.0.tgz", + "integrity": "sha512-uL588lHoBcF0ZPJyS3sLZMXgh/Pih16etHxSzqE/ilqLAnIrCt79fxGa06i5ASBVJCCOu0uX88Lk8iWJjPH+/w==", + "license": "MIT" + }, "node_modules/@partylayer/adapter-nightly": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@partylayer/adapter-nightly/-/adapter-nightly-0.2.9.tgz", - "integrity": "sha512-UrH64dFqcqx7f4zLrQ+XNQLLXSJGBDsyGgEnzI9tLsPG6luggWUfyw0rHibGR0S0eCr7yL5ciZXzq2NvRuuWog==", + "version": "0.2.21", + "resolved": "https://registry.npmjs.org/@partylayer/adapter-nightly/-/adapter-nightly-0.2.21.tgz", + "integrity": "sha512-FUi+OZdV+4q49sdHDkXpgFhhbIlhst0irjg7Cxhvmbp+QZaCtUv/s5uWBVIp4GyEh8eGMpf3fOpbNcfCCXCKCA==", "license": "MIT", "dependencies": { - "@partylayer/core": "^0.3.0" + "@partylayer/core": "^0.13.0" } }, + "node_modules/@partylayer/adapter-nightly/node_modules/@partylayer/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@partylayer/core/-/core-0.13.0.tgz", + "integrity": "sha512-lj6irpqCWft+F5H8Xz6FSzCEJ3Fr5/pAdRPFwJjxaNToMhifKWV2sr7fT/PQgODbdcWAE9CisYNtQxCVre6hJw==", + "license": "MIT" + }, "node_modules/@partylayer/adapter-send": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@partylayer/adapter-send/-/adapter-send-1.0.3.tgz", - "integrity": "sha512-ROdH7EwM3/msgYxDtAhKlVmvVHc5HCsJIRZtx8iQvqGrhMUU7G/WszxKID8x26DrwIa8igoRWtWPEtiJYCqSoQ==", + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@partylayer/adapter-send/-/adapter-send-1.2.7.tgz", + "integrity": "sha512-w4qUCiz/ABfHVQRdlL4aV1DUIjaku95/CSKy/wDLeKmZ6pLlyruGfGrFJh9kBMio4r0zdeiN+Up1qPIr4OfYzg==", + "license": "MIT", + "dependencies": { + "@partylayer/core": "^0.13.0", + "@partylayer/provider": "^0.5.2" + } + }, + "node_modules/@partylayer/adapter-send/node_modules/@partylayer/core": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@partylayer/core/-/core-0.13.0.tgz", + "integrity": "sha512-lj6irpqCWft+F5H8Xz6FSzCEJ3Fr5/pAdRPFwJjxaNToMhifKWV2sr7fT/PQgODbdcWAE9CisYNtQxCVre6hJw==", + "license": "MIT" + }, + "node_modules/@partylayer/adapter-send/node_modules/@partylayer/provider": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/@partylayer/provider/-/provider-0.5.2.tgz", + "integrity": "sha512-2aPAGMP3YdaPDWvBCURPaRQSs4pEvaynKsscCtSZc6NkhWpuHnYlVioCDEdrxSFrbXYjJhd0sbcCU4chYt++XA==", "license": "MIT", "dependencies": { - "@partylayer/core": "^0.3.1" + "@partylayer/core": "^0.13.0" } }, "node_modules/@partylayer/core": { @@ -1880,14 +1256,20 @@ } }, "node_modules/@partylayer/registry-client": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@partylayer/registry-client/-/registry-client-0.3.1.tgz", - "integrity": "sha512-etl6kmEdJ/MZhegxpmsEiZJyDfK78I6PIWmAJvvHmBFz77vMx8v24/5GKSFpvs7gCBfSFlpJDcUvjYK8vzeuqQ==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@partylayer/registry-client/-/registry-client-0.3.3.tgz", + "integrity": "sha512-rr+lZAsLVd+FDxdcv8YgBYNUVgQdffIcodGudk09YtnnrMkAKlhApObBV1J9YuP/2hx63ftjhG1/9SRdmy++IQ==", "license": "MIT", "dependencies": { - "@partylayer/core": "^0.3.0" + "@partylayer/core": "^0.5.0" } }, + "node_modules/@partylayer/registry-client/node_modules/@partylayer/core": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@partylayer/core/-/core-0.5.0.tgz", + "integrity": "sha512-w1kewR3O9bzJGakZImE4gW/6EJ8rFRE2fE2teWnc1hGOsBkvzpenrVe6L+FOAM/fDnU+xFZTKBWnyRh8qLdHow==", + "license": "MIT" + }, "node_modules/@partylayer/sdk": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@partylayer/sdk/-/sdk-0.4.1.tgz", @@ -1952,30 +1334,21 @@ "@protobuf-ts/runtime": "^2.11.1" } }, - "node_modules/@remix-run/router": { - "version": "1.23.2", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", - "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/@reown/appkit": { - "version": "1.8.19", - "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.8.19.tgz", - "integrity": "sha512-wB+xatkRbOy0AY1cZxxtcKzzPk3l3CTFulDbaISLVmZI6ZnQrOFuLnYc285zGsC6DB4d6bmwYUh89zcMLa4PvQ==", + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/@reown/appkit/-/appkit-1.8.23.tgz", + "integrity": "sha512-oW97J0ZJ1tEsrdDz2fzilsP8m0+9q8CT0uxMOpZbz3ZTkpn4Vo7QCafAbTTYVO5XYjIWztoQtXCW/McGc0hWbg==", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@reown/appkit-common": "1.8.19", - "@reown/appkit-controllers": "1.8.19", - "@reown/appkit-pay": "1.8.19", - "@reown/appkit-polyfills": "1.8.19", - "@reown/appkit-scaffold-ui": "1.8.19", - "@reown/appkit-ui": "1.8.19", - "@reown/appkit-utils": "1.8.19", - "@reown/appkit-wallet": "1.8.19", + "@reown/appkit-common": "1.8.23", + "@reown/appkit-controllers": "1.8.23", + "@reown/appkit-pay": "1.8.23", + "@reown/appkit-polyfills": "1.8.23", + "@reown/appkit-scaffold-ui": "1.8.23", + "@reown/appkit-ui": "1.8.23", + "@reown/appkit-utils": "1.8.23", + "@reown/appkit-wallet": "1.8.23", "@walletconnect/universal-provider": "2.23.7", "bs58": "6.0.0", "semver": "7.7.2", @@ -1987,9 +1360,9 @@ } }, "node_modules/@reown/appkit-common": { - "version": "1.8.19", - "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.8.19.tgz", - "integrity": "sha512-z5wDrYjUGY7YbM4b14NHVo54WKZ5++PQtGkcsXhiOP39yAVijubBQD8BfHs/Pu2fSFqnqLIFoCVvIEfNWWccRw==", + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/@reown/appkit-common/-/appkit-common-1.8.23.tgz", + "integrity": "sha512-6rl19bQvgXTKsN99wza3AOjZyhKI/9tUo9Jfqg+5aV4TPn3fK9OV6jVuPAQKXtujbM6kkjb6TbVizWqXMLIo7Q==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "big.js": "6.2.2", @@ -1997,260 +1370,184 @@ "viem": ">=2.45.0" } }, + "node_modules/@reown/appkit-common/node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/@reown/appkit-controllers": { - "version": "1.8.19", - "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.8.19.tgz", - "integrity": "sha512-JFNT8CfAVit9FJXh596Ye4U8A/oIapW+Y0KQqjB59DXyTCDZbxZDB32rULBQrSkZ6PufTEa239Dil4kABCQKtg==", + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/@reown/appkit-controllers/-/appkit-controllers-1.8.23.tgz", + "integrity": "sha512-nNG7Xp9bjEhyBj7F7nuStXbg1k0Vv7gteqI2TyKD8Bbov1F7j/KyjC2WQoYqfvhOSZtX361FZdemqvrQjT7ytw==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@reown/appkit-common": "1.8.19", - "@reown/appkit-wallet": "1.8.19", + "@reown/appkit-common": "1.8.23", + "@reown/appkit-wallet": "1.8.23", "@walletconnect/universal-provider": "2.23.7", "valtio": "2.1.7", "viem": ">=2.45.0" } }, "node_modules/@reown/appkit-pay": { - "version": "1.8.19", - "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.8.19.tgz", - "integrity": "sha512-HO/tQT0TbTQO3eONxNNPJAOZAOzUiHvjM0Mty1rFFeRBH68auiqQxQi2YFNMs014gNkRN+cb84VYau7+MCC0fQ==", + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/@reown/appkit-pay/-/appkit-pay-1.8.23.tgz", + "integrity": "sha512-uYkCOeDzCE/rPWVl8qL2dj058RvzOaXSgnrkTGZ92tmEwr5Km23U3v2KeEhLhUw0JYTlVLyTrM44zYQr5j1Y0Q==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@reown/appkit-common": "1.8.19", - "@reown/appkit-controllers": "1.8.19", - "@reown/appkit-ui": "1.8.19", - "@reown/appkit-utils": "1.8.19", + "@reown/appkit-common": "1.8.23", + "@reown/appkit-controllers": "1.8.23", + "@reown/appkit-ui": "1.8.23", + "@reown/appkit-utils": "1.8.23", "lit": "3.3.0", "valtio": "2.1.7" } }, + "node_modules/@reown/appkit-pay/node_modules/lit": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", + "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, "node_modules/@reown/appkit-polyfills": { - "version": "1.8.19", - "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.8.19.tgz", - "integrity": "sha512-PSoetRSuZg7f2YFPzdfs4BayQl51zcGqYr7frwOe6td0XEsspLrrVFn/zk5QFbFHZVsMdfRZ+TTunt84ozRdnQ==", + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/@reown/appkit-polyfills/-/appkit-polyfills-1.8.23.tgz", + "integrity": "sha512-4UtoYy9Nva0/iDwSqiX+EuD0Kpgy+TM2+nry+un/i0zbjshmiepBPyuc3ds52T8agjq2yQKJ9SapA9O+KT9PlA==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "buffer": "6.0.3" } }, "node_modules/@reown/appkit-scaffold-ui": { - "version": "1.8.19", - "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.8.19.tgz", - "integrity": "sha512-Ak767x0VzeDIXb0wbzkl19kx6udw7vkb1EU0SAweG3iKc9BunW87Rfcd48/YimzMZycJaYmlbtfmqQQDYs6Few==", + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/@reown/appkit-scaffold-ui/-/appkit-scaffold-ui-1.8.23.tgz", + "integrity": "sha512-wR3g5bgojukamAdDCMs1PFEdpx69FLoFvMontSzRsV2MKYaUOqYycw6mJsMPmA4t+LCt5IU3otClTT+ONOp7UA==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@reown/appkit-common": "1.8.19", - "@reown/appkit-controllers": "1.8.19", - "@reown/appkit-pay": "1.8.19", - "@reown/appkit-ui": "1.8.19", - "@reown/appkit-utils": "1.8.19", - "@reown/appkit-wallet": "1.8.19", + "@reown/appkit-common": "1.8.23", + "@reown/appkit-controllers": "1.8.23", + "@reown/appkit-pay": "1.8.23", + "@reown/appkit-ui": "1.8.23", + "@reown/appkit-utils": "1.8.23", + "@reown/appkit-wallet": "1.8.23", "lit": "3.3.0" } }, + "node_modules/@reown/appkit-scaffold-ui/node_modules/lit": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", + "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, "node_modules/@reown/appkit-ui": { - "version": "1.8.19", - "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.8.19.tgz", - "integrity": "sha512-fCAwW8yyyC3JcgKLBPvCtYuDGC4H8anO7u4LTaAXGEzdcU5H+IrCgNFSPNK7NuTSmgXm1TnoYxPxRFKNiNwFdA==", + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/@reown/appkit-ui/-/appkit-ui-1.8.23.tgz", + "integrity": "sha512-wR1+CWh0hKM4jIW7fMOdXofIUwX3TaeyJyUQ6jxXc0S/iu80S/nEoa/i5VW0qvtBAcRmnzoAtd7zRG5e8b5c1w==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@phosphor-icons/webcomponents": "2.1.5", - "@reown/appkit-common": "1.8.19", - "@reown/appkit-controllers": "1.8.19", - "@reown/appkit-wallet": "1.8.19", + "@reown/appkit-common": "1.8.23", + "@reown/appkit-controllers": "1.8.23", + "@reown/appkit-wallet": "1.8.23", "lit": "3.3.0", "qrcode": "1.5.3" } }, - "node_modules/@reown/appkit-universal-connector": { - "version": "1.8.19", - "resolved": "https://registry.npmjs.org/@reown/appkit-universal-connector/-/appkit-universal-connector-1.8.19.tgz", - "integrity": "sha512-9H9t+OkEu7jzZY5FbD8YJM29zUVPE3Xp+PNJgEIv7zcx/9JdDtK+99FvygMZmF/pm5E7Vm3oiyCCuNCkc+3BbA==", - "license": "SEE LICENSE IN LICENSE.md", + "node_modules/@reown/appkit-ui/node_modules/lit": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", + "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", + "license": "BSD-3-Clause", "dependencies": { - "@reown/appkit": "1.8.19", - "@reown/appkit-common": "1.8.19", - "@walletconnect/types": "2.23.7", - "@walletconnect/universal-provider": "2.23.7", - "bs58": "6.0.0" + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" } }, - "node_modules/@reown/appkit-universal-connector/node_modules/@walletconnect/keyvaluestorage": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", - "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "node_modules/@reown/appkit-ui/node_modules/qrcode": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", + "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", "license": "MIT", "dependencies": { - "@walletconnect/safe-json": "^1.0.1", - "idb-keyval": "^6.2.1", - "unstorage": "^1.9.0" + "dijkstrajs": "^1.0.1", + "encode-utf8": "^1.0.3", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" }, - "peerDependencies": { - "@react-native-async-storage/async-storage": "1.x" + "bin": { + "qrcode": "bin/qrcode" }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } + "engines": { + "node": ">=10.13.0" } }, - "node_modules/@reown/appkit-universal-connector/node_modules/@walletconnect/types": { - "version": "2.23.7", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.7.tgz", - "integrity": "sha512-6PAKK+iR2IntmlkCFLMAHjYeIaerCJJYRDmdRimhon0u+aNmQT+HyGM6zxDAth0rdpBD7qEvKP5IXZTE7KFUhw==", + "node_modules/@reown/appkit-universal-connector": { + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/@reown/appkit-universal-connector/-/appkit-universal-connector-1.8.23.tgz", + "integrity": "sha512-sErclUmFnwGcPqDb/uBma/6zXQwN7V7a+VEyRad9Wc6PfatCDxcGQtHCd4GXAM51a5yRYa0Mad+tPtqPXGrMBg==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "3.0.2", - "events": "3.3.0" - } - }, - "node_modules/@reown/appkit-universal-connector/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@reown/appkit-universal-connector/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@reown/appkit-universal-connector/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "@reown/appkit": "1.8.23", + "@reown/appkit-common": "1.8.23", + "@walletconnect/types": "2.23.7", + "@walletconnect/universal-provider": "2.23.7", + "bs58": "6.0.0" } }, - "node_modules/@reown/appkit-universal-connector/node_modules/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", - "license": "MIT", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^5.0.0", - "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", - "node-fetch-native": "^1.6.7", - "ofetch": "^1.5.1", - "ufo": "^1.6.3" + "node_modules/@reown/appkit-universal-connector/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" }, "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6 || ^7 || ^8", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/functions": "^2.2.12 || ^3.0.0", - "@vercel/kv": "^1 || ^2 || ^3", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" + "@react-native-async-storage/async-storage": "1.x" }, "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/functions": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { + "@react-native-async-storage/async-storage": { "optional": true } } }, + "node_modules/@reown/appkit-universal-connector/node_modules/@walletconnect/types": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.7.tgz", + "integrity": "sha512-6PAKK+iR2IntmlkCFLMAHjYeIaerCJJYRDmdRimhon0u+aNmQT+HyGM6zxDAth0rdpBD7qEvKP5IXZTE7KFUhw==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/events": "1.0.1", + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "events": "3.3.0" + } + }, "node_modules/@reown/appkit-utils": { - "version": "1.8.19", - "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.8.19.tgz", - "integrity": "sha512-VQPgUMTFqoh4UD3EDZSw9wyMkyZsmIVmu8CdQ2FUxIuqYW4fLd0VIpkDeO64MMhSv8b0X8Vd6m4+eGcqSwlUAg==", + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/@reown/appkit-utils/-/appkit-utils-1.8.23.tgz", + "integrity": "sha512-7M2pL5LqCgII1jpTqNLPtjC774yyUP8Mh2EHKYTLXW0NtSO+K8R5DfSfMygKcSiLiELe9LBVqpB4gBRML3R7zg==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@reown/appkit-common": "1.8.19", - "@reown/appkit-controllers": "1.8.19", - "@reown/appkit-polyfills": "1.8.19", - "@reown/appkit-wallet": "1.8.19", + "@reown/appkit-common": "1.8.23", + "@reown/appkit-controllers": "1.8.23", + "@reown/appkit-polyfills": "1.8.23", + "@reown/appkit-wallet": "1.8.23", "@wallet-standard/wallet": "1.1.0", "@walletconnect/logger": "3.0.2", "@walletconnect/universal-provider": "2.23.7", @@ -2259,6 +1556,7 @@ }, "optionalDependencies": { "@base-org/account": "2.4.0", + "@coinbase/wallet-sdk": "4.3.6", "@safe-global/safe-apps-provider": "0.18.6", "@safe-global/safe-apps-sdk": "9.1.0" }, @@ -2267,13 +1565,13 @@ } }, "node_modules/@reown/appkit-wallet": { - "version": "1.8.19", - "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.8.19.tgz", - "integrity": "sha512-NVdIKceUhkXYtsG32925ctmVn0QJFNyDlr+mWheMLCEZ/IUPn+6aA53vTVaSUquhyeFxUXtrCOh3ln6v1tup5w==", + "version": "1.8.23", + "resolved": "https://registry.npmjs.org/@reown/appkit-wallet/-/appkit-wallet-1.8.23.tgz", + "integrity": "sha512-IMcBTCN7jONQ3J2sjT2b3ZVAgzgn6RXh8kQf4/0cO6c0ukpQaka9/uW7BTApizRepSfAui9XuhXSZ5XOfJFnNg==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "@reown/appkit-common": "1.8.19", - "@reown/appkit-polyfills": "1.8.19", + "@reown/appkit-common": "1.8.23", + "@reown/appkit-polyfills": "1.8.23", "@walletconnect/logger": "3.0.2", "zod": "3.22.4" } @@ -2287,17 +1585,22 @@ "url": "https://github.com/sponsors/colinhacks" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.27", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", - "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", - "dev": true, - "license": "MIT" + "node_modules/@reown/appkit/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", - "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz", + "integrity": "sha512-b+jTcARdTiFLI6jB4a5XjTm0RWd6KcRfQj/I2356fxUZemiho9zQLxo0RtCuMDAyKcLo6cEltkgbQp6d1+sjjQ==", "cpu": [ "arm" ], @@ -2306,12 +1609,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", - "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.6.tgz", + "integrity": "sha512-lkWU8ZJaRk9q3CIEY1Tc7vIFALp3Xw5NfGJo2hQg5oIqNgxWi1zI+IiDEK3r70BF5Dzol1tcXsnzsRc8NLhG+Q==", "cpu": [ "arm64" ], @@ -2320,12 +1626,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", - "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.6.tgz", + "integrity": "sha512-dgR56NYnvAszm7Ob1B2/Vn0e8bUQYZH2UjVaMMtMVOCKFSfjhfLmuA/9+O+F+ajUdG6B/bSssrKW6JJYASa8jA==", "cpu": [ "arm64" ], @@ -2334,12 +1643,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", - "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.6.tgz", + "integrity": "sha512-vpVxFvUCFioJqug7OTvqptkc4yb8UX0AwfDmJpaR/0sWz+BUmqSVAf7c8JkUgnN8YLspb4a/N6NhTyMAmdyQ7Q==", "cpu": [ "x64" ], @@ -2348,26 +1660,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", - "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", - "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.6.tgz", + "integrity": "sha512-h1wG6Y6K3JlRswxsI64qQJqBAy4vrLuHgRbc8CZMGSWTOFRY6ghMApM1NKzB2I0n5xV1fjkE18SuVl2QpLeNpA==", "cpu": [ "x64" ], @@ -2376,26 +1677,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", - "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", - "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.6.tgz", + "integrity": "sha512-tbCiqub0q2MVWJKgF5PoAlNWCtQydiOYSLIkd8sByqK/6MMYLJRcSXSYodqYtd0O+Fw7QaVmKKlS4oL94YRZ0w==", "cpu": [ "arm" ], @@ -2404,26 +1694,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", - "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", - "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.6.tgz", + "integrity": "sha512-oxK9+baEBPhZG5HB4URY+uU04zJWeZlH6Tb9rB5DK4DF9XR1uXNLXt5Q5ZsugTKayNCNLhkcwz/ye74hRI98dg==", "cpu": [ "arm64" ], @@ -2432,54 +1711,32 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", - "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", - "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.6.tgz", + "integrity": "sha512-muWCk27FVBEZtv0MsK8gnfSmgczA8KQ0uRVJbTABKhkRfQc38aUrcb7fhi3BNiyseFmgcRsoMfQsSNJ+DbZdSw==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", - "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", - "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.6.tgz", + "integrity": "sha512-eWDoSfU7Co2qj3vgB3Dt4lj1mG6CoWbcJQkRMP3XJplyCMtuaq3LHvPFjS9QIPvMGWVadJC04Xiy0IdcVPtnwQ==", "cpu": [ "ppc64" ], @@ -2488,40 +1745,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", - "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", - "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", - "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.6.tgz", + "integrity": "sha512-2bWNjRSIayvupRKxXUY2tWG9fYdoUlTqWywHRvE8Eq3GvuQ+f2HeIkve697fIt+IQs/PV8yFsdWuhp1aJ1PdnA==", "cpu": [ "s390x" ], @@ -2530,12 +1762,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", - "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.6.tgz", + "integrity": "sha512-KekI0gS0wLxe1UBSQSjenBVwou/JkcQPDzBPICGZjxUv9k3RteHDPBQaiOicZUFKRIH2wKEimGwVpnJsbPzu7w==", "cpu": [ "x64" ], @@ -2544,12 +1779,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", - "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.6.tgz", + "integrity": "sha512-TvtPnfVr+HtyGiDmPK4VWmlNm7QhNNAcK5Q9A7aOXsI8545yCyaoMaicXrFZ72JzeYjaUVk7yT243zT0jzjFKQ==", "cpu": [ "x64" ], @@ -2558,26 +1796,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", - "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", - "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.6.tgz", + "integrity": "sha512-iOo0VEay2XFhaCcH0sps5XIimkSuOnNaZrf6+ZkoSOQBJPKNU48RkmJv0/lSpipexu5P+ouFgafe5IGr/DiQfg==", "cpu": [ "arm64" ], @@ -2586,54 +1813,32 @@ "optional": true, "os": [ "openharmony" - ] - }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", - "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", - "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", - "cpu": [ - "ia32" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", - "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.6.tgz", + "integrity": "sha512-y5NTmmasMS455JlOCO4ZM9krIchv3Mvm1crL1iUPGOPgEzSkves9n0SdC5Sjz6+qWDFhd8/JpfWMH8NSWNHe+A==", "cpu": [ - "x64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", - "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.6.tgz", + "integrity": "sha512-np8iZSLfXlAD4kWhiyq/u0Yt8oZDtRQ8lGhQaCXo2rl37KNjeU0GjJuwr4P3oeZ++ROfofsKNBqR5LTO8aXyWQ==", "cpu": [ "x64" ], @@ -2642,7 +1847,17 @@ "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" }, "node_modules/@safe-global/safe-apps-provider": { "version": "0.18.6", @@ -2687,77 +1902,85 @@ } }, "node_modules/@scure/bip32": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", - "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", + "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", "license": "MIT", "dependencies": { - "@noble/curves": "~1.9.0", - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@noble/curves": "~1.4.0", + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32/node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.4.2.tgz", + "integrity": "sha512-TavHr8qycMChk8UwMld0ZDRvatedkzWfH8IiaeGCfymOP5i0hSCozz9vHOL0nkwk7HRMlFnAiKpS2jrUmSybcw==", "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" + "@noble/hashes": "1.4.0" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip32/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/bip39": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", - "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", + "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", "license": "MIT", "dependencies": { - "@noble/hashes": "~1.8.0", - "@scure/base": "~1.2.5" + "@noble/hashes": "~1.4.0", + "@scure/base": "~1.1.6" }, "funding": { "url": "https://paulmillr.com/funding/" } }, "node_modules/@scure/bip39/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "license": "MIT", "engines": { - "node": "^14.21.3 || >=16" + "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" } }, - "node_modules/@sinclair/typebox": { - "version": "0.27.10", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", - "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", - "dev": true, - "license": "MIT" + "node_modules/@scure/bip39/node_modules/@scure/base": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", + "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } }, "node_modules/@solana-program/system": { "version": "0.10.0", @@ -3739,10 +2962,17 @@ } } }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@tanstack/query-core": { - "version": "5.100.10", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.100.10.tgz", - "integrity": "sha512-8UR0yJR+GiQ40m3lPhUr0xbfAupe6GSQiksSBSa9SM2NjezFyxXCIA69/lz8cSoNKZLrw1/PktIyQBJcVeMi3w==", + "version": "5.102.7", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.7.tgz", + "integrity": "sha512-4TH8KQrCLoNs9Zvl1LW1iaZ9cZx3dGTgfZ1BnLrcRxMNnX/toe/dGbfvF/DkvbzRyfLfpMKZkembz3o0rnugtw==", "license": "MIT", "funding": { "type": "github", @@ -3750,12 +2980,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.100.10", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.100.10.tgz", - "integrity": "sha512-FLaZf2RCrA/Zgp4aiu5tG3TyasTRO7aZ99skxQpr3Hg/zXOhu6yq5FZCYQ/tRaJtM9ylnoK8tFK7PolXQadv6Q==", + "version": "5.102.7", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.7.tgz", + "integrity": "sha512-bbd8T4jDIj9aPbNn11SsjNH8apKY4zspkrw63JRsNDXjpW5SRF7X0ipi7yrGd8DCs8QhYfhQJKqEj1YLru1DbQ==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.100.10" + "@tanstack/query-core": "5.102.7" }, "funding": { "type": "github", @@ -3849,49 +3079,15 @@ "license": "MIT", "peer": true }, - "node_modules/@types/babel__core": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", - "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" - } - }, - "node_modules/@types/babel__generator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", - "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__template": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", - "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" - } - }, - "node_modules/@types/babel__traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", - "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.2" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, "node_modules/@types/debug": { @@ -3903,10 +3099,17 @@ "@types/ms": "*" } }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, "license": "MIT" }, @@ -3917,9 +3120,9 @@ "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", - "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "version": "4.17.25", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.25.tgz", + "integrity": "sha512-+K1NIO8I+F9/wNulfVvu23QYd0Pe9/OCqRrim4NoYIf1VoEDL90Ve4ClzpyqBLc7NpGGWRvYNCKZ1BE/Jpf8dQ==", "license": "MIT" }, "node_modules/@types/ms": { @@ -3929,9 +3132,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "25.9.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz", - "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==", + "version": "25.9.5", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.5.tgz", + "integrity": "sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==", "license": "MIT", "dependencies": { "undici-types": ">=7.24.0 <7.24.7" @@ -3960,9 +3163,9 @@ } }, "node_modules/@types/react": { - "version": "18.3.28", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", - "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "version": "18.3.31", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", + "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3991,501 +3194,236 @@ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", "license": "MIT" - }, - "node_modules/@vitejs/plugin-react": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", - "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/core": "^7.28.0", - "@babel/plugin-transform-react-jsx-self": "^7.27.1", - "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.27", - "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" - }, - "engines": { - "node": "^14.18.0 || >=16.0.0" - }, - "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" - } - }, - "node_modules/@vitest/expect": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.1.tgz", - "integrity": "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "chai": "^4.3.10" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.1.tgz", - "integrity": "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/snapshot": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.1.tgz", - "integrity": "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/snapshot/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@vitest/spy": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.1.tgz", - "integrity": "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyspy": "^2.2.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/ui": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-1.6.1.tgz", - "integrity": "sha512-xa57bCPGuzEFqGjPs3vVLyqareG8DX0uMkr5U/v5vLv5/ZUrBrPL7gzxzTJedEyZxFMfsozwTIbbYfEQVo3kgg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "1.6.1", - "fast-glob": "^3.3.2", - "fflate": "^0.8.1", - "flatted": "^3.2.9", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "sirv": "^2.0.4" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "vitest": "1.6.1" - } - }, - "node_modules/@vitest/utils": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.1.tgz", - "integrity": "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "diff-sequences": "^29.6.3", - "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@wallet-standard/base": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", - "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=16" - } - }, - "node_modules/@wallet-standard/wallet": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wallet-standard/wallet/-/wallet-1.1.0.tgz", - "integrity": "sha512-Gt8TnSlDZpAl+RWOOAB/kuvC7RpcdWAlFbHNoi4gsXsfaWa1QCT6LBcfIYTPdOZC9OVZUDwqGuGAcqZejDmHjg==", - "license": "Apache-2.0", - "dependencies": { - "@wallet-standard/base": "^1.1.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/@walletconnect/core": { - "version": "2.23.9", - "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.9.tgz", - "integrity": "sha512-ws4WG8LeagUo2ERRo02HryXRcpwIRmCQ3pHLW5gWbVReLXXIpgk6ZAfID3fEGHevIwwnHSGww+nNhNpdXyiq0g==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-provider": "1.0.14", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/jsonrpc-ws-connection": "1.0.16", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "3.0.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.23.9", - "@walletconnect/utils": "2.23.9", - "@walletconnect/window-getters": "1.0.1", - "es-toolkit": "1.44.0", - "events": "3.3.0", - "uint8arrays": "3.1.1" - }, - "engines": { - "node": ">=18.20.8" - } - }, - "node_modules/@walletconnect/core/node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz", + "integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { - "node": "^14.21.3 || >=16" + "node": "^20.19.0 || >=22.12.0" }, - "funding": { - "url": "https://paulmillr.com/funding/" + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } } }, - "node_modules/@walletconnect/core/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@walletconnect/core/node_modules/@walletconnect/keyvaluestorage": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", - "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, "license": "MIT", "dependencies": { - "@walletconnect/safe-json": "^1.0.1", - "idb-keyval": "^6.2.1", - "unstorage": "^1.9.0" + "@vitest/spy": "4.1.11", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@react-native-async-storage/async-storage": "1.x" + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { + "msw": { + "optional": true + }, + "vite": { "optional": true } } }, - "node_modules/@walletconnect/core/node_modules/@walletconnect/utils": { - "version": "2.23.9", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.9.tgz", - "integrity": "sha512-C5TltCs8UPypNiteYnKSv8+ZDK2EjVDyXCxN6kA9bkA+j6KGsNIV7l9MUA8WBAvE5Gi5EcBdhD3R9Hpo/1HHqQ==", - "license": "SEE LICENSE IN LICENSE.md", + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", "dependencies": { - "@msgpack/msgpack": "3.1.3", - "@noble/ciphers": "1.3.0", - "@noble/curves": "1.9.7", - "@noble/hashes": "1.8.0", - "@scure/base": "1.2.6", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "3.0.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.23.9", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "blakejs": "1.2.1", - "detect-browser": "5.3.0", - "ox": "0.9.3", - "uint8arrays": "3.1.1" + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@walletconnect/core/node_modules/abitype": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", - "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==", + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@walletconnect/core/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, "license": "MIT", "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@walletconnect/core/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/@walletconnect/core/node_modules/ox": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz", - "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], + "node_modules/@vitest/ui": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.11.tgz", + "integrity": "sha512-r/rwyKoev21mWdRGSEkZOqkQ2BYy68mwjihg9M90nNRbf4NGrgzZ4cj6JNCEwlOGJkbKeMgsjlykvwKUbRr7gw==", + "dev": true, "license": "MIT", "dependencies": { - "@adraffy/ens-normalize": "^1.11.0", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "1.9.1", - "@noble/hashes": "^1.8.0", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "abitype": "^1.0.9", - "eventemitter3": "5.0.1" + "@vitest/utils": "4.1.11", + "fflate": "^0.8.2", + "flatted": "^3.4.2", + "pathe": "^2.0.3", + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0" }, - "peerDependencies": { - "typescript": ">=5.4.0" + "funding": { + "url": "https://opencollective.com/vitest" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "vitest": "4.1.11" } }, - "node_modules/@walletconnect/core/node_modules/ox/node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, "license": "MIT", "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { - "url": "https://paulmillr.com/funding/" + "url": "https://opencollective.com/vitest" } }, - "node_modules/@walletconnect/core/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", + "node_modules/@wallet-standard/base": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.1.tgz", + "integrity": "sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ==", + "license": "Apache-2.0", "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "node": ">=22" } }, - "node_modules/@walletconnect/core/node_modules/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", - "license": "MIT", + "node_modules/@wallet-standard/wallet": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/wallet/-/wallet-1.1.0.tgz", + "integrity": "sha512-Gt8TnSlDZpAl+RWOOAB/kuvC7RpcdWAlFbHNoi4gsXsfaWa1QCT6LBcfIYTPdOZC9OVZUDwqGuGAcqZejDmHjg==", + "license": "Apache-2.0", "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^5.0.0", - "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", - "node-fetch-native": "^1.6.7", - "ofetch": "^1.5.1", - "ufo": "^1.6.3" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6 || ^7 || ^8", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/functions": "^2.2.12 || ^3.0.0", - "@vercel/kv": "^1 || ^2 || ^3", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" + "@wallet-standard/base": "^1.1.0" }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/functions": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { + "engines": { + "node": ">=16" + } + }, + "node_modules/@walletconnect/core": { + "version": "2.23.10", + "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.10.tgz", + "integrity": "sha512-Qq2btHEoCgruvkZCWLSrVsvg/dYbM9Z045qeClwhJR4meL32jbIRT0mKWjf0HkRc2LA82MsnszVnfuZl3yWl5A==", + "license": "SEE LICENSE IN LICENSE.md", + "dependencies": { + "@walletconnect/heartbeat": "1.2.2", + "@walletconnect/jsonrpc-provider": "1.0.14", + "@walletconnect/jsonrpc-types": "1.0.4", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/jsonrpc-ws-connection": "1.0.16", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.10", + "@walletconnect/utils": "2.23.10", + "@walletconnect/window-getters": "1.0.1", + "es-toolkit": "1.45.1", + "events": "3.3.0", + "uint8arrays": "3.1.1" + }, + "engines": { + "node": ">=18.20.8" + } + }, + "node_modules/@walletconnect/core/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "license": "MIT", + "dependencies": { + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" + }, + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { "optional": true } } @@ -4499,6 +3437,12 @@ "tslib": "1.14.1" } }, + "node_modules/@walletconnect/environment/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/@walletconnect/events": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@walletconnect/events/-/events-1.0.1.tgz", @@ -4509,6 +3453,12 @@ "tslib": "1.14.1" } }, + "node_modules/@walletconnect/events/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/@walletconnect/heartbeat": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@walletconnect/heartbeat/-/heartbeat-1.2.2.tgz", @@ -4564,6 +3514,12 @@ "tslib": "1.14.1" } }, + "node_modules/@walletconnect/jsonrpc-utils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/@walletconnect/jsonrpc-ws-connection": { "version": "1.0.16", "resolved": "https://registry.npmjs.org/@walletconnect/jsonrpc-ws-connection/-/jsonrpc-ws-connection-1.0.16.tgz", @@ -4577,9 +3533,9 @@ } }, "node_modules/@walletconnect/jsonrpc-ws-connection/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "version": "7.5.13", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz", + "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==", "license": "MIT", "engines": { "node": ">=8.3.0" @@ -4607,6 +3563,46 @@ "pino": "10.0.0" } }, + "node_modules/@walletconnect/logger/node_modules/pino": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", + "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^2.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "slow-redact": "^0.3.0", + "sonic-boom": "^4.0.1", + "thread-stream": "^3.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/@walletconnect/logger/node_modules/pino-abstract-transport": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", + "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/@walletconnect/logger/node_modules/thread-stream": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.2.0.tgz", + "integrity": "sha512-zLBvqpwr4Esa0kRjcrzGU6zL25lePWaCLMx0RQFrmteozIfeNdaMLpG5U7PeHzvlFkAWaRKA9/KVW4F60iB+qw==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + } + }, "node_modules/@walletconnect/relay-api": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@walletconnect/relay-api/-/relay-api-1.0.11.tgz", @@ -4650,293 +3646,25 @@ "tslib": "1.14.1" } }, - "node_modules/@walletconnect/sign-client": { - "version": "2.23.9", - "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.9.tgz", - "integrity": "sha512-Xj+hw4E6mGRyhCdVOT/RMgnG+up/Y3v0ho5PlkVozvXWeVSqHNh9DmjLuU97a7OACoGd/oHBF6g3NVqD7MgCMQ==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@walletconnect/core": "2.23.9", - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/logger": "3.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.23.9", - "@walletconnect/utils": "2.23.9", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/sign-client/node_modules/@noble/curves": { - "version": "1.9.7", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", - "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/sign-client/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", - "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/sign-client/node_modules/@walletconnect/keyvaluestorage": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", - "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", - "license": "MIT", - "dependencies": { - "@walletconnect/safe-json": "^1.0.1", - "idb-keyval": "^6.2.1", - "unstorage": "^1.9.0" - }, - "peerDependencies": { - "@react-native-async-storage/async-storage": "1.x" - }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } - } - }, - "node_modules/@walletconnect/sign-client/node_modules/@walletconnect/utils": { - "version": "2.23.9", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.9.tgz", - "integrity": "sha512-C5TltCs8UPypNiteYnKSv8+ZDK2EjVDyXCxN6kA9bkA+j6KGsNIV7l9MUA8WBAvE5Gi5EcBdhD3R9Hpo/1HHqQ==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@msgpack/msgpack": "3.1.3", - "@noble/ciphers": "1.3.0", - "@noble/curves": "1.9.7", - "@noble/hashes": "1.8.0", - "@scure/base": "1.2.6", - "@walletconnect/jsonrpc-utils": "1.0.8", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "3.0.2", - "@walletconnect/relay-api": "1.0.11", - "@walletconnect/relay-auth": "1.1.0", - "@walletconnect/safe-json": "1.0.2", - "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.23.9", - "@walletconnect/window-getters": "1.0.1", - "@walletconnect/window-metadata": "1.0.1", - "blakejs": "1.2.1", - "detect-browser": "5.3.0", - "ox": "0.9.3", - "uint8arrays": "3.1.1" - } - }, - "node_modules/@walletconnect/sign-client/node_modules/abitype": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", - "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/@walletconnect/sign-client/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/sign-client/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@walletconnect/sign-client/node_modules/ox": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz", - "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/wevm" - } - ], - "license": "MIT", - "dependencies": { - "@adraffy/ens-normalize": "^1.11.0", - "@noble/ciphers": "^1.3.0", - "@noble/curves": "1.9.1", - "@noble/hashes": "^1.8.0", - "@scure/bip32": "^1.7.0", - "@scure/bip39": "^1.6.0", - "abitype": "^1.0.9", - "eventemitter3": "5.0.1" - }, - "peerDependencies": { - "typescript": ">=5.4.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@walletconnect/sign-client/node_modules/ox/node_modules/@noble/curves": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", - "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", - "license": "MIT", - "dependencies": { - "@noble/hashes": "1.8.0" - }, - "engines": { - "node": "^14.21.3 || >=16" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/sign-client/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } + "node_modules/@walletconnect/safe-json/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" }, - "node_modules/@walletconnect/sign-client/node_modules/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", - "license": "MIT", + "node_modules/@walletconnect/sign-client": { + "version": "2.23.10", + "resolved": "https://registry.npmjs.org/@walletconnect/sign-client/-/sign-client-2.23.10.tgz", + "integrity": "sha512-vO7DGRRmKo+rykmjVyQR1aM4I2nbk9kJ6olbxgjFRR6Jdhy+Kz+zgN7Ce5xVhPfWYVu4bV/XhOQxhvnQw7S5ng==", + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^5.0.0", - "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", - "node-fetch-native": "^1.6.7", - "ofetch": "^1.5.1", - "ufo": "^1.6.3" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6 || ^7 || ^8", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/functions": "^2.2.12 || ^3.0.0", - "@vercel/kv": "^1 || ^2 || ^3", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/functions": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { - "optional": true - } + "@walletconnect/core": "2.23.10", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/logger": "3.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.10", + "@walletconnect/utils": "2.23.10", + "events": "3.3.0" } }, "node_modules/@walletconnect/time": { @@ -4948,10 +3676,16 @@ "tslib": "1.14.1" } }, + "node_modules/@walletconnect/time/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/@walletconnect/types": { - "version": "2.23.9", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.9.tgz", - "integrity": "sha512-IUl1PpD/Dig8IE2OZ9XtjbPohEyOZJ73xs92EDUzoIyzRtfm36g2D340pY3iu3AAdLv1yFiaZafB8Hf8RFze8A==", + "version": "2.23.10", + "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.10.tgz", + "integrity": "sha512-XP8d41979anTrc1OJF3ISF+g81cvp1wim+ObdNnbcaT/jhwLwv+0T7rRe9VwRv+h8EaRgLyeb5YGy7oJ49vxVg==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@walletconnect/events": "1.0.1", @@ -4981,139 +3715,6 @@ } } }, - "node_modules/@walletconnect/types/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/types/node_modules/lru-cache": { - "version": "11.5.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.0.tgz", - "integrity": "sha512-5YgH9UJd7wVb9hIouI2adWpgqrrICkt070Dnj8EUY1+B4B2P9eRLPAkAAo6NICA7CEhOIeBHl46u9zSNpNu7zA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/@walletconnect/types/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/types/node_modules/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", - "license": "MIT", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^5.0.0", - "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", - "node-fetch-native": "^1.6.7", - "ofetch": "^1.5.1", - "ufo": "^1.6.3" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6 || ^7 || ^8", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/functions": "^2.2.12 || ^3.0.0", - "@vercel/kv": "^1 || ^2 || ^3", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/functions": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { - "optional": true - } - } - }, "node_modules/@walletconnect/universal-provider": { "version": "2.23.7", "resolved": "https://registry.npmjs.org/@walletconnect/universal-provider/-/universal-provider-2.23.7.tgz", @@ -5134,6 +3735,48 @@ "events": "3.3.0" } }, + "node_modules/@walletconnect/universal-provider/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@walletconnect/universal-provider/node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@walletconnect/universal-provider/node_modules/@walletconnect/core": { "version": "2.23.7", "resolved": "https://registry.npmjs.org/@walletconnect/core/-/core-2.23.7.tgz", @@ -5212,143 +3855,92 @@ "events": "3.3.0" } }, - "node_modules/@walletconnect/universal-provider/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", + "node_modules/@walletconnect/universal-provider/node_modules/@walletconnect/utils": { + "version": "2.23.7", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.7.tgz", + "integrity": "sha512-3p38gNrkVcIiQixVrlsWSa66Gjs5PqHOug2TxDgYUVBW5NcKjwQA08GkC6CKBQUfr5iaCtbfy6uZJW1LKSIvWQ==", + "license": "SEE LICENSE IN LICENSE.md", "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/universal-provider/node_modules/lru-cache": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", - "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" + "@msgpack/msgpack": "3.1.3", + "@noble/ciphers": "1.3.0", + "@noble/curves": "1.9.7", + "@noble/hashes": "1.8.0", + "@scure/base": "1.2.6", + "@walletconnect/jsonrpc-utils": "1.0.8", + "@walletconnect/keyvaluestorage": "1.1.1", + "@walletconnect/logger": "3.0.2", + "@walletconnect/relay-api": "1.0.11", + "@walletconnect/relay-auth": "1.1.0", + "@walletconnect/safe-json": "1.0.2", + "@walletconnect/time": "1.0.2", + "@walletconnect/types": "2.23.7", + "@walletconnect/window-getters": "1.0.1", + "@walletconnect/window-metadata": "1.0.1", + "blakejs": "1.2.1", + "detect-browser": "5.3.0", + "ox": "0.9.3", + "uint8arrays": "3.1.1" } }, - "node_modules/@walletconnect/universal-provider/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "node_modules/@walletconnect/universal-provider/node_modules/es-toolkit": { + "version": "1.44.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", + "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } + "workspaces": [ + "docs", + "benchmarks" + ] }, - "node_modules/@walletconnect/universal-provider/node_modules/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "node_modules/@walletconnect/universal-provider/node_modules/ox": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.9.3.tgz", + "integrity": "sha512-KzyJP+fPV4uhuuqrTZyok4DC7vFzi7HLUFiUNEmpbyh59htKWkOC98IONC1zgXJPbHAhQgqs6B0Z6StCGhmQvg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], "license": "MIT", "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^5.0.0", - "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", - "node-fetch-native": "^1.6.7", - "ofetch": "^1.5.1", - "ufo": "^1.6.3" + "@adraffy/ens-normalize": "^1.11.0", + "@noble/ciphers": "^1.3.0", + "@noble/curves": "1.9.1", + "@noble/hashes": "^1.8.0", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "abitype": "^1.0.9", + "eventemitter3": "5.0.1" }, "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6 || ^7 || ^8", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/functions": "^2.2.12 || ^3.0.0", - "@vercel/kv": "^1 || ^2 || ^3", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" + "typescript": ">=5.4.0" }, "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/functions": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { + "typescript": { "optional": true } } }, + "node_modules/@walletconnect/universal-provider/node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.1.tgz", + "integrity": "sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@walletconnect/utils": { - "version": "2.23.7", - "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.7.tgz", - "integrity": "sha512-3p38gNrkVcIiQixVrlsWSa66Gjs5PqHOug2TxDgYUVBW5NcKjwQA08GkC6CKBQUfr5iaCtbfy6uZJW1LKSIvWQ==", + "version": "2.23.10", + "resolved": "https://registry.npmjs.org/@walletconnect/utils/-/utils-2.23.10.tgz", + "integrity": "sha512-b1c9FRF2g7vNnz66oLW5WZD2VCMrbu9xhpmwJJwqGarBiGW7cY8NbUtS9/w2/qc0vsBVKJ/bzDn4TGjpELU6aQ==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@msgpack/msgpack": "3.1.3", @@ -5363,7 +3955,7 @@ "@walletconnect/relay-auth": "1.1.0", "@walletconnect/safe-json": "1.0.2", "@walletconnect/time": "1.0.2", - "@walletconnect/types": "2.23.7", + "@walletconnect/types": "2.23.10", "@walletconnect/window-getters": "1.0.1", "@walletconnect/window-metadata": "1.0.1", "blakejs": "1.2.1", @@ -5387,94 +3979,50 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@walletconnect/utils/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "node_modules/@walletconnect/utils/node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" }, "funding": { "url": "https://paulmillr.com/funding/" } }, - "node_modules/@walletconnect/utils/node_modules/@walletconnect/keyvaluestorage": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", - "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", + "node_modules/@walletconnect/utils/node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", "license": "MIT", "dependencies": { - "@walletconnect/safe-json": "^1.0.1", - "idb-keyval": "^6.2.1", - "unstorage": "^1.9.0" - }, - "peerDependencies": { - "@react-native-async-storage/async-storage": "1.x" + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" }, - "peerDependenciesMeta": { - "@react-native-async-storage/async-storage": { - "optional": true - } - } - }, - "node_modules/@walletconnect/utils/node_modules/@walletconnect/types": { - "version": "2.23.7", - "resolved": "https://registry.npmjs.org/@walletconnect/types/-/types-2.23.7.tgz", - "integrity": "sha512-6PAKK+iR2IntmlkCFLMAHjYeIaerCJJYRDmdRimhon0u+aNmQT+HyGM6zxDAth0rdpBD7qEvKP5IXZTE7KFUhw==", - "license": "SEE LICENSE IN LICENSE.md", - "dependencies": { - "@walletconnect/events": "1.0.1", - "@walletconnect/heartbeat": "1.2.2", - "@walletconnect/jsonrpc-types": "1.0.4", - "@walletconnect/keyvaluestorage": "1.1.1", - "@walletconnect/logger": "3.0.2", - "events": "3.3.0" - } - }, - "node_modules/@walletconnect/utils/node_modules/abitype": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.2.4.tgz", - "integrity": "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg==", - "license": "MIT", "funding": { - "url": "https://github.com/sponsors/wevm" - }, - "peerDependencies": { - "typescript": ">=5.0.4", - "zod": "^3.22.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - }, - "zod": { - "optional": true - } + "url": "https://paulmillr.com/funding/" } }, - "node_modules/@walletconnect/utils/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "node_modules/@walletconnect/utils/node_modules/@walletconnect/keyvaluestorage": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@walletconnect/keyvaluestorage/-/keyvaluestorage-1.1.1.tgz", + "integrity": "sha512-V7ZQq2+mSxAq7MrRqDxanTzu2RcElfK1PfNYiaVnJgJ7Q7G7hTVwF8voIBx92qsRyGHZihrwNPHuZd1aKkd0rA==", "license": "MIT", "dependencies": { - "readdirp": "^5.0.0" + "@walletconnect/safe-json": "^1.0.1", + "idb-keyval": "^6.2.1", + "unstorage": "^1.9.0" }, - "engines": { - "node": ">= 20.19.0" + "peerDependencies": { + "@react-native-async-storage/async-storage": "1.x" }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/lru-cache": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", - "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } } }, "node_modules/@walletconnect/utils/node_modules/ox": { @@ -5522,115 +4070,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@walletconnect/utils/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/@walletconnect/utils/node_modules/unstorage": { - "version": "1.17.5", - "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", - "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", - "license": "MIT", - "dependencies": { - "anymatch": "^3.1.3", - "chokidar": "^5.0.0", - "destr": "^2.0.5", - "h3": "^1.15.10", - "lru-cache": "^11.2.7", - "node-fetch-native": "^1.6.7", - "ofetch": "^1.5.1", - "ufo": "^1.6.3" - }, - "peerDependencies": { - "@azure/app-configuration": "^1.8.0", - "@azure/cosmos": "^4.2.0", - "@azure/data-tables": "^13.3.0", - "@azure/identity": "^4.6.0", - "@azure/keyvault-secrets": "^4.9.0", - "@azure/storage-blob": "^12.26.0", - "@capacitor/preferences": "^6 || ^7 || ^8", - "@deno/kv": ">=0.9.0", - "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", - "@planetscale/database": "^1.19.0", - "@upstash/redis": "^1.34.3", - "@vercel/blob": ">=0.27.1", - "@vercel/functions": "^2.2.12 || ^3.0.0", - "@vercel/kv": "^1 || ^2 || ^3", - "aws4fetch": "^1.0.20", - "db0": ">=0.2.1", - "idb-keyval": "^6.2.1", - "ioredis": "^5.4.2", - "uploadthing": "^7.4.4" - }, - "peerDependenciesMeta": { - "@azure/app-configuration": { - "optional": true - }, - "@azure/cosmos": { - "optional": true - }, - "@azure/data-tables": { - "optional": true - }, - "@azure/identity": { - "optional": true - }, - "@azure/keyvault-secrets": { - "optional": true - }, - "@azure/storage-blob": { - "optional": true - }, - "@capacitor/preferences": { - "optional": true - }, - "@deno/kv": { - "optional": true - }, - "@netlify/blobs": { - "optional": true - }, - "@planetscale/database": { - "optional": true - }, - "@upstash/redis": { - "optional": true - }, - "@vercel/blob": { - "optional": true - }, - "@vercel/functions": { - "optional": true - }, - "@vercel/kv": { - "optional": true - }, - "aws4fetch": { - "optional": true - }, - "db0": { - "optional": true - }, - "idb-keyval": { - "optional": true - }, - "ioredis": { - "optional": true - }, - "uploadthing": { - "optional": true - } - } - }, "node_modules/@walletconnect/window-getters": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@walletconnect/window-getters/-/window-getters-1.0.1.tgz", @@ -5640,6 +4079,12 @@ "tslib": "1.14.1" } }, + "node_modules/@walletconnect/window-getters/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/@walletconnect/window-metadata": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@walletconnect/window-metadata/-/window-metadata-1.0.1.tgz", @@ -5650,18 +4095,23 @@ "tslib": "1.14.1" } }, + "node_modules/@walletconnect/window-metadata/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/abitype": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.6.tgz", - "integrity": "sha512-MMSqYh4+C/aVqI2RQaWqbvI4Kxo5cQV40WQ4QFtDnNzCkqChm8MuENhElmynZlO0qUy/ObkEUaXtKqYnx1Kp3A==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.3.0.tgz", + "integrity": "sha512-fk6Te+bojIFrMvMZrnOO+SxCB+RUksTGOzq/60ZRvs1L+BVzvi2bqt9L3W/17ZLdZsyM1FuYf65P5nlmoiH1Bg==", "license": "MIT", - "optional": true, "funding": { "url": "https://github.com/sponsors/wevm" }, "peerDependencies": { "typescript": ">=5.0.4", - "zod": "^3 >=3.22.0" + "zod": "^3.22.0 || ^4.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -5672,30 +4122,16 @@ } } }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.5", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", - "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", - "dev": true, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", "license": "MIT", "dependencies": { - "acorn": "^8.11.0" + "debug": "4" }, "engines": { - "node": ">=0.4.0" + "node": ">= 6.0.0" } }, "node_modules/ajv": { @@ -5738,16 +4174,15 @@ } }, "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "color-convert": "^1.9.0" }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "engines": { + "node": ">=4" } }, "node_modules/any-promise": { @@ -5831,13 +4266,13 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "license": "MIT", "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/async-function": { @@ -5865,9 +4300,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.0.tgz", - "integrity": "sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==", + "version": "10.5.4", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.5.4.tgz", + "integrity": "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA==", "dev": true, "funding": [ { @@ -5885,8 +4320,8 @@ ], "license": "MIT", "dependencies": { - "browserslist": "^4.28.2", - "caniuse-lite": "^1.0.30001787", + "browserslist": "^4.28.6", + "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" @@ -5917,13 +4352,14 @@ } }, "node_modules/axios": { - "version": "1.16.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.16.0.tgz", - "integrity": "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w==", + "version": "1.20.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.20.0.tgz", + "integrity": "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==", "license": "MIT", "dependencies": { "follow-redirects": "^1.16.0", - "form-data": "^4.0.5", + "form-data": "^4.0.6", + "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, @@ -5973,9 +4409,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.30", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.30.tgz", - "integrity": "sha512-xjOFN16Ha1+Rz4nFYKqHU/LSB+gx/Vi3yQLX7r7sAW+Wa+8hhF2h4pvqTrTMc8+WcDBEunnUurr46Jvv0jk3Vg==", + "version": "2.11.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.19.tgz", + "integrity": "sha512-Grytf1xOxOEMTGRwx6rLGKkTabd4vMg3VrKdj/7joCmV0qgh4QwMMO6xh34YEXQqirAuUdgQGa5orJQQ+69RBw==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6053,9 +4489,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -6076,9 +4512,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", - "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", "dev": true, "funding": [ { @@ -6096,11 +4532,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.10.12", - "caniuse-lite": "^1.0.30001782", - "electron-to-chromium": "^1.5.328", - "node-releases": "^2.0.36", - "update-browserslist-db": "^1.2.3" + "baseline-browser-mapping": "^2.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" }, "bin": { "browserslist": "cli.js" @@ -6170,76 +4606,6 @@ } } }, - "node_modules/c12/node_modules/chokidar": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", - "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", - "license": "MIT", - "dependencies": { - "readdirp": "^5.0.0" - }, - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/c12/node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", - "license": "MIT" - }, - "node_modules/c12/node_modules/jiti": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", - "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" - } - }, - "node_modules/c12/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "license": "MIT" - }, - "node_modules/c12/node_modules/pkg-types": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", - "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", - "license": "MIT", - "dependencies": { - "confbox": "^0.2.4", - "exsolve": "^1.0.8", - "pathe": "^2.0.3" - } - }, - "node_modules/c12/node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", - "license": "MIT", - "engines": { - "node": ">= 20.19.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/cac": { - "version": "6.7.14", - "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", - "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -6294,12 +4660,15 @@ "license": "MIT" }, "node_modules/camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-9.0.0.tgz", + "integrity": "sha512-TO9xmyXTZ9HUHI8M1OnvExxYB0eYVS/1e5s7IDMTAoIcwUd+aNcFODs6Xk83mobk0velyHFQgA1yIrvYc6wclw==", "license": "MIT", "engines": { - "node": ">=6" + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/camelcase-css": { @@ -6330,22 +4699,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/camelcase-keys/node_modules/camelcase": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-9.0.0.tgz", - "integrity": "sha512-TO9xmyXTZ9HUHI8M1OnvExxYB0eYVS/1e5s7IDMTAoIcwUd+aNcFODs6Xk83mobk0velyHFQgA1yIrvYc6wclw==", - "license": "MIT", - "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/caniuse-lite": { - "version": "1.0.30001793", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001793.tgz", - "integrity": "sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==", + "version": "1.0.30001810", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", + "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", "dev": true, "funding": [ { @@ -6364,22 +4721,13 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.5.0.tgz", - "integrity": "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.1.0" - }, "engines": { - "node": ">=4" + "node": ">=18" } }, "node_modules/chalk": { @@ -6405,55 +4753,19 @@ "node": "*" } }, - "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.2" - }, - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", "license": "MIT", "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "readdirp": "^5.0.0" }, "engines": { - "node": ">= 8.10.0" + "node": ">= 20.19.0" }, "funding": { "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" } }, "node_modules/citty": { @@ -6505,21 +4817,18 @@ } }, "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", "license": "MIT", "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" + "color-name": "1.1.3" } }, "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, "node_modules/combined-stream": { @@ -6551,10 +4860,9 @@ "license": "MIT" }, "node_modules/confbox": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", - "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", - "dev": true, + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", "license": "MIT" }, "node_modules/consola": { @@ -6573,6 +4881,19 @@ "dev": true, "license": "MIT" }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/cookie-es": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", @@ -6601,18 +4922,28 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", "license": "MIT", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" }, "engines": { - "node": ">= 8" + "node": ">=4.8" + } + }, + "node_modules/cross-spawn/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" } }, "node_modules/crossws": { @@ -6741,9 +5072,9 @@ } }, "node_modules/dayjs": { - "version": "1.11.13", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", - "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", "license": "MIT" }, "node_modules/debug": { @@ -6779,19 +5110,6 @@ "dev": true, "license": "MIT" }, - "node_modules/deep-eql": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.4.tgz", - "integrity": "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -6863,6 +5181,16 @@ "integrity": "sha512-53rsFbGdwMwlF7qvCt0ypLM5V5/Mbl0szB7GPN8y9NCcbknYOeVVXdrXEq+90IwAfrrzt6Hd+u2E2ntakICU8w==", "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -6870,16 +5198,6 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dijkstrajs": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", @@ -6902,9 +5220,9 @@ "peer": true }, "node_modules/dompurify": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.8.tgz", - "integrity": "sha512-yb1cEmaOum7wFvOCSQxyfgVlv5D47Rc30iZWoMpbDIWTnJ6grDDQyu2KFJzB2k7u0pMuJcQ1zphH//fFnw2tjQ==", + "version": "3.4.14", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.14.tgz", + "integrity": "sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -6937,9 +5255,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.357", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.357.tgz", - "integrity": "sha512-NHlTIQDK8fmVwHwuIzmXYEJ1Ewq3D9wDNc0cWXxDGysP6Pb21giwGNkxiTifyKy/4SoPuN5l6GLP1W9Sv7zB2g==", + "version": "1.5.415", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.415.tgz", + "integrity": "sha512-958V+Kbhtgz+SxXeEVKBjrlKRBIDAYvUJfwhjxMZ5S6ut9jAl7l9ZKBkBrvjyjZE36PabLUo2L8kEeV5O4vgJg==", "dev": true, "license": "ISC" }, @@ -7045,6 +5363,24 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -7063,10 +5399,17 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7091,14 +5434,17 @@ } }, "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.4.tgz", + "integrity": "sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==", "license": "MIT", "dependencies": { + "es-abstract-get": "^1.0.0", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -7108,9 +5454,9 @@ } }, "node_modules/es-toolkit": { - "version": "1.44.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.44.0.tgz", - "integrity": "sha512-6penXeZalaV88MM3cGkFZZfOoLGWshWWfdy0tWw/RlVVyhvMaWSBTOvXNeiW3e5FwdS5ePW0LGEu17zT139ktg==", + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", "license": "MIT", "workspaces": [ "docs", @@ -7123,45 +5469,6 @@ "integrity": "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==", "license": "MIT" }, - "node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -7226,37 +5533,13 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/ethereum-cryptography/node_modules/@scure/base": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.1.9.tgz", - "integrity": "sha512-8YKhl8GHiNI/pU2VMaofa2Tor7PJRAjwQLBBuilkJ9L5+13yVbC7JO/wS7piioAvPSwR3JKM1IJ/u4xQzbcXKg==", - "license": "MIT", - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/bip32": { + "node_modules/ethereum-cryptography/node_modules/@noble/hashes": { "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.4.0.tgz", - "integrity": "sha512-sVUpc0Vq3tXCkDGYVWGIZTRfnvu8LoTDaev7vbwh0omSvVORONr960MQWdKqJDCReIEmTj3PAr73O3aoxz7OPg==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.4.0", - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/ethereum-cryptography/node_modules/@scure/bip39": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.3.0.tgz", - "integrity": "sha512-disdg7gHuTDZtY+ZdkmLpPCk7fxZSu3gBiEGuoC1XYxv9cGx3Z6cpTggCgW6odSOOIXCiDjuGejW+aJKCY/pIQ==", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz", + "integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==", "license": "MIT", - "dependencies": { - "@noble/hashes": "~1.4.0", - "@scure/base": "~1.1.6" + "engines": { + "node": ">= 16" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -7277,34 +5560,20 @@ "node": ">=0.8.x" } }, - "node_modules/execa": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", - "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", "dev": true, - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^8.0.1", - "human-signals": "^5.0.0", - "is-stream": "^3.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^5.1.0", - "onetime": "^6.0.0", - "signal-exit": "^4.1.0", - "strip-final-newline": "^3.0.0" - }, + "license": "Apache-2.0", "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" + "node": ">=12.0.0" } }, "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -7330,19 +5599,6 @@ "node": ">=8.6.0" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -7350,9 +5606,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.6.tgz", + "integrity": "sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==", "funding": [ { "type": "github", @@ -7409,9 +5665,9 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -7451,16 +5707,16 @@ } }, "node_modules/form-data": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", - "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.6.tgz", + "integrity": "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", - "hasown": "^2.0.2", - "mime-types": "^2.1.12" + "hasown": "^2.0.4", + "mime-types": "^2.1.35" }, "engines": { "node": ">= 6" @@ -7505,17 +5761,20 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", - "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.2.0.tgz", + "integrity": "sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", - "hasown": "^2.0.2", - "is-callable": "^1.2.7" + "has-property-descriptors": "^1.0.2", + "hasown": "^2.0.4", + "is-callable": "^1.2.7", + "is-document.all": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -7542,16 +5801,6 @@ "node": ">= 0.4" } }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -7561,16 +5810,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", - "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", @@ -7608,19 +5847,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", - "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -7639,9 +5865,9 @@ } }, "node_modules/giget": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/giget/-/giget-3.2.0.tgz", - "integrity": "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A==", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/giget/-/giget-3.3.1.tgz", + "integrity": "sha512-r+mvuDjrjMpsdw46Kmeydb8bdHm7wOKw8wNBtTndkjbPjgAp5oUJUxRE76wZFknxIPokfWvep2qSXK37aXE6zg==", "license": "MIT", "bin": { "giget": "dist/cli.mjs" @@ -7665,16 +5891,16 @@ } }, "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, "license": "ISC", "dependencies": { - "is-glob": "^4.0.3" + "is-glob": "^4.0.1" }, "engines": { - "node": ">=10.13.0" + "node": ">= 6" } }, "node_modules/glob/node_modules/balanced-match": { @@ -7687,24 +5913,24 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "license": "MIT", "dependencies": { "balanced-match": "^4.0.2" }, "engines": { - "node": "18 || 20 || >=22" + "node": "20 || >=22" } }, "node_modules/glob/node_modules/minimatch": { - "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.5" + "brace-expansion": "^5.0.8" }, "engines": { "node": "18 || 20 || >=22" @@ -7840,9 +6066,9 @@ } }, "node_modules/hasown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", - "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7870,38 +6096,23 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/html-encoding-sniffer/node_modules/@exodus/bytes": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", - "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, "node_modules/http2-client": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/http2-client/-/http2-client-1.3.5.tgz", "integrity": "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA==", "license": "MIT" }, - "node_modules/human-signals": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", - "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", - "dev": true, - "license": "Apache-2.0", + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, "engines": { - "node": ">=16.17.0" + "node": ">= 6" } }, "node_modules/idb-keyval": { @@ -8122,6 +6333,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-document.all": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-document.all/-/is-document.all-1.0.0.tgz", + "integrity": "sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==", + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -8303,19 +6529,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", - "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-string": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", @@ -8435,19 +6648,18 @@ } }, "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "dev": true, + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", "license": "MIT", "bin": { - "jiti": "bin/jiti.js" + "jiti": "lib/jiti-cli.mjs" } }, "node_modules/jose": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.3.tgz", - "integrity": "sha512-YYVDInQKFJfR/xa3ojUTl8c2KoTwiL1R5Wg9YCydwH0x0B9grbzlg5HC7mMjCtUJjbQ/YnGEZIhI5tCgfTb4Hw==", + "version": "6.2.10", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.10.tgz", + "integrity": "sha512-iiW7J9qRFlGxvCOIBDBDxFePQSn7ZMAnrYGhrrOo6siO/MIqwfyilLR27pkfDgUk+raLuzADS8A3S/KLBisc0g==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" @@ -8460,9 +6672,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "funding": [ { "type": "github", @@ -8522,94 +6734,324 @@ } } }, - "node_modules/jsdom/node_modules/@exodus/bytes": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", - "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "node_modules/jsdom/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "license": "MIT" + }, + "node_modules/json-schema-to-openapi-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema-to-openapi-schema/-/json-schema-to-openapi-schema-0.4.0.tgz", + "integrity": "sha512-/DY8s4l28M5ZIJBhmcUFWbZChJV5v7RCA7RMVxubyD1l5KwIceUq6+EUnqQ2q3wh/2D3Zn8bNSeAu1i2X+sMHQ==", + "deprecated": "This package is no longer maintained. Use @openapi-contrib/json-schema-to-openapi-schema instead.", + "license": "MIT", + "dependencies": { + "@cloudflare/json-schema-walker": "^0.1.1" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=6" + } + }, + "node_modules/keyvaluestorage-interface": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", + "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", + "license": "MIT" + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "license": "MIT", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" + "engines": { + "node": ">= 12.0.0" }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jsdom/node_modules/lru-cache": { - "version": "11.4.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", - "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BlueOak-1.0.0", + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "20 || >=22" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "license": "MIT" - }, - "node_modules/json-schema-to-openapi-schema": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/json-schema-to-openapi-schema/-/json-schema-to-openapi-schema-0.4.0.tgz", - "integrity": "sha512-/DY8s4l28M5ZIJBhmcUFWbZChJV5v7RCA7RMVxubyD1l5KwIceUq6+EUnqQ2q3wh/2D3Zn8bNSeAu1i2X+sMHQ==", - "deprecated": "This package is no longer maintained. Use @openapi-contrib/json-schema-to-openapi-schema instead.", - "license": "MIT", - "dependencies": { - "@cloudflare/json-schema-walker": "^0.1.1" - } - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "node": ">= 12.0.0" }, - "engines": { - "node": ">=6" - } - }, - "node_modules/keyvaluestorage-interface": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/keyvaluestorage-interface/-/keyvaluestorage-interface-1.0.0.tgz", - "integrity": "sha512-8t6Q3TclQ4uZynJY9IGr2+SsIGwK9JHcO6ootkHCGA0CrQCRy+VkouYNO2xicET6b9al7QKzpebNow+gkpCL8g==", - "license": "MIT" - }, - "node_modules/lie": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", - "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", - "license": "MIT", - "dependencies": { - "immediate": "~3.0.5" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/lilconfig": { @@ -8633,9 +7075,9 @@ "license": "MIT" }, "node_modules/lit": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.0.tgz", - "integrity": "sha512-DGVsqsOIHBww2DqnuZzW7QsuCdahp50ojuDaBPC7jUDRpYoH0z7kHBBYZewRzer75FwtrkmkKk7iOAwSaWdBmw==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", + "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", "license": "BSD-3-Clause", "dependencies": { "@lit/reactive-element": "^2.1.0", @@ -8678,32 +7120,6 @@ "node": ">=4" } }, - "node_modules/load-json-file/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/local-pkg": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.1.tgz", - "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mlly": "^1.7.3", - "pkg-types": "^1.2.1" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/localforage": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", @@ -8750,26 +7166,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "get-func-name": "^2.0.1" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, "node_modules/lz-string": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", @@ -8839,13 +7235,6 @@ "node": ">= 0.10.0" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true, - "license": "MIT" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -8897,19 +7286,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", - "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8941,26 +7317,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/mlly": { - "version": "1.8.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", - "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "^8.16.0", - "pathe": "^2.0.3", - "pkg-types": "^1.3.1", - "ufo": "^1.6.3" - } - }, - "node_modules/mlly/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -8996,9 +7352,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "dev": true, "funding": [ { @@ -9081,149 +7437,43 @@ } }, "node_modules/node-mock-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", - "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", - "license": "MIT" - }, - "node_modules/node-readfiles": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", - "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", - "license": "MIT", - "dependencies": { - "es6-promise": "^3.2.1" - } - }, - "node_modules/node-releases": { - "version": "2.0.44", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.44.tgz", - "integrity": "sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "license": "BSD-2-Clause", - "dependencies": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-all": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", - "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "chalk": "^2.4.1", - "cross-spawn": "^6.0.5", - "memorystream": "^0.3.1", - "minimatch": "^3.0.4", - "pidtree": "^0.3.0", - "read-pkg": "^3.0.0", - "shell-quote": "^1.6.1", - "string.prototype.padend": "^3.0.0" - }, - "bin": { - "npm-run-all": "bin/npm-run-all/index.js", - "run-p": "bin/run-p/index.js", - "run-s": "bin/run-s/index.js" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/npm-run-all/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-all/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/npm-run-all/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/npm-run-all/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, - "node_modules/npm-run-all/node_modules/cross-spawn": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", - "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", + "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", + "license": "MIT" + }, + "node_modules/node-readfiles": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/node-readfiles/-/node-readfiles-0.2.0.tgz", + "integrity": "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA==", "license": "MIT", "dependencies": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, - "engines": { - "node": ">=4.8" + "es6-promise": "^3.2.1" } }, - "node_modules/npm-run-all/node_modules/path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", + "node_modules/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" } }, - "node_modules/npm-run-all/node_modules/semver": { + "node_modules/normalize-package-data/node_modules/semver": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", @@ -9232,66 +7482,52 @@ "semver": "bin/semver" } }, - "node_modules/npm-run-all/node_modules/shebang-command": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "license": "MIT", - "dependencies": { - "shebang-regex": "^1.0.0" - }, "engines": { "node": ">=0.10.0" } }, - "node_modules/npm-run-all/node_modules/shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "node_modules/npm-run-all": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/npm-run-all/-/npm-run-all-4.1.5.tgz", + "integrity": "sha512-Oo82gJDAVcaMdi3nuoKFavkIHBRVqQ1qvMb+9LHk/cF4P6B2m8aP04hGf7oL6wZ9BuGwX1onlLhpuoofSyoQDQ==", "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/npm-run-all/node_modules/which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "license": "ISC", "dependencies": { - "isexe": "^2.0.0" + "ansi-styles": "^3.2.1", + "chalk": "^2.4.1", + "cross-spawn": "^6.0.5", + "memorystream": "^0.3.1", + "minimatch": "^3.0.4", + "pidtree": "^0.3.0", + "read-pkg": "^3.0.0", + "shell-quote": "^1.6.1", + "string.prototype.padend": "^3.0.0" }, "bin": { - "which": "bin/which" - } - }, - "node_modules/npm-run-path": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", - "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^4.0.0" + "npm-run-all": "bin/npm-run-all/index.js", + "run-p": "bin/run-p/index.js", + "run-s": "bin/run-s/index.js" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">= 4" } }, - "node_modules/npm-run-path/node_modules/path-key": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", - "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", - "dev": true, + "node_modules/npm-run-all/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=4" } }, "node_modules/oas-kit-common": { @@ -9374,6 +7610,24 @@ "node": ">=12" } }, + "node_modules/oas-resolver/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/oas-resolver/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/oas-resolver/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -9410,9 +7664,9 @@ } }, "node_modules/oas-resolver/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -9534,6 +7788,20 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, "node_modules/ofetch": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", @@ -9546,9 +7814,9 @@ } }, "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", "license": "MIT" }, "node_modules/on-exit-leak-free": { @@ -9560,22 +7828,6 @@ "node": ">=14.0.0" } }, - "node_modules/onetime": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", - "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "mimic-fn": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/openapi-fetch": { "version": "0.17.0", "resolved": "https://registry.npmjs.org/openapi-fetch/-/openapi-fetch-0.17.0.tgz", @@ -9598,12 +7850,13 @@ "license": "MIT" }, "node_modules/own-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", - "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.2.tgz", + "integrity": "sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==", "license": "MIT", "dependencies": { - "get-intrinsic": "^1.2.6", + "call-bound": "^1.0.4", + "get-intrinsic": "^1.3.0", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" }, @@ -9644,12 +7897,15 @@ } } }, - "node_modules/ox/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "node_modules/ox/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", "license": "MIT", "optional": true, + "dependencies": { + "@noble/hashes": "1.8.0" + }, "engines": { "node": "^14.21.3 || >=16" }, @@ -9657,19 +7913,33 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "node_modules/ox/node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", "license": "MIT", + "optional": true, "dependencies": { - "p-try": "^2.0.0" + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" }, - "engines": { - "node": ">=6" + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ox/node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "optional": true, + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://paulmillr.com/funding/" } }, "node_modules/p-locate": { @@ -9684,6 +7954,21 @@ "node": ">=8" } }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -9729,13 +8014,12 @@ } }, "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=4" } }, "node_modules/path-parse": { @@ -9761,9 +8045,9 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "11.5.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", - "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" @@ -9781,32 +8065,12 @@ "node": ">=4" } }, - "node_modules/path-type/node_modules/pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", - "dev": true, + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "license": "MIT" }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, "node_modules/perfect-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", @@ -9845,41 +8109,40 @@ } }, "node_modules/pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", - "dev": true, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=4" } }, "node_modules/pino": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/pino/-/pino-10.0.0.tgz", - "integrity": "sha512-eI9pKwWEix40kfvSzqEP6ldqOoBIN7dwD/o91TY5z8vQI12sAffpR/pOqAD1IVVwIVHDpHjkq0joBPdJD0rafA==", + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", "license": "MIT", "dependencies": { + "@pinojs/redact": "^0.4.0", "atomic-sleep": "^1.0.0", "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "^2.0.0", + "pino-abstract-transport": "^3.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", - "slow-redact": "^0.3.0", "sonic-boom": "^4.0.1", - "thread-stream": "^3.0.0" + "thread-stream": "^4.0.0" }, "bin": { "pino": "bin.js" } }, "node_modules/pino-abstract-transport": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-2.0.0.tgz", - "integrity": "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", "license": "MIT", "dependencies": { "split2": "^4.0.0" @@ -9902,23 +8165,15 @@ } }, "node_modules/pkg-types": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", - "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", - "dev": true, + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", "license": "MIT", "dependencies": { - "confbox": "^0.1.8", - "mlly": "^1.7.4", - "pathe": "^2.0.1" - } - }, - "node_modules/pkg-types/node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } }, "node_modules/pngjs": { "version": "5.0.0", @@ -9948,9 +8203,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "dev": true, "funding": [ { @@ -9968,7 +8223,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -10090,9 +8345,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10137,10 +8392,24 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process-warning": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", - "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", "funding": [ { "type": "github", @@ -10179,13 +8448,12 @@ } }, "node_modules/qrcode": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", - "integrity": "sha512-puyri6ApkEHYiVl4CFzo1tDkAZ+ATcnbJrJ6RiBM1Fhctdn/ix9MTE3hRph33omisEbC/2fcfemsseiKgBPKZg==", + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", "license": "MIT", "dependencies": { "dijkstrajs": "^1.0.1", - "encode-utf8": "^1.0.3", "pngjs": "^5.0.0", "yargs": "^15.3.1" }, @@ -10290,57 +8558,50 @@ "license": "MIT", "peer": true }, - "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/react-router": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", - "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8" + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, "node_modules/react-router-dom": { - "version": "6.30.3", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", - "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.2", - "react-router": "6.30.3" + "react-router": "7.18.2" }, "engines": { - "node": ">=14.0.0" + "node": ">=20.0.0" }, "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" + "react": ">=18", + "react-dom": ">=18" } }, "node_modules/read-cache": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", - "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.2.tgz", + "integrity": "sha512-/peqiBB/n07gQGLsWaHho3WfvUyRscw0gYTsEFMhrIe/nWLkYaf5SbKYjGYqtRV3aPwykJgF2VEMo1ac4bnsGA==", "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^2.3.0" - } + "license": "MIT" }, "node_modules/read-pkg": { "version": "3.0.0", @@ -10357,16 +8618,16 @@ } }, "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, "engines": { - "node": ">=8.10.0" + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, "node_modules/real-require": { @@ -10499,49 +8760,38 @@ "node": ">=0.10.0" } }, - "node_modules/rollup": { - "version": "4.60.4", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", - "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", + "node_modules/rolldown": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz", + "integrity": "sha512-vMM4q3aixf46GiF1Kok8jDPFsEpXgFWGjUHXNkNHNm+Y2adXAG2dbX91jkti3i0ZRsOlcmbuzAz1poObSHCmUA==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.147.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.4", - "@rollup/rollup-android-arm64": "4.60.4", - "@rollup/rollup-darwin-arm64": "4.60.4", - "@rollup/rollup-darwin-x64": "4.60.4", - "@rollup/rollup-freebsd-arm64": "4.60.4", - "@rollup/rollup-freebsd-x64": "4.60.4", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", - "@rollup/rollup-linux-arm-musleabihf": "4.60.4", - "@rollup/rollup-linux-arm64-gnu": "4.60.4", - "@rollup/rollup-linux-arm64-musl": "4.60.4", - "@rollup/rollup-linux-loong64-gnu": "4.60.4", - "@rollup/rollup-linux-loong64-musl": "4.60.4", - "@rollup/rollup-linux-ppc64-gnu": "4.60.4", - "@rollup/rollup-linux-ppc64-musl": "4.60.4", - "@rollup/rollup-linux-riscv64-gnu": "4.60.4", - "@rollup/rollup-linux-riscv64-musl": "4.60.4", - "@rollup/rollup-linux-s390x-gnu": "4.60.4", - "@rollup/rollup-linux-x64-gnu": "4.60.4", - "@rollup/rollup-linux-x64-musl": "4.60.4", - "@rollup/rollup-openbsd-x64": "4.60.4", - "@rollup/rollup-openharmony-arm64": "4.60.4", - "@rollup/rollup-win32-arm64-msvc": "4.60.4", - "@rollup/rollup-win32-ia32-msvc": "4.60.4", - "@rollup/rollup-win32-x64-gnu": "4.60.4", - "@rollup/rollup-win32-x64-msvc": "4.60.4", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm-eabi": "1.2.6", + "@rolldown/binding-android-arm64": "1.2.6", + "@rolldown/binding-darwin-arm64": "1.2.6", + "@rolldown/binding-darwin-x64": "1.2.6", + "@rolldown/binding-freebsd-x64": "1.2.6", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.6", + "@rolldown/binding-linux-arm64-gnu": "1.2.6", + "@rolldown/binding-linux-arm64-musl": "1.2.6", + "@rolldown/binding-linux-ppc64-gnu": "1.2.6", + "@rolldown/binding-linux-s390x-gnu": "1.2.6", + "@rolldown/binding-linux-x64-gnu": "1.2.6", + "@rolldown/binding-linux-x64-musl": "1.2.6", + "@rolldown/binding-openharmony-arm64": "1.2.6", + "@rolldown/binding-win32-arm64-msvc": "1.2.6", + "@rolldown/binding-win32-x64-msvc": "1.2.6" } }, "node_modules/run-parallel": { @@ -10652,9 +8902,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10669,6 +8919,12 @@ "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", "license": "ISC" }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -10716,32 +8972,30 @@ } }, "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "shebang-regex": "^1.0.0" }, "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=0.10.0" } }, "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -10805,14 +9059,14 @@ "license": "MIT" }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -10883,23 +9137,10 @@ "dev": true, "license": "ISC" }, - "node_modules/signal-exit": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", - "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -10908,7 +9149,7 @@ "totalist": "^3.0.0" }, "engines": { - "node": ">= 10" + "node": ">=18" } }, "node_modules/slow-redact": { @@ -10985,9 +9226,9 @@ "license": "MIT" }, "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", "dev": true, "license": "MIT" }, @@ -11037,18 +9278,19 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", - "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.11.tgz", + "integrity": "sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.5", - "es-object-atoms": "^1.0.0", - "has-property-descriptors": "^1.0.2" + "es-abstract": "^1.24.2", + "es-object-atoms": "^1.1.2", + "has-property-descriptors": "^1.0.2", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -11058,15 +9300,15 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", - "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.10.tgz", + "integrity": "sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==", "license": "MIT", "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.2", + "call-bind": "^1.0.9", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0" + "es-object-atoms": "^1.1.2" }, "engines": { "node": ">= 0.4" @@ -11113,19 +9355,6 @@ "node": ">=4" } }, - "node_modules/strip-final-newline": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", - "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -11139,26 +9368,6 @@ "node": ">=8" } }, - "node_modules/strip-literal": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.1.tgz", - "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -11223,28 +9432,28 @@ "license": "ISC" }, "node_modules/swagger-typescript-api": { - "version": "13.12.1", - "resolved": "https://registry.npmjs.org/swagger-typescript-api/-/swagger-typescript-api-13.12.1.tgz", - "integrity": "sha512-Qvf1mylQPavRJIQ6D1bupW28H7IUxApCWlUIut/VbvYHlQd0IDL3ocERVpZj3yewANHBEZEJz6s84ck7G+lVAw==", + "version": "13.12.6", + "resolved": "https://registry.npmjs.org/swagger-typescript-api/-/swagger-typescript-api-13.12.6.tgz", + "integrity": "sha512-BFnSbchubRZrxxBRKy506tHBL0//durBph8ZZohZ2RQg/BKOEGm449YMAbSM5DDTwqNpTUPhgCT59S/9d84pdQ==", "license": "MIT", "dependencies": { "@apidevtools/swagger-parser": "12.1.0", - "@biomejs/js-api": "4.0.0", - "@biomejs/wasm-nodejs": "2.4.16", + "@biomejs/js-api": "6.0.0", + "@biomejs/wasm-nodejs": "2.5.2", "@types/swagger-schema-official": "^2.0.25", "c12": "^3.3.4", "citty": "^0.2.2", "consola": "^3.4.2", - "es-toolkit": "^1.47.0", + "es-toolkit": "^1.49.0", "eta": "^3.5.0", - "nanoid": "^5.1.11", + "nanoid": "^5.1.16", "openapi-types": "^12.1.3", "swagger-schema-official": "2.0.0-bab6bed", "swagger2openapi": "^7.0.8", "type-fest": "^5.7.0", "typescript": "^6.0.3", "yaml": "^2.9.0", - "yummies": "7.19.4" + "yummies": "7.20.1" }, "bin": { "swagger-typescript-api": "dist/cli.mjs" @@ -11254,19 +9463,21 @@ } }, "node_modules/swagger-typescript-api/node_modules/es-toolkit": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.47.0.tgz", - "integrity": "sha512-n1GuoD0WEQZMBk5tttoZSqwgyLx01oqa5XsBmCHwPyNe1S9jPBEmtR2pSgp2kJuWE3ciFZ6yRHmY4pM4C3OOkw==", + "version": "1.51.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.51.0.tgz", + "integrity": "sha512-zC2lQGkM7QX+Gm6iM3+WIdZJzthsEd14LvRNJneSO2hzyz/zNBENR8+YXWo1cKxgPBtV6ksPYHELbcwBRzmdCw==", "license": "MIT", "workspaces": [ "docs", - "benchmarks" + "benchmarks", + "tests/types", + "tests/browser-compat" ] }, "node_modules/swagger-typescript-api/node_modules/nanoid": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", - "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", "funding": [ { "type": "github", @@ -11350,6 +9561,24 @@ "node": ">=12" } }, + "node_modules/swagger2openapi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/swagger2openapi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/swagger2openapi/node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -11386,9 +9615,9 @@ } }, "node_modules/swagger2openapi/node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", "license": "MIT", "dependencies": { "cliui": "^8.0.1", @@ -11479,6 +9708,80 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/tailwindcss/node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/tailwindcss/node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/tailwindcss/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -11503,14 +9806,23 @@ } }, "node_modules/thread-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-3.1.0.tgz", - "integrity": "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", "license": "MIT", "dependencies": { - "real-require": "^0.2.0" + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" } }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -11518,10 +9830,20 @@ "dev": true, "license": "MIT" }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", "dependencies": { @@ -11554,9 +9876,9 @@ } }, "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", "engines": { @@ -11566,20 +9888,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", "dev": true, "license": "MIT", "engines": { @@ -11587,22 +9899,22 @@ } }, "node_modules/tldts": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.30.tgz", - "integrity": "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw==", + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.11.tgz", + "integrity": "sha512-aBiNayCfTQxuIJBm06M+xR14cYaYlDlSXZbgsnKzKNxDKUVq7KFwTjwBSsb7m9Y5xO8WfPnBc63WaYFMTGlvqw==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.0.30" + "tldts-core": "^7.4.11" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.0.30", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.30.tgz", - "integrity": "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q==", + "version": "7.4.11", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz", + "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==", "dev": true, "license": "MIT" }, @@ -11630,9 +9942,9 @@ } }, "node_modules/tough-cookie": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", - "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11693,32 +10005,16 @@ "node": ">=20" } }, - "node_modules/ts-json-schema-generator/node_modules/tslib": { + "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "license": "0BSD" - }, - "node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.7.0.tgz", - "integrity": "sha512-1URUxUqfHFM1c+zfSPsa3gnkO7Aq21qyH75SIduNYz4SzY964rn1X2vCMQaHSHhktiw+0kPa2iyb6PUpXqB6Vg==", + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", + "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", "license": "(MIT OR CC0-1.0)", "dependencies": { "tagged-tag": "^1.0.0" @@ -11857,9 +10153,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", "engines": { @@ -11867,16 +10163,121 @@ } }, "node_modules/undici-types": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.25.0.tgz", - "integrity": "sha512-AXNgS1Byr27fTI+2bsPEkV9CxkT8H6xNyRI68b3TatlZo3RkzlqQBLL+w7SmGPVpokjHbcuNVQUWE7FRTg+LRA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.29.0.tgz", + "integrity": "sha512-vamA8dGlzMwhpyYpQp9d8vka3o4D/yn5I7ez7Or+msDA4bZ8Uh+Zy91WvWf3I73gDAkFha9JcYRqm2li0Npfgg==", "license": "MIT", "optional": true }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/unstorage/node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/update-browserslist-db": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", - "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", "dev": true, "funding": [ { @@ -11921,9 +10322,9 @@ "license": "MIT" }, "node_modules/uuid": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.0.tgz", - "integrity": "sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==", + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.2.tgz", + "integrity": "sha512-xZe/16rV4aa+HGSOCiY2YeLT1OybRLrrkL/Rqaq7p7GMVXjFh+6wN4oMYgjFmnSnhY8t6Xpdl2l9qmnHYuMHwQ==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" @@ -11968,9 +10369,9 @@ } }, "node_modules/viem": { - "version": "2.49.3", - "resolved": "https://registry.npmjs.org/viem/-/viem-2.49.3.tgz", - "integrity": "sha512-FlIXd2kRygDxJtvjtPp74vjmyOKMjKlXXgTNdMxr8h3kcDrQ4bYb9q1MpSWyCVa3L2NJc9gSv+u8HcHYIZQUkw==", + "version": "2.56.0", + "resolved": "https://registry.npmjs.org/viem/-/viem-2.56.0.tgz", + "integrity": "sha512-JmkgIk4jN+im4oguLxwPv19pwSaGH9kcGSq25Ilm55106ULBBMNR3eWKKA0wZoeyLPSHIiOwZ4sGceEYEIq7LA==", "funding": [ { "type": "github", @@ -11985,8 +10386,8 @@ "@scure/bip39": "1.6.0", "abitype": "1.2.3", "isows": "1.0.7", - "ox": "0.14.20", - "ws": "8.18.3" + "ox": "0.14.34", + "ws": "8.21.0" }, "peerDependencies": { "typescript": ">=5.0.4" @@ -12012,13 +10413,28 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/viem/node_modules/@noble/hashes": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", - "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "node_modules/viem/node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", "license": "MIT", - "engines": { - "node": "^14.21.3 || >=16" + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/viem/node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" }, "funding": { "url": "https://paulmillr.com/funding/" @@ -12046,9 +10462,9 @@ } }, "node_modules/viem/node_modules/ox": { - "version": "0.14.20", - "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.20.tgz", - "integrity": "sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw==", + "version": "0.14.34", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.14.34.tgz", + "integrity": "sha512-12seOIk7dv8eAoGQhcWaeKZxNz304IVcDvb9U5Y7JZAEVe21Nm1YMxLjhWah+su5BD4Omx4Zz0z5x3ij9M4GYQ==", "funding": [ { "type": "github", @@ -12076,9 +10492,9 @@ } }, "node_modules/viem/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -12097,21 +10513,23 @@ } }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -12120,23 +10538,33 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" }, "peerDependenciesMeta": { "@types/node": { "optional": true }, - "less": { + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -12153,85 +10581,102 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite-node": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.1.tgz", - "integrity": "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA==", + "node_modules/vite/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "dev": true, "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=12" }, "funding": { - "url": "https://opencollective.com/vitest" + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/vitest": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.1.tgz", - "integrity": "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "1.6.1", - "@vitest/runner": "1.6.1", - "@vitest/snapshot": "1.6.1", - "@vitest/spy": "1.6.1", - "@vitest/utils": "1.6.1", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", - "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", - "vite": "^5.0.0", - "vite-node": "1.6.1", - "why-is-node-running": "^2.2.2" + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.1", - "@vitest/ui": "1.6.1", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -12242,9 +10687,25 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/w3c-xmlserializer": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", @@ -12293,38 +10754,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/whatwg-url/node_modules/@exodus/bytes": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", - "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" + "which": "bin/which" } }, "node_modules/which-boxed-primitive": { @@ -12398,9 +10837,9 @@ "license": "ISC" }, "node_modules/which-typed-array": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.21.tgz", - "integrity": "sha512-zbRA8cVm6io/d5W8uIe2hblzN76/Wm3v/yiythQvr+dpBWeqhPSWIDNj4zOyHi4zKbMK6DN34Xsr9jPHJERAEw==", + "version": "1.1.22", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.22.tgz", + "integrity": "sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==", "license": "MIT", "dependencies": { "available-typed-arrays": "^1.0.7", @@ -12464,10 +10903,28 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, "node_modules/ws": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz", - "integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -12508,13 +10965,6 @@ "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", "license": "ISC" }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true, - "license": "ISC" - }, "node_modules/yaml": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", @@ -12565,31 +11015,27 @@ "node": ">=6" } }, - "node_modules/yocto-queue": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", - "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", - "dev": true, + "node_modules/yargs-parser/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "license": "MIT", "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/yummies": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/yummies/-/yummies-7.19.4.tgz", - "integrity": "sha512-OO7jPtnYW+D8DX/1P1vQsWGZZjpQljuW2oEHc3RJ1a8e6SiWOtIUCL8sTwkLO6kaf7NfocRr/ld48vXZnO2hOg==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/yummies/-/yummies-7.20.1.tgz", + "integrity": "sha512-H/AxRy+SjVlfpeI0TkiVkU/M2n+XpR1XGt5yS5r7GsP9Iz3mr17dbq9DLW18PmMlyKO78+yvH/9Z9VAnVuV3og==", "license": "MIT", "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "dayjs": "^1.11.20", - "dompurify": "^3.4.1", - "nanoid": "^5.1.7", - "tailwind-merge": "^3.5.0" + "dayjs": "^1.11.21", + "dompurify": "^3.4.11", + "nanoid": "^5.1.15", + "tailwind-merge": "^3.6.0" }, "peerDependencies": { "mobx": "^6.12.4", @@ -12613,16 +11059,10 @@ "node": ">=6" } }, - "node_modules/yummies/node_modules/dayjs": { - "version": "1.11.21", - "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.21.tgz", - "integrity": "sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==", - "license": "MIT" - }, "node_modules/yummies/node_modules/nanoid": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", - "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", + "version": "5.1.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.16.tgz", + "integrity": "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==", "funding": [ { "type": "github", @@ -12638,11 +11078,10 @@ } }, "node_modules/zod": { - "version": "3.25.76", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", - "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", "license": "MIT", - "optional": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/app/web/package.json b/app/web/package.json index 733d46d0..1c26526e 100644 --- a/app/web/package.json +++ b/app/web/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "https://github.com/canton-foundation/canton-dex" + "url": "https://github.com/srikanth-bitdynamics/Canton-Dex-Reference-Implementation.git" }, "private": true, "type": "module", @@ -27,22 +27,30 @@ "@walletconnect/types": "^2.23.9", "react": "^18.3.1", "react-dom": "^18.3.1", - "react-router-dom": "^6.26.0", + "react-router-dom": "^7.18.2", "zustand": "^4.5.0" }, + "overrides": { + "@coinbase/cdp-sdk": { + "axios": "^1.20.0" + }, + "@metamask/utils": { + "uuid": "^11.1.1" + } + }, "devDependencies": { "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.1", - "@vitest/ui": "^1.6.1", + "@vitejs/plugin-react": "^6.1.0", + "@vitest/ui": "^4.1.11", "autoprefixer": "^10.4.19", "jsdom": "^29.1.1", "postcss": "^8.4.39", "tailwindcss": "^3.4.6", "typescript": "^5.5.3", - "vite": "^5.3.4", - "vitest": "^1.6.1" + "vite": "^8.2.2", + "vitest": "^4.1.11" } } diff --git a/app/web/src/__tests__/api-auth.test.ts b/app/web/src/__tests__/api-auth.test.ts new file mode 100644 index 00000000..80f36918 --- /dev/null +++ b/app/web/src/__tests__/api-auth.test.ts @@ -0,0 +1,59 @@ +import { afterEach, describe, expect, it } from 'vitest'; + +import { + apiAuthHeaders, + clearApiSessionCredentials, + getApiSessionCredentials, + setApiSessionCredentials, +} from '@/services/api-auth'; + +afterEach(() => clearApiSessionCredentials()); + +describe('operator API session credentials', () => { + it('adds only the caller token to reads', () => { + setApiSessionCredentials({ + operatorToken: 'operator-secret', + adminToken: 'admin-secret', + callerToken: 'caller.jwt', + }); + expect(apiAuthHeaders('/v1/pools', 'GET')).toEqual({ + 'X-Caller-Token': 'caller.jwt', + }); + }); + + it('uses the operator token and caller token for trader writes', () => { + setApiSessionCredentials({ + operatorToken: 'operator-secret', + adminToken: 'admin-secret', + callerToken: 'caller.jwt', + }); + expect(apiAuthHeaders('/v1/pools/swap', 'POST')).toEqual({ + Authorization: 'Bearer operator-secret', + 'X-Caller-Token': 'caller.jwt', + }); + }); + + it('uses the separate admin token for admin writes', () => { + setApiSessionCredentials({ + operatorToken: 'operator-secret', + adminToken: 'admin-secret', + callerToken: '', + }); + expect(apiAuthHeaders('/v1/admin/pools', 'POST')).toEqual({ + Authorization: 'Bearer admin-secret', + }); + }); + + it('clears whitespace-only values instead of storing them', () => { + setApiSessionCredentials({ + operatorToken: ' ', + adminToken: '', + callerToken: '', + }); + expect(getApiSessionCredentials()).toEqual({ + operatorToken: '', + adminToken: '', + callerToken: '', + }); + }); +}); diff --git a/app/web/src/__tests__/canton-direct-provider.test.ts b/app/web/src/__tests__/canton-direct-provider.test.ts new file mode 100644 index 00000000..6a99bc26 --- /dev/null +++ b/app/web/src/__tests__/canton-direct-provider.test.ts @@ -0,0 +1,29 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { + CANTON_DIRECT_DISABLED_MESSAGE, + CantonDirectProvider, +} from "@/wallet/canton-direct-provider"; + +describe("disabled Direct Canton experiment", () => { + beforeEach(() => { + vi.restoreAllMocks(); + }); + + it("fails closed without sending a bearer credential or network request", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch"); + const provider = new CantonDirectProvider( + "https://participant.example", + "must-not-be-used", + ); + + await expect(provider.connect()).rejects.toThrow( + CANTON_DIRECT_DISABLED_MESSAGE, + ); + expect(provider.getStatus()).toEqual({ + kind: "error", + message: CANTON_DIRECT_DISABLED_MESSAGE, + }); + expect(fetchSpy).not.toHaveBeenCalled(); + }); +}); diff --git a/app/web/src/__tests__/capabilities.test.ts b/app/web/src/__tests__/capabilities.test.ts index a702e7fc..822da5e8 100644 --- a/app/web/src/__tests__/capabilities.test.ts +++ b/app/web/src/__tests__/capabilities.test.ts @@ -12,7 +12,6 @@ const ALL_IDS: WalletProviderId[] = [ "partylayer", "token-standard", "walletconnect", - "canton-direct", "mock", ]; @@ -38,9 +37,13 @@ describe("wallet capabilities", () => { expect(cap.note.toLowerCase()).not.toContain("recommended"); }); - it("relay-only providers are marked no-DvP", () => { + it("WalletConnect is marked no-DvP", () => { expect(capabilityFor("walletconnect").dvp).toBe("unsupported"); - expect(capabilityFor("canton-direct").dvp).toBe("unsupported"); + }); + + it("both non-wallet development adapters are marked dev-only", () => { + expect(capabilityFor("token-standard").dvp).toBe("dev-only"); + expect(capabilityFor("mock").dvp).toBe("dev-only"); }); it("dvpBadge maps readiness → tone", () => { diff --git a/app/web/src/__tests__/commands.test.ts b/app/web/src/__tests__/commands.test.ts index dadeb13b..7af13af4 100644 --- a/app/web/src/__tests__/commands.test.ts +++ b/app/web/src/__tests__/commands.test.ts @@ -11,6 +11,7 @@ import { import type { WalletIntent, RequestSwapIntent } from '@/wallet/types'; const FIXED_NOW = new Date('2026-05-19T12:00:00.000Z'); +const REQUESTED_AT = FIXED_NOW.toISOString(); const ctx: ComposeContext = { party: 'alice::1220a', @@ -60,6 +61,7 @@ describe('composeCommands', () => { committed: true, meta: { values: {} }, }, + requestedAt: REQUESTED_AT, inputHoldingCids: ['holding1', 'holding2'], hint: { instrumentId: 'USDC', amount: '100.0' }, }; @@ -205,6 +207,7 @@ describe('composeCommands', () => { poolId: 'pool1234567890', allocationSpec: swapAllocationSpec, settlement: swapSettlement, + requestedAt: REQUESTED_AT, factoryCid: 'factory1', allocationFactoryExtraArgs, disclosure, @@ -220,6 +223,10 @@ describe('composeCommands', () => { 'ExerciseCommand.templateId', '#splice-api-token-allocation-instruction-v2:Splice.Api.Token.AllocationInstructionV2:AllocationFactory', ); + expect(composed.commands[0]).toHaveProperty( + 'ExerciseCommand.choiceArgument.requestedAt', + REQUESTED_AT, + ); }); it('request-swap refuses unconfigured factory', () => { @@ -228,6 +235,7 @@ describe('composeCommands', () => { poolId: 'pool1', allocationSpec: swapAllocationSpec, settlement: swapSettlement, + requestedAt: REQUESTED_AT, factoryCid: 'PENDING_FACTORY', allocationFactoryExtraArgs, disclosure, @@ -267,10 +275,13 @@ describe('composeCommands', () => { requestCid: 'reqABCDEFGH12', settlement, allocations: [baseSpec, quoteSpec, receiptSpec], - depositFactoryCid: 'depF', - lpFactoryCid: 'lpF', - depositFactoryExtraArgs: allocationFactoryExtraArgs, - lpFactoryExtraArgs, + requestedAt: REQUESTED_AT, + factoryCids: ['depF', 'depF', 'lpF'], + allocationFactoryExtraArgs: [ + allocationFactoryExtraArgs, + allocationFactoryExtraArgs, + lpFactoryExtraArgs, + ], allocationRequestExtraArgs, disclosure, baseHoldingCids: ['b1'], @@ -304,6 +315,7 @@ describe('composeCommands', () => { expect(arg.actions.slice(1).map((a) => a.value.cid)).toEqual(['depF', 'depF', 'lpF']); for (const a of arg.actions.slice(1)) { expect(a.value.arg.inputHoldingCids).toEqual([]); + expect(a.value.arg.requestedAt).toBe(REQUESTED_AT); } expect(arg.actions.slice(1).map((a) => a.value.arg.extraArgs)).toEqual([ allocationFactoryExtraArgs, @@ -329,10 +341,13 @@ describe('composeCommands', () => { requestCid: 'reqREMOVE1234', settlement, allocations: [baseRcpt, quoteRcpt, burnSpec], - depositFactoryCid: 'depF', - lpFactoryCid: 'lpF', - depositFactoryExtraArgs: allocationFactoryExtraArgs, - lpFactoryExtraArgs, + requestedAt: REQUESTED_AT, + factoryCids: ['depF', 'depF', 'lpF'], + allocationFactoryExtraArgs: [ + allocationFactoryExtraArgs, + allocationFactoryExtraArgs, + lpFactoryExtraArgs, + ], allocationRequestExtraArgs, disclosure, lpHoldingCids: ['lp1', 'lp2'], @@ -344,11 +359,14 @@ describe('composeCommands', () => { expect(cmd.choice).toBe('BatchingUtility_ExecuteBatch'); const arg = cmd.choiceArgument as { inputHoldingMap: { byAdminAndAccount: [Record, Record][] }; - actions: { tag: string; value: { cid: string } }[]; + actions: { tag: string; value: { cid: string; arg: { requestedAt: string } } }[]; }; expect(arg.actions[0].tag).toBe('TSA_AllocationRequest_AcceptV2'); expect(arg.actions[0].value.cid).toBe('reqREMOVE1234'); expect(arg.actions.slice(1).map((a) => a.value.cid)).toEqual(['depF', 'depF', 'lpF']); + expect( + arg.actions.slice(1).map((a) => a.value.arg.requestedAt), + ).toEqual([REQUESTED_AT, REQUESTED_AT, REQUESTED_AT]); // Only the burn-sender (LP) funds from holdings; the two receipts lock // nothing. ALL fragmented LP holdings are threaded so any position redeems. expect(arg.inputHoldingMap.byAdminAndAccount).toHaveLength(1); @@ -367,10 +385,13 @@ describe('composeCommands', () => { mkSpec('lp-quote-deposit', 'USDC', 'SenderSide', true), mkSpec('lp-mint', 'BTC-USDC-LP', 'ReceiverSide', false), ], - depositFactoryCid: 'depF', - lpFactoryCid: 'lpF', - depositFactoryExtraArgs: allocationFactoryExtraArgs, - lpFactoryExtraArgs, + requestedAt: REQUESTED_AT, + factoryCids: ['depF', 'depF', 'lpF'], + allocationFactoryExtraArgs: [ + allocationFactoryExtraArgs, + allocationFactoryExtraArgs, + lpFactoryExtraArgs, + ], allocationRequestExtraArgs, disclosure, baseHoldingCids: ['b1'], diff --git a/app/web/src/__tests__/detection.test.ts b/app/web/src/__tests__/detection.test.ts index 5604c316..43d998c0 100644 --- a/app/web/src/__tests__/detection.test.ts +++ b/app/web/src/__tests__/detection.test.ts @@ -4,8 +4,8 @@ import type { DetectedWallet, WalletProvider } from "@/wallet/types"; // discoverWallets() reads the provider registry + the default-provider id, so we // mock the registry module and drive it with fake providers. capabilities.ts is -// left real so the "other providers become one row" mapping is exercised end to -// end. +// left real so the complete "other providers become one row" mapping is +// exercised. const reg = vi.hoisted(() => ({ map: new Map(), defaultId: null as string | null, diff --git a/app/web/src/__tests__/layout-preview-boundary.test.tsx b/app/web/src/__tests__/layout-preview-boundary.test.tsx new file mode 100644 index 00000000..184dd87a --- /dev/null +++ b/app/web/src/__tests__/layout-preview-boundary.test.tsx @@ -0,0 +1,56 @@ +// The seeded backend must be visually impossible to mistake for a synchronized +// Canton environment. This test pins both sides of that UI boundary. + +import { render, screen } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { MemoryRouter, Route, Routes } from 'react-router-dom'; +import { describe, expect, it, vi } from 'vitest'; + +import { Layout } from '@/components/Layout'; + +function renderLayout(network: string) { + globalThis.fetch = vi.fn(async () => + new Response(JSON.stringify({ + network, + slot: 42, + synced: true, + serverTime: '2026-08-27T00:00:00Z', + }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }), + ); + + const client = new QueryClient({ + defaultOptions: { queries: { retry: false } }, + }); + render( + + + + }> + route content} /> + + + + , + ); +} + +describe('preview boundary', () => { + it('labels the seeded backend as non-Canton and non-settling', async () => { + renderLayout('preview:in-memory'); + + const notice = await screen.findByRole('status'); + expect(notice).toHaveTextContent('In-memory preview — no Canton participant.'); + expect(notice).toHaveTextContent('they do not settle token value'); + expect(screen.getByText('Preview · no Canton')).toBeInTheDocument(); + }); + + it('does not show the preview warning for a live network status', async () => { + renderLayout('canton:testnet'); + + expect(await screen.findByText('Synced · slot 42')).toBeInTheDocument(); + expect(screen.queryByText(/In-memory preview/)).not.toBeInTheDocument(); + }); +}); diff --git a/app/web/src/__tests__/ledger.test.ts b/app/web/src/__tests__/ledger.test.ts index 790c2270..3c334d3c 100644 --- a/app/web/src/__tests__/ledger.test.ts +++ b/app/web/src/__tests__/ledger.test.ts @@ -30,10 +30,6 @@ const swapAuthorityFixture = (): Parameters[0] => ({ operator: 'op::1', lpRegistrar: 'lp::1', admin: 'default-ad::1', - allocationFactoryCid: 'factory', - settlementFactoryCid: 'settlement-factory', - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network: 'local', }, pool: { diff --git a/app/web/src/__tests__/normalize-funding.test.ts b/app/web/src/__tests__/normalize-funding.test.ts index 3393f51c..93379935 100644 --- a/app/web/src/__tests__/normalize-funding.test.ts +++ b/app/web/src/__tests__/normalize-funding.test.ts @@ -12,7 +12,7 @@ import type { WalletIntent, WalletResult } from '@/wallet/types'; // holdings ACS through a controllable queue. const handToWalletMock = - vi.fn<[WalletIntent], Promise>(); + vi.fn<(intent: WalletIntent) => Promise>(); let activeProviderId: string | null = 'token-standard'; diff --git a/app/web/src/__tests__/operator-relay-provider.test.ts b/app/web/src/__tests__/operator-relay-provider.test.ts new file mode 100644 index 00000000..d446a371 --- /dev/null +++ b/app/web/src/__tests__/operator-relay-provider.test.ts @@ -0,0 +1,40 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +import { TokenStandardProvider } from "@/wallet/token-standard-provider"; + +const SESSION_KEY = "canton-dex:token-standard:session"; + +describe("development operator relay identity", () => { + beforeEach(() => { + window.localStorage.clear(); + vi.restoreAllMocks(); + }); + + it("is labelled as a relay rather than an external Token Standard wallet", () => { + const provider = new TokenStandardProvider("http://localhost:8080"); + expect(provider.label).toBe("Operator Relay (dev only)"); + }); + + it("sanitizes a restored legacy session down to party and user id", () => { + window.localStorage.setItem( + SESSION_KEY, + JSON.stringify({ + party: "demo::1220", + userId: "ledger-api-user", + token: "legacy-secret", + ledgerUrl: "https://participant.example", + }), + ); + + const provider = new TokenStandardProvider("http://localhost:8080"); + + expect(provider.getStatus()).toMatchObject({ + kind: "connected", + account: { party: "demo::1220", label: "Operator Relay (dev only)" }, + }); + expect(JSON.parse(window.localStorage.getItem(SESSION_KEY) ?? "null")).toEqual({ + party: "demo::1220", + userId: "ledger-api-user", + }); + }); +}); diff --git a/app/web/src/__tests__/pages.test.tsx b/app/web/src/__tests__/pages.test.tsx index f18867c3..7257770d 100644 --- a/app/web/src/__tests__/pages.test.tsx +++ b/app/web/src/__tests__/pages.test.tsx @@ -1,7 +1,7 @@ // Page-render smoke tests. Each top-level page must render without // crashing when wired with mocked operator-backend responses. -// We don't assert specific UI semantics — that's what e2e is for. We -// only catch "throws on mount" regressions. +// We don't assert specific interaction semantics or browser-to-ledger flows. +// These tests only catch "throws on mount" regressions. import { render } from '@testing-library/react'; import { MemoryRouter, Routes, Route } from 'react-router-dom'; diff --git a/app/web/src/__tests__/partylayer-provider.test.ts b/app/web/src/__tests__/partylayer-provider.test.ts index 7c5c35f4..c86874cd 100644 --- a/app/web/src/__tests__/partylayer-provider.test.ts +++ b/app/web/src/__tests__/partylayer-provider.test.ts @@ -6,7 +6,7 @@ import { parsePartyLayerHoldings, type PartyLayerClient, } from "@/wallet/partylayer-provider"; -import type { WalletIntent } from "@/wallet/types"; +import type { RequestSwapIntent } from "@/wallet/types"; // A fake @partylayer/sdk client: records the submitted command tree and returns // an updateId-only receipt, matching the provider contract. @@ -101,7 +101,7 @@ function failingClient(error: Error) { return { client, disconnectCalls }; } -const swapIntent: WalletIntent = { +const swapIntent: RequestSwapIntent = { kind: "request-swap", poolId: "pool-abc", settlement: { executors: ["op"], id: "s", cid: null, meta: { values: {} } }, @@ -112,13 +112,14 @@ const swapIntent: WalletIntent = { settlementDeadline: null, nextIterationFunding: null, committed: true, - meta: {}, + meta: { values: {} }, }, + requestedAt: "2026-05-19T12:00:00.000Z", factoryCid: "fac", allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, inputHoldingCids: ["h1"], disclosure: [], -} as unknown as WalletIntent; +}; describe("PartyLayerProvider", () => { const ctx = () => new PartyLayerProvider("#canton-dex-trading", async () => fake.client); @@ -170,6 +171,10 @@ describe("PartyLayerProvider", () => { expect(fake.calls[0].signedTx.actAs).toEqual(["alice::1220a"]); expect(fake.calls[0].signedTx.commandId).toMatch(/^swap-pool-abc-/); expect(fake.calls[0].signedTx.commands).toHaveLength(1); + expect(fake.calls[0].signedTx.commands[0]).toHaveProperty( + "ExerciseCommand.choiceArgument.requestedAt", + swapIntent.requestedAt, + ); }); it("rejects submit when the wallet receipt has no updateId", async () => { diff --git a/app/web/src/__tests__/registry-boundaries.test.ts b/app/web/src/__tests__/registry-boundaries.test.ts new file mode 100644 index 00000000..301ed47e --- /dev/null +++ b/app/web/src/__tests__/registry-boundaries.test.ts @@ -0,0 +1,17 @@ +import { beforeEach, describe, expect, it } from "vitest"; + +import { getProviders } from "@/wallet/registry"; + +describe("wallet registry authority boundaries", () => { + beforeEach(() => { + window.localStorage.setItem( + "canton-dex:direct:session", + JSON.stringify({ token: "legacy-participant-secret" }), + ); + }); + + it("never registers Direct Canton and removes its legacy bearer session", () => { + expect([...getProviders().keys()]).not.toContain("canton-direct"); + expect(window.localStorage.getItem("canton-dex:direct:session")).toBeNull(); + }); +}); diff --git a/app/web/src/__tests__/registry-recommendation.test.ts b/app/web/src/__tests__/registry-recommendation.test.ts new file mode 100644 index 00000000..93f3d59c --- /dev/null +++ b/app/web/src/__tests__/registry-recommendation.test.ts @@ -0,0 +1,34 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +async function recommendation(env: { + sdk?: string; + partyLayer?: string; + walletConnect?: string; +}) { + vi.resetModules(); + vi.stubEnv("VITE_ENABLE_SDK", env.sdk ?? "0"); + vi.stubEnv("VITE_ENABLE_PARTYLAYER", env.partyLayer ?? "0"); + vi.stubEnv("VITE_WC_PROJECT_ID", env.walletConnect ?? ""); + return (await import("@/wallet/registry")).DEFAULT_PROVIDER_ID; +} + +describe("production-facing wallet recommendation order", () => { + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it("prefers the DvP-ready dapp SDK when every adapter is enabled", async () => { + await expect( + recommendation({ sdk: "1", partyLayer: "1", walletConnect: "project" }), + ).resolves.toBe("sdk"); + }); + + it("falls through to PartyLayer, then WalletConnect", async () => { + await expect( + recommendation({ partyLayer: "1", walletConnect: "project" }), + ).resolves.toBe("partylayer"); + await expect(recommendation({ walletConnect: "project" })).resolves.toBe( + "walletconnect", + ); + }); +}); diff --git a/app/web/src/__tests__/sdk-provider.test.ts b/app/web/src/__tests__/sdk-provider.test.ts index 9c0800ce..1cef119a 100644 --- a/app/web/src/__tests__/sdk-provider.test.ts +++ b/app/web/src/__tests__/sdk-provider.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest"; -import type { RequestSwapIntent, WalletIntent } from "@/wallet/types"; +import type { RequestSwapIntent } from "@/wallet/types"; // SdkProvider owns a private DappSDK instance (with a custom walletPicker) and // a RemoteAdapter for the configured gateway. We mock those two classes so every @@ -59,7 +59,7 @@ vi.mock("@canton-network/dapp-sdk", () => ({ import { SdkProvider } from "@/wallet/sdk-provider"; -const swapIntent: WalletIntent = { +const swapIntent: RequestSwapIntent = { kind: "request-swap", poolId: "pool1234567890", allocationSpec: { @@ -75,6 +75,7 @@ const swapIntent: WalletIntent = { executor: "op::1", settlementRef: { id: "DexPool", cid: "pool1234567890" }, } as unknown as RequestSwapIntent["settlement"], + requestedAt: "2026-05-19T12:00:00.000Z", factoryCid: "factory1", allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, disclosure: [ diff --git a/app/web/src/__tests__/setup.ts b/app/web/src/__tests__/setup.ts index e4f1ad4d..30f81f99 100644 --- a/app/web/src/__tests__/setup.ts +++ b/app/web/src/__tests__/setup.ts @@ -12,10 +12,6 @@ const DEFAULT_RESPONSES: Record = { operator: 'op::1', lpRegistrar: 'lp::1', admin: 'ad::1', - allocationFactoryCid: 'fac:1', - settlementFactoryCid: 'set:1', - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network: 'canton:test', }, '/v1/pools': [], diff --git a/app/web/src/__tests__/walletconnect-provider.test.ts b/app/web/src/__tests__/walletconnect-provider.test.ts index c45bccb7..f64c9d4e 100644 --- a/app/web/src/__tests__/walletconnect-provider.test.ts +++ b/app/web/src/__tests__/walletconnect-provider.test.ts @@ -4,12 +4,18 @@ import { WalletConnectProvider, WalletStatusUnknownError, } from '@/wallet/walletconnect-provider'; -import type { WalletIntent } from '@/wallet/types'; +import type { PlaceOrderIntent } from '@/wallet/types'; -const swapIntent = { - kind: 'request-swap', - poolId: 'pool-1', -} as unknown as WalletIntent; +const placeOrderIntent: PlaceOrderIntent = { + kind: 'place-order', + pair: { base: 'BTC', quote: 'USDC' }, + side: 'Bid', + limitPrice: '20000.0000000000', + quantity: '0.1000000000', + expiry: null, + operator: 'operator::1220a', + admin: 'admin::1220a', +}; // Inject a fake connector + connected status without driving the AppKit import. // `request` is a vitest mock; typed `any` so each test can return whatever @@ -44,7 +50,7 @@ describe('WalletConnectProvider submit retry safety', () => { // Attach the rejection handler up front so the rejection is never orphaned // while the fake timer advances. let captured: unknown; - const submit = p.submit(swapIntent).catch((e) => { + const submit = p.submit(placeOrderIntent).catch((e) => { captured = e; }); // Drive the 30s submit timeout. @@ -65,7 +71,7 @@ describe('WalletConnectProvider submit retry safety', () => { })); const { p } = connectedProvider(request); - await p.submit(swapIntent); + await p.submit(placeOrderIntent); expect(request).toHaveBeenCalledTimes(1); const arg = (request.mock.calls as unknown[][])[0]![0] as { @@ -73,7 +79,7 @@ describe('WalletConnectProvider submit retry safety', () => { params: Array<{ commandId?: string }>; }; expect(arg.method).toBe('canton_prepareExecute'); - expect(arg.params[0]!.commandId).toMatch(/^wc-request-swap-/); + expect(arg.params[0]!.commandId).toMatch(/^wc-place-order-/); }); it('propagates non-timeout errors unchanged (e.g. user reject)', async () => { @@ -82,7 +88,7 @@ describe('WalletConnectProvider submit retry safety', () => { }); const { p } = connectedProvider(request); - await expect(p.submit(swapIntent)).rejects.toThrow('user rejected'); + await expect(p.submit(placeOrderIntent)).rejects.toThrow('user rejected'); expect(request).toHaveBeenCalledTimes(1); }); }); diff --git a/app/web/src/components/Layout.tsx b/app/web/src/components/Layout.tsx index 6e22d7de..4df47af6 100644 --- a/app/web/src/components/Layout.tsx +++ b/app/web/src/components/Layout.tsx @@ -14,6 +14,9 @@ import { ConnectWalletButton } from '@/components/ConnectWalletButton'; import { ledger } from '@/services/ledger'; const APP_VERSION = (import.meta.env.VITE_APP_VERSION as string | undefined) ?? 'v0.6.0'; +const DOCS_URL = + (import.meta.env.VITE_DOCS_URL as string | undefined) || + 'https://srikanth-bitdynamics.github.io/Canton-Dex-Reference-Implementation/'; const NAV_ITEMS = [ { to: '/', label: 'Trade' }, @@ -30,9 +33,14 @@ export function Layout() { queryFn: ledger.getStatus, refetchInterval: 5000, }); - const networkLabel = status?.network ?? 'connecting…'; + const isInMemoryPreview = status?.network === 'preview:in-memory'; + const networkLabel = isInMemoryPreview + ? 'in-memory preview' + : (status?.network ?? 'connecting…'); const slotLabel = status - ? status.synced + ? isInMemoryPreview + ? 'Preview · no Canton' + : status.synced ? `Synced · slot ${status.slot.toLocaleString()}` : `Catching up · slot ${status.slot.toLocaleString()}` : 'Connecting…'; @@ -87,7 +95,7 @@ export function Layout() { ))}

+ {isInMemoryPreview && ( +
+ + In-memory preview — no Canton participant. + {' '} + Data is seeded and wallet actions demonstrate intent composition; + they do not settle token value. +
+ )}
diff --git a/app/web/src/components/PoolDetail.tsx b/app/web/src/components/PoolDetail.tsx index 4f27e8a7..f28d09b7 100644 --- a/app/web/src/components/PoolDetail.tsx +++ b/app/web/src/components/PoolDetail.tsx @@ -577,7 +577,7 @@ export function PoolDetail({ pool, holdings, lpHeld, onBack }: Props) {
- Delivery-versus-payment in three steps: the operator creates a{' '} + Live-ledger DvP design in three steps: the operator creates a{' '} LiquidityAllocationRequest → your wallet authors the base/quote receipt + LP burn-sender allocations → the operator and lpRegistrar settle, delivering diff --git a/app/web/src/components/Portfolio.tsx b/app/web/src/components/Portfolio.tsx index 2f93e355..7b3954a7 100644 --- a/app/web/src/components/Portfolio.tsx +++ b/app/web/src/components/Portfolio.tsx @@ -165,7 +165,7 @@ export function Portfolio({

Portfolio

- All holdings, LP positions, and on-ledger activity for your party. + Holdings, LP positions, and activity reported by the configured backend.

diff --git a/app/web/src/components/SwapCard.tsx b/app/web/src/components/SwapCard.tsx index bbbffa02..7349f2aa 100644 --- a/app/web/src/components/SwapCard.tsx +++ b/app/web/src/components/SwapCard.tsx @@ -334,7 +334,7 @@ export function SwapCard({ pool, userBalances, onSwapComplete }: SwapCardProps)
-
On-ledger sequence
+
Intended live-ledger sequence
- By approving, your wallet locks the input in a single - allocation; the operator then settles the swap on-ledger. + With a live participant and supported wallet configured, your + wallet locks the input in one allocation and the operator + settles the swap on-ledger. Preview mode only simulates this handoff.
diff --git a/app/web/src/pages/AdminPage.tsx b/app/web/src/pages/AdminPage.tsx index 19fbfa9d..57ebcdcd 100644 --- a/app/web/src/pages/AdminPage.tsx +++ b/app/web/src/pages/AdminPage.tsx @@ -2,6 +2,12 @@ import { useState } from 'react'; import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; import { ledger } from '@/services/ledger'; import { OperatorApi } from '@/services/operator-api'; +import { + clearApiSessionCredentials, + getApiSessionCredentials, + setApiSessionCredentials, + type ApiSessionCredentials, +} from '@/services/api-auth'; const operatorApi = new OperatorApi( (import.meta.env.VITE_API_BASE as string | undefined) ?? 'http://localhost:8080', @@ -11,6 +17,10 @@ type TradingMode = 'TM_OrderBook' | 'TM_Pool' | 'TM_Both'; export function AdminPage() { const qc = useQueryClient(); + const [apiCredentials, setApiCredentials] = useState( + () => getApiSessionCredentials(), + ); + const [credentialNotice, setCredentialNotice] = useState(''); const { data: pairs } = useQuery({ queryKey: ['pairs'], queryFn: ledger.getPairs, @@ -71,6 +81,27 @@ export function AdminPage() { return (
+ { + setApiSessionCredentials(apiCredentials); + setCredentialNotice( + 'Saved for this browser tab. Protected API writes will now attach the matching credential.', + ); + }} + onClear={() => { + clearApiSessionCredentials(); + setApiCredentials({ + operatorToken: '', + adminToken: '', + callerToken: '', + }); + setCredentialNotice('Cleared this tab’s API credentials.'); + }} + /> +

@@ -338,6 +369,89 @@ export function AdminPage() { ); } +interface ApiCredentialsPanelProps { + credentials: ApiSessionCredentials; + notice: string; + onChange: (credentials: ApiSessionCredentials) => void; + onSave: () => void; + onClear: () => void; +} + +function ApiCredentialsPanel({ + credentials, + notice, + onChange, + onSave, + onClear, +}: ApiCredentialsPanelProps) { + const update = (field: keyof ApiSessionCredentials, value: string) => + onChange({ ...credentials, [field]: value }); + + return ( +
+

+ API session credentials +

+

+ Real-Canton backends fail closed on writes. Enter short-lived tokens + issued by your deployment before using the Admin screen or a + trader-flow settle action. Values stay in this tab’s session storage; + they are never compiled into the dApp bundle. +

+
+ + update('operatorToken', e.target.value)} + placeholder="DEX_OPERATOR_API_TOKEN" + /> + + + update('adminToken', e.target.value)} + placeholder="OPERATOR_ADMIN_TOKEN" + /> + + + update('callerToken', e.target.value)} + placeholder="X-Caller-Token" + /> + +
+

+ A public multi-user deployment should obtain scoped, short-lived + credentials from an authenticated BFF or session service. Do not share + the venue’s long-lived operator/admin tokens with ordinary traders. +

+
+ + + {notice && ( + + {notice} + + )} +
+
+ ); +} + interface CreatePairProps { onClose: () => void; onSubmit: (input: { diff --git a/app/web/src/pages/PoolsPage.tsx b/app/web/src/pages/PoolsPage.tsx index 93cc729c..c8f85e73 100644 --- a/app/web/src/pages/PoolsPage.tsx +++ b/app/web/src/pages/PoolsPage.tsx @@ -89,8 +89,8 @@ export function PoolsPage() {

Liquidity pools

- Provide liquidity to earn a share of swap fees. LP positions are - minted as on-ledger LP tokens. + Explore liquidity and swap-fee shares. In a configured live + deployment, positions are minted as on-ledger LP tokens.

diff --git a/app/web/src/pages/PortfolioPage.tsx b/app/web/src/pages/PortfolioPage.tsx index f88fd369..a17aacba 100644 --- a/app/web/src/pages/PortfolioPage.tsx +++ b/app/web/src/pages/PortfolioPage.tsx @@ -65,7 +65,7 @@ export function PortfolioPage() { if (!party) { return ( - Connect a wallet to view holdings, LP positions, and on-ledger activity. + Connect a wallet to view backend-reported holdings, LP positions, and activity. ); } diff --git a/app/web/src/pages/RfqPage.tsx b/app/web/src/pages/RfqPage.tsx index d228a687..970b0ea3 100644 --- a/app/web/src/pages/RfqPage.tsx +++ b/app/web/src/pages/RfqPage.tsx @@ -40,6 +40,8 @@ const operatorApi = new OperatorApi( (import.meta.env.VITE_API_BASE as string | undefined) ?? 'http://localhost:8080', ); +const HOSTED_RFQ_WRITES_ENABLED = + import.meta.env.DEV || import.meta.env.VITE_ENABLE_HOSTED_RFQ === '1'; type Tab = 'active' | 'accepted' | 'expired'; type SortMode = 'policy' | 'price' | 'earliest' | 'trusted'; @@ -313,6 +315,17 @@ export function RfqPage() {
)} + {!HOSTED_RFQ_WRITES_ENABLED && ( +
+ RFQ reads are available, but this production build has the custodial + RFQ write surface disabled. Use wallet-authored RFQ commands, or have + the operator deliberately enable both the frontend and the + caller-bound backend relay. +
+ )}

RFQ

@@ -323,8 +336,14 @@ export function RfqPage() { @@ -428,6 +447,7 @@ export function RfqPage() { onCancelRfq={cancelRfq} onPolicyOpen={(id) => setPolicyOpenFor(id)} onComposeFirst={() => setComposing(true)} + writesEnabled={HOSTED_RFQ_WRITES_ENABLED} /> )} {tab === 'accepted' && ( @@ -476,6 +496,7 @@ interface ActiveTabProps { onCancelRfq: (id: string) => void; onPolicyOpen: (id: string) => void; onComposeFirst: () => void; + writesEnabled: boolean; } function ActiveTab({ @@ -487,12 +508,18 @@ function ActiveTab({ onCancelRfq, onPolicyOpen, onComposeFirst, + writesEnabled, }: ActiveTabProps) { if (rfqs.length === 0) { return (
No active RFQs
-
@@ -512,6 +539,7 @@ function ActiveTab({ onAccept={(q) => onAccept(r, q)} onCancelRfq={() => onCancelRfq(r.contractId)} onPolicyOpen={() => onPolicyOpen(r.contractId)} + writesEnabled={writesEnabled} /> ))}
@@ -526,6 +554,7 @@ interface RfqRowProps { onAccept: (q: RfqQuote) => void; onCancelRfq: () => void; onPolicyOpen: () => void; + writesEnabled: boolean; } function RfqRow({ @@ -536,6 +565,7 @@ function RfqRow({ onAccept, onCancelRfq, onPolicyOpen, + writesEnabled, }: RfqRowProps) { const { data: dealers } = useDealers(); const lifecycle = @@ -860,6 +890,8 @@ function RfqRow({ @@ -924,7 +956,12 @@ function RfqRow({ className="row" style={{ justifyContent: 'flex-end', marginTop: 12, gap: 8 }} > -
@@ -1437,10 +1474,10 @@ function ComposeRfqSheet({ trader, operator, onClose, onSubmit }: ComposeProps) lineHeight: 1.55, }} > - Off-ledger: the request reaches the selected dealers through a - private channel. On-ledger: accepting a quote creates a - MatchedTrade visible to the trader, dealer, and operator. Token - settlement is a later allocation-backed step. + Workflow design: the request reaches selected dealers through a + private off-ledger channel. In live-ledger mode, accepting a quote + creates a MatchedTrade visible to trader, dealer, and operator; + token settlement is a later allocation-backed step.
@@ -1513,8 +1550,9 @@ function PolicyModal({ rfq, onClose }: { rfq: Rfq; onClose: () => void }) { {POLICY_VERSION} {' '} - · published by operator. Both trader and dealer can audit this against - the on-chain config. + · published by operator. Trader and dealer can audit the version and + receipt inputs returned by the operator; this reference has no on-ledger + DexRules or policy-configuration contract.
void }) { 2. Sort by tier (trusted before whitelist)
- 3. Then by price ( - {rfq.side === 'RFQ_Buy' ? 'lowest' : 'highest'}) + 3. Then by expiry (later + first)
4. Then by postedAt{' '} (earliest)
5. Tiebreaker:{' '} - venue ID hash + dealer party ID +
+
+ Price is intentionally not part of policy v2.0. A dealer declares its + quote tier; the operator endorses the considered set when it + co-authorizes acceptance.
Ranking applied right now
diff --git a/app/web/src/pages/TradePage.tsx b/app/web/src/pages/TradePage.tsx index 85420484..ad413e76 100644 --- a/app/web/src/pages/TradePage.tsx +++ b/app/web/src/pages/TradePage.tsx @@ -129,8 +129,9 @@ export function TradePage() {

Trade

- Swap directly against a Canton DEX liquidity pool. All amounts - settle on-ledger via Token Standard V2 allocations. + Explore swaps against a Canton DEX liquidity pool. In a configured + live deployment, Token Standard V2 allocations settle the amounts + on-ledger; the local preview uses seeded in-memory state.

diff --git a/app/web/src/primitives/PolicyReceiptModal.tsx b/app/web/src/primitives/PolicyReceiptModal.tsx index afb1b1f6..a0bf56b9 100644 --- a/app/web/src/primitives/PolicyReceiptModal.tsx +++ b/app/web/src/primitives/PolicyReceiptModal.tsx @@ -143,9 +143,8 @@ export function PolicyReceiptModal({ trade, onClose }: Props) { at the time of accept. Inputs and ranking output are folded into SettlementInfo.meta via the{' '} dex.policy.* key prefix, so the - receipt rides on-ledger atomically with the trade. Disclosable - to regulators or counterparties without revealing the trade - itself. + receipt rides atomically with the trade in live-ledger mode. It can be + disclosed to regulators or counterparties without revealing the trade itself.
); diff --git a/app/web/src/services/api-auth.ts b/app/web/src/services/api-auth.ts new file mode 100644 index 00000000..2198e325 --- /dev/null +++ b/app/web/src/services/api-auth.ts @@ -0,0 +1,103 @@ +// Runtime credentials for the operator HTTP API. +// +// These tokens are deliberately NOT Vite environment variables: VITE_* values +// are compiled into the public JavaScript bundle. An operator or validator may +// enter short-lived credentials in the Admin screen; they live only in this +// browser tab's sessionStorage and are attached to write requests. +// +// A public/multi-user deployment should replace this manual handoff with its +// own authenticated BFF/session issuer. Never distribute a shared long-lived +// operator or admin token to ordinary traders. + +const OPERATOR_TOKEN_KEY = "canton-dex.operator-api-token"; +const ADMIN_TOKEN_KEY = "canton-dex.admin-api-token"; +const CALLER_TOKEN_KEY = "canton-dex.caller-token"; + +export interface ApiSessionCredentials { + operatorToken: string; + adminToken: string; + callerToken: string; +} + +function session(): Storage | null { + if (typeof window === "undefined") return null; + try { + return window.sessionStorage; + } catch { + // Storage can be disabled by browser policy. Reads remain available and + // the backend still fails closed for protected writes. + return null; + } +} + +function read(key: string): string { + try { + return session()?.getItem(key)?.trim() ?? ""; + } catch { + return ""; + } +} + +function write(key: string, value: string): void { + const storage = session(); + if (!storage) return; + try { + const normalized = value.trim(); + if (normalized) storage.setItem(key, normalized); + else storage.removeItem(key); + } catch { + // A privacy policy or exhausted quota can reject the write. The backend + // remains fail-closed; no credential is moved to a less-safe fallback. + } +} + +export function getApiSessionCredentials(): ApiSessionCredentials { + return { + operatorToken: read(OPERATOR_TOKEN_KEY), + adminToken: read(ADMIN_TOKEN_KEY), + callerToken: read(CALLER_TOKEN_KEY), + }; +} + +export function setApiSessionCredentials( + credentials: ApiSessionCredentials, +): void { + write(OPERATOR_TOKEN_KEY, credentials.operatorToken); + write(ADMIN_TOKEN_KEY, credentials.adminToken); + write(CALLER_TOKEN_KEY, credentials.callerToken); +} + +export function clearApiSessionCredentials(): void { + const storage = session(); + try { + storage?.removeItem(OPERATOR_TOKEN_KEY); + storage?.removeItem(ADMIN_TOKEN_KEY); + storage?.removeItem(CALLER_TOKEN_KEY); + } catch { + // Treat unavailable storage as already cleared from the app's point of + // view. Reads return empty strings and protected requests carry no token. + } +} + +/** Headers required by the backend's fail-closed write and private-read gates. */ +export function apiAuthHeaders( + path: string, + method = "GET", +): Record { + const normalizedMethod = method.toUpperCase(); + const credentials = getApiSessionCredentials(); + if (["GET", "HEAD", "OPTIONS"].includes(normalizedMethod)) { + return credentials.callerToken + ? { "X-Caller-Token": credentials.callerToken } + : {}; + } + const bearer = path.startsWith("/v1/admin/") + ? credentials.adminToken + : credentials.operatorToken; + return { + ...(bearer ? { Authorization: `Bearer ${bearer}` } : {}), + ...(credentials.callerToken + ? { "X-Caller-Token": credentials.callerToken } + : {}), + }; +} diff --git a/app/web/src/services/ledger.ts b/app/web/src/services/ledger.ts index 7035fe62..758e98b7 100644 --- a/app/web/src/services/ledger.ts +++ b/app/web/src/services/ledger.ts @@ -9,13 +9,13 @@ // components below this layer should never reach past it. import { OperatorApi, type SwapQuoteBinding } from './operator-api'; +import { apiAuthHeaders } from './api-auth'; import { handToWallet } from '@/wallet/handoff'; import { getProvider } from '@/wallet/registry'; import { coSignsAdmin } from '@/wallet/capabilities'; import { useWalletStore } from '@/wallet/store'; import type { ContractId, - DisclosedContract, V2AllocationSpecification, V2ExtraArgs, V2SettlementInfo, @@ -40,12 +40,6 @@ interface RequestAddResult { quoteAmount: string; allocations: V2AllocationSpecification[]; settlement: V2SettlementInfo; - depositFactoryCid: string; - lpFactoryCid: string; - depositFactoryExtraArgs: V2ExtraArgs; - lpFactoryExtraArgs: V2ExtraArgs; - depositFactoryDisclosure: DisclosedContract[]; - lpFactoryDisclosure: DisclosedContract[]; } interface RequestRemoveResult { requestCid: string; @@ -56,12 +50,6 @@ interface RequestRemoveResult { quoteOuts: string[]; allocations: V2AllocationSpecification[]; settlement: V2SettlementInfo; - depositFactoryCid: string; - lpFactoryCid: string; - depositFactoryExtraArgs: V2ExtraArgs; - lpFactoryExtraArgs: V2ExtraArgs; - depositFactoryDisclosure: DisclosedContract[]; - lpFactoryDisclosure: DisclosedContract[]; } function connectedParty(): string { @@ -74,6 +62,27 @@ const API_BASE = import.meta.env.VITE_API_BASE ?? 'http://localhost:8080'; const operator = new OperatorApi(API_BASE); +async function discoverAllocationFactory(params: { + admin: string; + settlement: V2SettlementInfo; + allocation: V2AllocationSpecification; + requestedAt: string; + inputHoldingCids: string[]; + actors: string[]; +}) { + return operator.getAllocationFactory({ + admin: params.admin, + choiceArguments: { + settlement: params.settlement, + allocation: params.allocation, + requestedAt: params.requestedAt, + inputHoldingCids: params.inputHoldingCids, + actors: params.actors, + extraArgs: EMPTY_EXTRA_ARGS, + }, + }); +} + async function getWalletNativeHoldings(owner: string): Promise { const walletState = useWalletStore.getState(); const providerId = walletState.activeProviderId; @@ -611,10 +620,6 @@ export interface DexContext { operator: string; lpRegistrar: string; admin: string; - allocationFactoryCid: string; - settlementFactoryCid: string; - allocationFactoryExtraArgs: V2ExtraArgs; - allocationFactoryDisclosure: DisclosedContract[]; network: string; } @@ -779,15 +784,26 @@ export const ledger = { quoteBinding: req.quoteBinding, }); + const requestedAt = new Date().toISOString(); + const factory = await discoverAllocationFactory({ + admin: params.pool.admin, + settlement: req.settlement as V2SettlementInfo, + allocation: req.allocationSpec as V2AllocationSpecification, + requestedAt, + inputHoldingCids, + actors: [params.swapperParty], + }); + // 2. Wallet authors the exact terminal allocation. const walletResult = await handToWallet({ kind: 'request-swap', poolId: params.pool.contractId, allocationSpec: req.allocationSpec as V2AllocationSpecification, settlement: req.settlement as V2SettlementInfo, - factoryCid: req.factoryCid, - allocationFactoryExtraArgs: req.allocationFactoryExtraArgs, - disclosure: req.allocationFactoryDisclosure, + requestedAt, + factoryCid: factory.factoryCid, + allocationFactoryExtraArgs: factory.extraArgs, + disclosure: factory.disclosure, inputHoldingCids: inputHoldingCids as ContractId<'Holding'>[], }); const swapperAllocationCid = walletResult.createdAllocationCids?.[0]; @@ -883,13 +899,23 @@ export const ledger = { ); } + const requestedAt = new Date().toISOString(); + const factory = await discoverAllocationFactory({ + admin: params.context.admin, + settlement: bindRes.settlement as V2SettlementInfo, + allocation: bindRes.allocationSpec as V2AllocationSpecification, + requestedAt, + inputHoldingCids, + actors: [trader], + }); const walletRes = await handToWallet({ kind: 'fund-order', - factoryCid: params.context.allocationFactoryCid as ContractId<'AllocationFactory'>, - allocationFactoryExtraArgs: params.context.allocationFactoryExtraArgs, - disclosure: params.context.allocationFactoryDisclosure, + factoryCid: factory.factoryCid, + allocationFactoryExtraArgs: factory.extraArgs, + disclosure: factory.disclosure, settlement: bindRes.settlement as V2SettlementInfo, allocationSpec: bindRes.allocationSpec as V2AllocationSpecification, + requestedAt, inputHoldingCids: inputHoldingCids as ContractId<'Holding'>[], hint: { instrumentId: lockInstrumentId, amount: lockAmount }, }); @@ -968,20 +994,34 @@ export const ledger = { requestedAt, }), }); + const holdingInputs = [ + params.baseHoldingCids ?? [], + params.quoteHoldingCids ?? [], + [], + ]; + const factories = await Promise.all( + req.allocations.map((allocation, index) => + discoverAllocationFactory({ + admin: allocation.admin, + settlement: req.settlement, + allocation, + requestedAt, + inputHoldingCids: holdingInputs[index] ?? [], + actors: [recipient], + }), + ), + ); const walletRes = await handToWallet({ kind: 'add-liquidity', requestCid: req.requestCid, settlement: req.settlement, allocations: req.allocations, - // Distinct factories per admin (deposits under pool.admin, LP receipt - // under pool.lpRegistrar) — both come from /request, not context. - depositFactoryCid: req.depositFactoryCid, - lpFactoryCid: req.lpFactoryCid, - depositFactoryExtraArgs: req.depositFactoryExtraArgs, - lpFactoryExtraArgs: req.lpFactoryExtraArgs, + requestedAt, + factoryCids: factories.map((f) => f.factoryCid), + allocationFactoryExtraArgs: factories.map((f) => f.extraArgs), // The request lives in our own DAR; accept needs no registry context. allocationRequestExtraArgs: EMPTY_EXTRA_ARGS, - disclosure: [...req.depositFactoryDisclosure, ...req.lpFactoryDisclosure], + disclosure: factories.flatMap((f) => f.disclosure), baseHoldingCids: params.baseHoldingCids ?? [], quoteHoldingCids: params.quoteHoldingCids ?? [], }); @@ -1068,17 +1108,29 @@ export const ledger = { requestedAt, }), }); + const holdingInputs = [[], [], holderLpHoldingCids]; + const factories = await Promise.all( + req.allocations.map((allocation, index) => + discoverAllocationFactory({ + admin: allocation.admin, + settlement: req.settlement, + allocation, + requestedAt, + inputHoldingCids: holdingInputs[index] ?? [], + actors: [params.holder], + }), + ), + ); const walletRes = await handToWallet({ kind: 'remove-liquidity', requestCid: req.requestCid, settlement: req.settlement, allocations: req.allocations, - depositFactoryCid: req.depositFactoryCid, - lpFactoryCid: req.lpFactoryCid, - depositFactoryExtraArgs: req.depositFactoryExtraArgs, - lpFactoryExtraArgs: req.lpFactoryExtraArgs, + requestedAt, + factoryCids: factories.map((f) => f.factoryCid), + allocationFactoryExtraArgs: factories.map((f) => f.extraArgs), allocationRequestExtraArgs: EMPTY_EXTRA_ARGS, - disclosure: [...req.depositFactoryDisclosure, ...req.lpFactoryDisclosure], + disclosure: factories.flatMap((f) => f.disclosure), lpHoldingCids: holderLpHoldingCids, }); const cids = walletRes.createdAllocationCids; @@ -1126,9 +1178,14 @@ async function fetchJson( path: string, init: RequestInit = {}, ): Promise { + const method = init.method ?? 'GET'; const res = await fetch(`${API_BASE}${path}`, { - headers: { 'Content-Type': 'application/json', ...(init.headers ?? {}) }, ...init, + headers: { + 'Content-Type': 'application/json', + ...apiAuthHeaders(path, method), + ...(init.headers ?? {}), + }, }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); if (res.status === 204) return undefined as T; diff --git a/app/web/src/services/operator-api.ts b/app/web/src/services/operator-api.ts index 706dac17..636d7559 100644 --- a/app/web/src/services/operator-api.ts +++ b/app/web/src/services/operator-api.ts @@ -3,6 +3,8 @@ // through `wallet/handoff.ts`; hosted RFQ routes are the documented relay // exception. +import { apiAuthHeaders } from "./api-auth"; + export type Party = string; export type ContractId<_T> = string; export type Decimal = string; @@ -21,6 +23,12 @@ export interface DisclosedContract { synchronizerId?: string; } +export interface AllocationFactorySurface { + factoryCid: ContractId<"AllocationFactory">; + extraArgs: V2ExtraArgs; + disclosure: DisclosedContract[]; +} + export interface SwapQuoteBinding { expectedPoolId: string; poolStateCid: ContractId<"PoolState">; @@ -114,6 +122,13 @@ export class OperatorApi { return this.post("/v1/swaps/quote", req); } + async getAllocationFactory(req: { + admin: Party; + choiceArguments: Record; + }): Promise { + return this.post("/v1/registry/allocation-factory", req); + } + // Operator builds the exact two-sided allocation against one pool snapshot; // the wallet authorizes it and swap() settles that same quote binding. async requestSwap(req: { @@ -126,9 +141,6 @@ export class OperatorApi { allocationSpec: unknown; settlement: unknown; quoteBinding: SwapQuoteBinding; - factoryCid: ContractId<"AllocationFactory">; - allocationFactoryExtraArgs: V2ExtraArgs; - allocationFactoryDisclosure: DisclosedContract[]; }> { return this.post("/v1/pools/swap/request", req); } @@ -169,9 +181,10 @@ export class OperatorApi { } async cancelRfq(rfqCid: ContractId<"Rfq">): Promise { + const path = `/v1/rfq/${encodeURIComponent(rfqCid)}/cancel`; const res = await fetch( - `${this.baseUrl}/v1/rfq/${encodeURIComponent(rfqCid)}/cancel`, - { method: "POST" }, + `${this.baseUrl}${path}`, + { method: "POST", headers: apiAuthHeaders(path, "POST") }, ); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); } @@ -260,7 +273,9 @@ export class OperatorApi { // === internals ============================================================ private async get(path: string): Promise { - const res = await fetch(`${this.baseUrl}${path}`); + const res = await fetch(`${this.baseUrl}${path}`, { + headers: apiAuthHeaders(path, "GET"), + }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); return (await res.json()) as T; } @@ -268,7 +283,10 @@ export class OperatorApi { private async post(path: string, body: unknown): Promise { const res = await fetch(`${this.baseUrl}${path}`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + ...apiAuthHeaders(path, "POST"), + }, body: JSON.stringify(body), }); if (!res.ok) throw new Error(`${res.status}: ${await res.text()}`); diff --git a/app/web/src/services/rfq-policy.ts b/app/web/src/services/rfq-policy.ts index d0e59fb5..867cd9b9 100644 --- a/app/web/src/services/rfq-policy.ts +++ b/app/web/src/services/rfq-policy.ts @@ -51,7 +51,8 @@ export function rankQuotes( const postedCmp = a.postedAt.localeCompare(b.postedAt); if (postedCmp !== 0) return postedCmp; // deterministic tie-breaker on dealer party id - return a.dealer.localeCompare(b.dealer); + // Daml compares Party text by code unit; localeCompare can disagree. + return a.dealer < b.dealer ? -1 : a.dealer > b.dealer ? 1 : 0; }); } diff --git a/app/web/src/vite-env.d.ts b/app/web/src/vite-env.d.ts index ee7058e0..baeab255 100644 --- a/app/web/src/vite-env.d.ts +++ b/app/web/src/vite-env.d.ts @@ -2,16 +2,29 @@ interface ImportMetaEnv { readonly VITE_API_BASE: string; + /** Published documentation URL opened by the app navigation. */ + readonly VITE_DOCS_URL?: string; /** Reown / WalletConnect Cloud project id. Get one at cloud.reown.com. */ readonly VITE_WC_PROJECT_ID?: string; /** CAIP network id for the target Canton network, e.g. canton:devnet. */ readonly VITE_CANTON_NETWORK_ID?: string; + readonly VITE_CANTON_SYNCHRONIZER?: string; + readonly VITE_CANTON_DEX_PACKAGE_ID?: string; + readonly VITE_CANTON_DEFAULT_PARTY?: string; + readonly VITE_CANTON_USER_ID?: string; + readonly VITE_ENABLE_SDK?: string; + readonly VITE_WALLET_GATEWAY_URL?: string; + readonly VITE_WALLET_GATEWAY_NAME?: string; + readonly VITE_WALLET_SHOW_FULL_CATALOG?: string; readonly VITE_ENABLE_PARTYLAYER?: string; + /** Enable the explicitly custodial RFQ write UI in a production build. */ + readonly VITE_ENABLE_HOSTED_RFQ?: string; readonly VITE_PARTYLAYER_APP_NAME?: string; readonly VITE_PARTYLAYER_NETWORK?: string; readonly VITE_PARTYLAYER_WALLET_IDS?: string; readonly VITE_PARTYLAYER_REGISTRY_URL?: string; readonly VITE_PARTYLAYER_REGISTRY_CHANNEL?: string; + readonly VITE_PARTYLAYER_CONNECT_TIMEOUT_MS?: string; } interface ImportMeta { diff --git a/app/web/src/wallet/canton-direct-provider.ts b/app/web/src/wallet/canton-direct-provider.ts index 34affa23..f730e845 100644 --- a/app/web/src/wallet/canton-direct-provider.ts +++ b/app/web/src/wallet/canton-direct-provider.ts @@ -1,18 +1,12 @@ -// Direct Canton ledger wallet provider. +// Disabled Direct Canton experiment. // -// Lightweight fallback for testnet/dev. Submits intents to the Canton -// JSON Ledger API directly using a bearer token, without WalletConnect -// pairing flow. The operator backend translates the intent into the -// concrete Daml command tree; this provider just signs and submits. -// -// Use cases: -// - Dev sessions where the user already has a JWT and a participant URL -// - Manual validation against a controlled testnet -// - Smoke testing the dApp without a wallet round-trip -// -// NOT suitable for end users: relies on the user trusting a long-lived -// JWT stored in localStorage. The Token Standard provider should be the -// default for real wallets. +// A participant JSON Ledger API can accept concrete Daml commands, but it does +// not expose the DEX-specific `/v1/wallet/execute` intent endpoint that an older +// version of this class called. Keeping a participant bearer token in browser +// localStorage would also be an unsafe public-deployment pattern. The provider +// registry therefore does not register this class, and both connect and submit +// fail closed. Use the dapp SDK, PartyLayer, or WalletConnect for a real wallet; +// use the development operator relay when explicitly testing backend signing. import type { WalletAccount, @@ -21,49 +15,25 @@ import type { WalletProvider, WalletResult, } from "./types"; -import { LiquidityAllocationUnsupportedError } from "./types"; -const LS_KEY = "canton-dex:direct:session"; - -interface PersistedSession { - ledgerUrl: string; - token: string; - party: string; -} +export const CANTON_DIRECT_DISABLED_MESSAGE = + "Direct Canton is intentionally unavailable: the participant API accepts concrete Daml commands, not DEX wallet intents. Use a supported external wallet or the DEV-only operator relay."; export class CantonDirectProvider implements WalletProvider { readonly id = "canton-direct"; - readonly label = "Direct Canton (advanced)"; + readonly label = "Direct Canton (disabled)"; private status: WalletConnectionStatus = { kind: "disconnected" }; private readonly listeners = new Set<(s: WalletConnectionStatus) => void>(); - private session: PersistedSession | null = null; constructor( - private readonly defaultLedgerUrl: string, - private readonly defaultToken: string, - ) { - // Auto-restore prior session on construction so a page reload keeps - // the user signed in. Dev-only: in prod we never rehydrate a persisted - // bearer-token session. - const stored = - import.meta.env.DEV && typeof window !== "undefined" - ? window.localStorage.getItem(LS_KEY) - : null; - if (stored) { - try { - this.session = JSON.parse(stored) as PersistedSession; - this.status = { - kind: "connected", - account: { party: this.session.party, label: this.label }, - providerId: this.id, - }; - } catch { - // Stored session was tampered — drop it. - window.localStorage.removeItem(LS_KEY); - } - } - } + // Preserve the old constructor shape for downstream imports while making + // it impossible to retain either credential. + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _defaultLedgerUrl = "", + // eslint-disable-next-line @typescript-eslint/no-unused-vars + _defaultToken = "", + ) {} getStatus(): WalletConnectionStatus { return this.status; @@ -80,74 +50,15 @@ export class CantonDirectProvider implements WalletProvider { } async connect(): Promise { - if (this.status.kind === "connected") return this.status.account; - // Never read/persist a long-lived bearer token outside dev. - if (!import.meta.env.DEV) { - const msg = - "canton-direct is a dev-only provider and is disabled in production builds"; - // eslint-disable-next-line no-console - console.error(`[wallet] ${msg}`); - this.setStatus({ kind: "error", message: msg }); - throw new Error(msg); - } - if (!this.defaultLedgerUrl || !this.defaultToken) { - const msg = "VITE_CANTON_LEDGER_URL and VITE_CANTON_AUTH_TOKEN must be set"; - this.setStatus({ kind: "error", message: msg }); - throw new Error(msg); - } - this.setStatus({ kind: "connecting" }); - try { - const res = await fetch(new URL("/v2/users/current", this.defaultLedgerUrl).toString(), { - headers: { Authorization: `Bearer ${this.defaultToken}` }, - }); - if (!res.ok) throw new Error(`ledger /v2/users/current returned ${res.status}`); - const body = (await res.json()) as { primaryParty?: string; party?: string }; - const party = body.primaryParty ?? body.party; - if (!party) throw new Error("ledger did not return a primary party"); - this.session = { ledgerUrl: this.defaultLedgerUrl, token: this.defaultToken, party }; - window.localStorage.setItem(LS_KEY, JSON.stringify(this.session)); - const account: WalletAccount = { party, label: this.label }; - this.setStatus({ kind: "connected", account, providerId: this.id }); - return account; - } catch (e) { - const msg = e instanceof Error ? e.message : String(e); - this.setStatus({ kind: "error", message: msg }); - throw e; - } + this.setStatus({ kind: "error", message: CANTON_DIRECT_DISABLED_MESSAGE }); + throw new Error(CANTON_DIRECT_DISABLED_MESSAGE); } async disconnect(): Promise { - this.session = null; - window.localStorage.removeItem(LS_KEY); this.setStatus({ kind: "disconnected" }); } - async submit(intent: WalletIntent): Promise { - if (this.status.kind !== "connected" || !this.session) { - throw new Error("canton-direct: not connected"); - } - if (intent.kind === "add-liquidity" || intent.kind === "remove-liquidity") { - // This provider cannot surface the created allocation cids required by - // the LP settle endpoint. - throw new LiquidityAllocationUnsupportedError(this.id); - } - // The Direct provider forwards the intent verbatim to the operator - // backend's intent-execution endpoint. The backend resolves it into - // a Daml command tree and signs as the trader (using the same - // direct bearer token under the hood). This is the simplest path - // for testnet smoke flows. - const res = await fetch(new URL("/v1/wallet/execute", this.session.ledgerUrl).toString(), { - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${this.session.token}`, - }, - body: JSON.stringify({ party: this.session.party, intent }), - }); - if (!res.ok) { - const text = await res.text(); - throw new Error(`wallet execute failed: ${res.status} ${text}`); - } - return (await res.json()) as WalletResult; + async submit(_intent: WalletIntent): Promise { + throw new Error(CANTON_DIRECT_DISABLED_MESSAGE); } } diff --git a/app/web/src/wallet/capabilities.ts b/app/web/src/wallet/capabilities.ts index 4197b6d2..54902c3e 100644 --- a/app/web/src/wallet/capabilities.ts +++ b/app/web/src/wallet/capabilities.ts @@ -50,8 +50,8 @@ export const WALLET_CAPABILITIES: Record = { coSignsAdmin: false, }, mock: { - dvp: "ready", - note: "Dev mock; deterministic cids.", + dvp: "dev-only", + note: "Dev only — returns deterministic placeholder cids; no ledger submission.", coSignsAdmin: true, }, partylayer: { @@ -64,11 +64,6 @@ export const WALLET_CAPABILITIES: Record = { note: "Settlement-accept only; cannot complete LP DvP.", coSignsAdmin: false, }, - "canton-direct": { - dvp: "unsupported", - note: "Settlement-accept only; cannot complete LP DvP.", - coSignsAdmin: true, - }, }; /** diff --git a/app/web/src/wallet/commands.ts b/app/web/src/wallet/commands.ts index 7fa9c27c..3c04f80d 100644 --- a/app/web/src/wallet/commands.ts +++ b/app/web/src/wallet/commands.ts @@ -116,7 +116,7 @@ function composeFundOrder( intent.allocationSpec, intent.inputHoldingCids, ctx.party, - ctx.now().toISOString(), + intent.requestedAt, intent.allocationFactoryExtraArgs, ), ], @@ -169,7 +169,7 @@ function composeRequestSwap( intent.allocationSpec, intent.inputHoldingCids, ctx.party, - ctx.now().toISOString(), + intent.requestedAt, intent.allocationFactoryExtraArgs, ), ], @@ -234,8 +234,7 @@ function composeAddLiquidity( intent: Extract, ctx: ComposeContext, ): ComposedCommands { - assertFactoryReady(intent.depositFactoryCid, "add-liquidity"); - assertFactoryReady(intent.lpFactoryCid, "add-liquidity"); + intent.factoryCids.forEach((cid) => assertFactoryReady(cid, "add-liquidity")); if (intent.allocations.length !== 3) { throw new Error(`add-liquidity: expected 3 allocation specs, got ${intent.allocations.length}`); } @@ -270,23 +269,21 @@ function batchingUtilityCommand( requestCid: ContractId<"LiquidityAllocationRequest">; settlement: V2SettlementInfo; allocations: V2AllocationSpecification[]; - depositFactoryCid: ContractId<"AllocationFactory">; - lpFactoryCid: ContractId<"AllocationFactory">; - depositFactoryExtraArgs: V2ExtraArgs; - lpFactoryExtraArgs: V2ExtraArgs; + requestedAt: string; + factoryCids: ContractId<"AllocationFactory">[]; + allocationFactoryExtraArgs: V2ExtraArgs[]; allocationRequestExtraArgs: V2ExtraArgs; disclosure: DisclosedContract[]; }, ctx: ComposeContext, holdingsBySpec: string[][], ): ComposedCommands { - const requestedAt = ctx.now().toISOString(); - const factoryCids = [intent.depositFactoryCid, intent.depositFactoryCid, intent.lpFactoryCid]; - const allocExtraArgs = [ - intent.depositFactoryExtraArgs, - intent.depositFactoryExtraArgs, - intent.lpFactoryExtraArgs, - ]; + const requestedAt = intent.requestedAt; + const factoryCids = intent.factoryCids; + const allocExtraArgs = intent.allocationFactoryExtraArgs; + if (factoryCids.length !== intent.allocations.length || allocExtraArgs.length !== intent.allocations.length) { + throw new Error("batching: each allocation requires its own factory and choice context"); + } // HoldingMap: GenMap ScopedAccount -> TextMap instrumentId -> [holding cids]. // A GenMap encodes as [key, value] pairs on the JSON Ledger API. const buckets = new Map< @@ -363,8 +360,7 @@ function composeRemoveLiquidity( intent: Extract, ctx: ComposeContext, ): ComposedCommands { - assertFactoryReady(intent.depositFactoryCid, "remove-liquidity"); - assertFactoryReady(intent.lpFactoryCid, "remove-liquidity"); + intent.factoryCids.forEach((cid) => assertFactoryReady(cid, "remove-liquidity")); if (intent.allocations.length !== 3) { throw new Error(`remove-liquidity: expected 3 allocation specs, got ${intent.allocations.length}`); } diff --git a/app/web/src/wallet/registry.ts b/app/web/src/wallet/registry.ts index f50221c0..c433cd81 100644 --- a/app/web/src/wallet/registry.ts +++ b/app/web/src/wallet/registry.ts @@ -1,6 +1,5 @@ // Wallet provider registry. Single place to add or gate providers. -import { CantonDirectProvider } from "./canton-direct-provider"; import { MockWalletProvider } from "./mock-provider"; import { DEFAULT_PARTYLAYER_CONNECT_TIMEOUT_MS, @@ -17,7 +16,6 @@ export type WalletProviderId = | "partylayer" | "token-standard" | "walletconnect" - | "canton-direct" | "mock"; function optionalEnv(name: string): string | undefined { @@ -56,36 +54,24 @@ function partyLayerClientFactory(networkId: string): () => Promise | null = null; function buildRegistry(): Map { + // An older, now-disabled Direct Canton experiment persisted a participant + // bearer credential at this key. Remove it during app startup even though the + // provider itself is no longer constructed. + if (typeof window !== "undefined") { + try { + window.localStorage.removeItem("canton-dex:direct:session"); + } catch { + // Storage can be unavailable in locked-down browser contexts. Direct + // Canton is still absent from the registry, so fail closed without + // preventing the safe wallet adapters from loading. + } + } const projectId = (import.meta.env.VITE_WC_PROJECT_ID ?? "") as string; const networkId = (import.meta.env.VITE_CANTON_NETWORK_ID ?? "canton:devnet") as string; - const ledgerUrl = (import.meta.env.VITE_CANTON_LEDGER_URL ?? "") as string; - // VITE_CANTON_AUTH_TOKEN is a long-lived bearer credential. It must never be - // read into a production bundle. In prod we refuse to read it and - // log an error so a misconfigured deploy is loud, not silently insecure. - const authToken = devOnlyAuthToken(); const apiBase = (import.meta.env.VITE_API_BASE ?? "http://localhost:8080") as string; const enableSdk = @@ -119,13 +105,16 @@ function buildRegistry(): Map { ), ); } - map.set("token-standard", new TokenStandardProvider(ledgerUrl, authToken, apiBase)); - if (projectId) map.set("walletconnect", new WalletConnectProvider(projectId, networkId)); - // canton-direct relies on a long-lived bearer token in localStorage, so it is - // gated to dev like `mock`. `authToken` is already "" in prod. - if (import.meta.env.DEV && ledgerUrl && authToken) { - map.set("canton-direct", new CantonDirectProvider(ledgerUrl, authToken)); + // This provider sends trader-authority commands through the operator relay. + // Keep the implementation available for local diagnosis, but do not expose + // it in a production bundle where it could be mistaken for self-custody. + if (import.meta.env.DEV) { + map.set("token-standard", new TokenStandardProvider(apiBase)); } + if (projectId) map.set("walletconnect", new WalletConnectProvider(projectId, networkId)); + // Direct Canton is intentionally not registered. A participant accepts + // concrete Ledger API commands, not DEX wallet intents, and a browser should + // never retain its bearer credential. See canton-direct-provider.ts. if (import.meta.env.DEV) map.set("mock", new MockWalletProvider()); return map; @@ -149,11 +138,11 @@ export function getProvider(id: WalletProviderId): WalletProvider { // operator effectively signs on the user's behalf. The relay is a dev-only // convenience and is gated behind `import.meta.env.DEV` below. // -// Real-build preference order: -// 1. PartyLayer when explicitly enabled (VITE_ENABLE_PARTYLAYER=1) — a real -// external multi-wallet connector. -// 2. WalletConnect when a project id is configured — a real external wallet. -// 3. SDK when enabled — a real CIP-0103 wallet. +// Real-build recommendation order follows the capability table: +// 1. SDK when enabled — the full DvP path is implemented. +// 2. PartyLayer when explicitly enabled — the path is implemented but remains +// marked unproven until the selected wallet passes live validation. +// 3. WalletConnect when configured — the current adapter is marked no-DvP. // 4. `null` (no auto-default): the user must pick a provider in the Connect // menu. We deliberately do NOT silently fall back to the operator relay. // In dev builds we keep `token-standard` as the convenient default so local @@ -163,9 +152,9 @@ function resolveDefaultProviderId(): WalletProviderId | null { const hasWalletConnect = !!(import.meta.env.VITE_WC_PROJECT_ID ?? ""); const enableSdk = (import.meta.env.VITE_ENABLE_SDK ?? "") === "1"; + if (enableSdk) return "sdk"; if (enablePartyLayer) return "partylayer"; if (hasWalletConnect) return "walletconnect"; - if (enableSdk) return "sdk"; // Dev convenience only: the operator relay default. Never in prod. if (import.meta.env.DEV) return "token-standard"; // No safe real wallet configured: force an explicit pick rather than routing diff --git a/app/web/src/wallet/sdk-provider.ts b/app/web/src/wallet/sdk-provider.ts index bc1760f0..fc29bbad 100644 --- a/app/web/src/wallet/sdk-provider.ts +++ b/app/web/src/wallet/sdk-provider.ts @@ -39,7 +39,7 @@ export interface SdkProviderOptions { } // Structural mirror of core-wallet-discovery's WalletPickerEntry/Result (not -// re-exported by @canton-network/dapp-sdk 1.1.0). The SDK calls our walletPicker +// re-exported by @canton-network/dapp-sdk). The SDK calls our walletPicker // with the discovered adapters and expects one back. interface PickerEntry { providerId: string; @@ -53,7 +53,7 @@ interface PickerEntry { // --- Browser CIP-103 wallet discovery ------------------------------------ // -// @canton-network/dapp-sdk 1.1.0 does not re-export its internal +// @canton-network/dapp-sdk does not re-export its internal // injected/announced discovery helpers, so we mirror the standard CIP-103 // browser handshake here (same shape the SDK uses internally): read the // `window.canton` injection namespace, and dispatch `canton:requestProvider` @@ -371,7 +371,12 @@ export class SdkProvider implements WalletProvider { try { result = await this.sdk.prepareExecuteAndWait({ commandId: composed.commandId, - commands: composed.commands as unknown as Record, + // The SDK deliberately types each Ledger API command payload as opaque; + // our composer supplies the same tagged command union with stricter + // inner fields. + commands: composed.commands as unknown as Parameters< + DappSDK["prepareExecuteAndWait"] + >[0]["commands"], actAs: composed.actAs, // Off-participant factory/request contracts (AllocationFactory, the // AllocationRequest) the trader's participant does not host must be @@ -379,7 +384,7 @@ export class SdkProvider implements WalletProvider { ...(composed.disclosedContracts && composed.disclosedContracts.length > 0 ? { disclosedContracts: composed.disclosedContracts } : {}), - } as Parameters[0]); + }); } catch (e) { // Surface the wallet or gateway's normalized error. throw new Error(`wallet submission failed: ${describeWalletError(e)}`); diff --git a/app/web/src/wallet/token-standard-provider.ts b/app/web/src/wallet/token-standard-provider.ts index 261052e1..e3c370fa 100644 --- a/app/web/src/wallet/token-standard-provider.ts +++ b/app/web/src/wallet/token-standard-provider.ts @@ -1,10 +1,12 @@ -// Token Standard V2 wallet provider — Canton-native, no backend hop. +// Development-only operator-signing relay. // -// The provider holds the user's JWT (from env or a per-user signing -// session) and submits Daml commands directly to the participant's -// JSON Ledger API at `/v2/commands/submit-and-wait`. The dApp never -// signs as the trader; this provider IS the signing surface for -// trader-authority actions. +// Its provider id is `token-standard`, because the commands it composes use +// the Token Standard V2 allocation interfaces. It is NOT a Token +// Standard wallet and it is NOT self-custodial: the browser posts shaped Daml +// commands to the operator backend's `/v1/wallet/submit` route, and that backend +// submits them with its configured ledger credential. The registry exposes this +// class only in Vite DEV builds. Production deployments must use an external +// wallet through the dapp SDK, PartyLayer, or WalletConnect adapters. // // What each intent maps to on-ledger: // @@ -16,16 +18,15 @@ // remove-liquidity → CreateAndExercise BatchingUtilityV2.ExecuteBatch // (accept + all 3 allocations in one command) // -// Connection lifecycle: -// - connect() validates the ledger URL, fetches the user's primary -// party via /v2/users/current, stores session in localStorage. -// - reload() re-reads the localStorage session so reloads don't -// drop the user. +// Development connection lifecycle: +// - connect() verifies the operator backend and uses the explicitly +// configured demo party. +// - reload() restores only the party and ledger user id from localStorage. // - disconnect() clears the session. // -// Session storage is intentionally narrow — just party + token + url. -// The party never changes during a session; the JWT is short-lived -// and refreshed via the wallet's auth flow (out of scope here). +// No participant JWT is read or stored here. Browser-to-backend write +// authorization is supplied separately by apiAuthHeaders; the backend's ledger +// credential remains server-side. import type { DisclosedContract, @@ -40,6 +41,7 @@ import { extractCreatedAllocationCids, extractLiquidityAcceptanceCid, } from "./commands"; +import { apiAuthHeaders } from "../services/api-auth"; const LS_KEY = "canton-dex:token-standard:session"; const SUBMIT_TIMEOUT_MS = 60_000; @@ -52,8 +54,6 @@ const PACKAGE_PREFIX = "#canton-dex-trading"; interface PersistedSession { - ledgerUrl: string; - token: string; party: string; userId: string; } @@ -85,29 +85,25 @@ function template(name: string): string { export class TokenStandardProvider implements WalletProvider { readonly id = "token-standard"; - readonly label = "Canton Wallet (Token Standard V2)"; + readonly label = "Operator Relay (dev only)"; private status: WalletConnectionStatus = { kind: "disconnected" }; private readonly listeners = new Set<(s: WalletConnectionStatus) => void>(); private session: PersistedSession | null = null; - constructor( - // Kept for typed parity with other providers. Browser submissions - // route through the operator backend's ledger proxy so local demos - // do not require participant CORS configuration. Production wallet - // integrations should hold their own credentials and submit through - // a participant endpoint that allows the dApp origin. - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _defaultLedgerUrl: string, - // eslint-disable-next-line @typescript-eslint/no-unused-vars - _defaultToken: string, - private readonly apiBase: string, - ) { - if (typeof window === "undefined") return; + constructor(private readonly apiBase: string) { + if (!import.meta.env.DEV || typeof window === "undefined") return; const stored = window.localStorage.getItem(LS_KEY); if (!stored) return; try { - this.session = JSON.parse(stored) as PersistedSession; + const parsed = JSON.parse(stored) as Partial; + if (typeof parsed.party !== "string" || typeof parsed.userId !== "string") { + throw new Error("invalid operator-relay session"); + } + // Rewrite the narrow shape so fields from an older implementation are + // not retained indefinitely in browser storage. + this.session = { party: parsed.party, userId: parsed.userId }; + window.localStorage.setItem(LS_KEY, JSON.stringify(this.session)); this.status = { kind: "connected", account: { party: this.session.party, label: this.label }, @@ -135,18 +131,24 @@ export class TokenStandardProvider implements WalletProvider { async connect(): Promise { if (this.status.kind === "connected" && this.session) return this.status.account; + if (!import.meta.env.DEV) { + const msg = + "the operator relay is development-only; configure an external wallet for production"; + this.setStatus({ kind: "error", message: msg }); + throw new Error(msg); + } if (!this.apiBase) { const msg = - "Set VITE_API_BASE in .env.local to use the Token Standard provider"; + "Set VITE_API_BASE in .env.local to use the development operator relay"; this.setStatus({ kind: "error", message: msg }); throw new Error(msg); } this.setStatus({ kind: "connecting" }); try { - // Resolve the user's party. In production a CIP-0103 wallet - // returns its own party id; on this testnet we use the env- - // configured default since the shared JWT has no primary party. + // A real wallet returns its own party. This relay instead uses an + // explicitly configured demo party whose ledger rights are held by the + // backend credential. const party = (import.meta.env.VITE_CANTON_DEFAULT_PARTY as string | undefined) ?? null; @@ -155,11 +157,11 @@ export class TokenStandardProvider implements WalletProvider { "ledger-api-user"; if (!party) { throw new Error( - "Set VITE_CANTON_DEFAULT_PARTY in .env.local. In production a CIP-0103 wallet would provide this; on testnet the operator allocates parties up front.", + "Set VITE_CANTON_DEFAULT_PARTY in .env.local to use the development operator relay.", ); } - // Verify the backend can talk to the ledger (proves the JWT is - // valid and the participant is reachable). + // This checks only that the backend is reachable. The first write is the + // point at which backend authorization and ledger submission are proven. const health = await fetch(`${this.apiBase}/v1/status`); if (!health.ok) { throw new Error( @@ -167,8 +169,6 @@ export class TokenStandardProvider implements WalletProvider { ); } this.session = { - ledgerUrl: this.apiBase, - token: "", party, userId, }; @@ -192,8 +192,13 @@ export class TokenStandardProvider implements WalletProvider { // -- intent dispatch ----------------------------------------------- async submit(intent: WalletIntent): Promise { + if (!import.meta.env.DEV) { + throw new Error( + "the operator relay is development-only; configure an external wallet for production", + ); + } if (this.status.kind !== "connected" || !this.session) { - throw new Error("token-standard: not connected"); + throw new Error("operator-relay: not connected"); } switch (intent.kind) { case "place-order": @@ -205,11 +210,11 @@ export class TokenStandardProvider implements WalletProvider { case "merge-holdings": case "add-liquidity": case "remove-liquidity": - // DvP swap + LP add/remove: author the allocation(s) via the shared - // composer and recover their created cids from the submit response + // DvP swap + LP add/remove: compose the allocation command(s), ask the + // operator backend to submit them, and recover their created cids. // The backend's /v1/wallet/submit now follows the transaction - // tree and returns createdEvents, so the operator-relay path CAN surface - // the allocation cids the settle needs — no CIP-0103 wallet required. + // tree and returns createdEvents, so this development relay can surface + // the allocation cids that settle needs. return this.submitComposed(intent); } } @@ -242,7 +247,10 @@ export class TokenStandardProvider implements WalletProvider { try { const res = await fetch(`${this.apiBase}/v1/wallet/submit`, { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + ...apiAuthHeaders("/v1/wallet/submit", "POST"), + }, body: JSON.stringify(body), signal: controller.signal, }); diff --git a/app/web/src/wallet/types.ts b/app/web/src/wallet/types.ts index 80ea66b1..bbe634c5 100644 --- a/app/web/src/wallet/types.ts +++ b/app/web/src/wallet/types.ts @@ -90,6 +90,7 @@ export interface FundOrderIntent { disclosure: DisclosedContract[]; settlement: V2SettlementInfo; allocationSpec: V2AllocationSpecification; + requestedAt: string; /** Holdings the wallet should propose to lock. */ inputHoldingCids: ContractId<"Holding">[]; /** @@ -127,6 +128,7 @@ export interface RequestSwapIntent { poolId: string; allocationSpec: V2AllocationSpecification; settlement: V2SettlementInfo; + requestedAt: string; factoryCid: ContractId<"AllocationFactory">; allocationFactoryExtraArgs: V2ExtraArgs; disclosure: DisclosedContract[]; @@ -155,24 +157,23 @@ export interface MergeHoldingsIntent { /** * Trader provides liquidity (DvP). The operator has created a * LiquidityAllocationRequest; the wallet authors the three allocations it - * names — base deposit + quote deposit (under `depositFactoryCid` = - * pool.admin) and the LP-token receipt (under `lpFactoryCid` = - * pool.lpRegistrar) — via a CreateAndExercise of the token standard's + * names — base deposit + quote deposit (under pool.admin) and the LP-token + * receipt (under pool.lpRegistrar) — via a CreateAndExercise of the token standard's * `BatchingUtilityV2.ExecuteBatch`, which accepts the request (leaving the * acceptance receipt) and authors all three inside ONE Daml transaction / one * top-level command for gateways that accept one command. `allocations` - * is the canonical order [base deposit, quote deposit, LP receipt]; the - * created cids are recovered operator-side from the single updateId for /settle. + * is the canonical order [base deposit, quote deposit, LP receipt]. + * `factoryCids` and `allocationFactoryExtraArgs` are parallel to that order; + * each pair comes from registry discovery for the exact Allocate arguments. */ export interface AddLiquidityIntent { kind: "add-liquidity"; requestCid: ContractId<"LiquidityAllocationRequest">; settlement: V2SettlementInfo; allocations: V2AllocationSpecification[]; - depositFactoryCid: ContractId<"AllocationFactory">; - lpFactoryCid: ContractId<"AllocationFactory">; - depositFactoryExtraArgs: V2ExtraArgs; - lpFactoryExtraArgs: V2ExtraArgs; + requestedAt: string; + factoryCids: ContractId<"AllocationFactory">[]; + allocationFactoryExtraArgs: V2ExtraArgs[]; /** Context for the AllocationRequest_Accept call (empty for the self-registry). */ allocationRequestExtraArgs: V2ExtraArgs; disclosure: DisclosedContract[]; @@ -183,19 +184,18 @@ export interface AddLiquidityIntent { /** * Trader removes liquidity (DvP). Symmetric to add: the wallet * authors the three allocations the request names — base receipt + quote - * receipt (under `depositFactoryCid` = pool.admin) and the LP burn-sender - * (under `lpFactoryCid` = pool.lpRegistrar, locking `lpHoldingCid`) — in - * canonical order [base receipt, quote receipt, LP burn-sender]. + * receipt (under pool.admin) and the LP burn-sender (under pool.lpRegistrar, + * locking `lpHoldingCids`) — in canonical order [base receipt, quote receipt, + * LP burn-sender]. The factory/context arrays use the same order. */ export interface RemoveLiquidityIntent { kind: "remove-liquidity"; requestCid: ContractId<"LiquidityAllocationRequest">; settlement: V2SettlementInfo; allocations: V2AllocationSpecification[]; - depositFactoryCid: ContractId<"AllocationFactory">; - lpFactoryCid: ContractId<"AllocationFactory">; - depositFactoryExtraArgs: V2ExtraArgs; - lpFactoryExtraArgs: V2ExtraArgs; + requestedAt: string; + factoryCids: ContractId<"AllocationFactory">[]; + allocationFactoryExtraArgs: V2ExtraArgs[]; /** Context for the AllocationRequest_Accept call (empty for the self-registry). */ allocationRequestExtraArgs: V2ExtraArgs; disclosure: DisclosedContract[]; @@ -230,9 +230,9 @@ export interface WalletResult { * For multi-allocation intents (add/remove-liquidity), the created * V2.Allocation cids in the SAME order as the intent's `allocations` — * i.e. the order the AllocationFactory_Allocate commands were emitted. The - * dApp forwards these to the operator-backend `/settle` call. Providers - * that cannot extract created-contract cids from their submit response - * MUST reject those intents rather than return this empty/partial. + * dApp forwards these to the operator-backend `/settle` call. An updateId-only + * provider omits this array and instead returns `auxiliaryCids.updateId`, which + * lets the operator recover the allocations from the transaction tree. */ createdAllocationCids?: string[]; /** diff --git a/app/web/src/wallet/walletconnect-provider.ts b/app/web/src/wallet/walletconnect-provider.ts index 0c430ff9..137e670d 100644 --- a/app/web/src/wallet/walletconnect-provider.ts +++ b/app/web/src/wallet/walletconnect-provider.ts @@ -9,7 +9,6 @@ // Environment configuration: // VITE_WC_PROJECT_ID — Reown / WalletConnect Cloud project id (required) // VITE_CANTON_NETWORK_ID — CAIP network id, e.g. "canton:devnet" (default: canton:devnet) -// VITE_CANTON_LEDGER_URL — Validator JSON Ledger API URL for non-signing reads // // Method-string note: // The Canton WalletConnect namespace methods follow CIP-0103 verb names diff --git a/app/web/vite.config.ts b/app/web/vite.config.ts index 95d423e2..4bee0b60 100644 --- a/app/web/vite.config.ts +++ b/app/web/vite.config.ts @@ -1,12 +1,37 @@ import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; -import path from 'path'; +import { resolve } from 'node:path'; export default defineConfig({ plugins: [react()], resolve: { alias: { - '@': path.resolve(__dirname, './src'), + '@': resolve(import.meta.dirname, './src'), + }, + }, + build: { + // The Canton wallet picker is published as one pre-bundled module. At the + // current lockfile SDK version it is about 580 kB minified (127 kB gzip), so + // Rolldown cannot divide it further. Keep that exception named and bound; + // all other third-party code is split into chunks no larger than 400 kB. + chunkSizeWarningLimit: 600, + rolldownOptions: { + output: { + codeSplitting: { + groups: [ + { + name: 'canton-wallet-ui', + test: /node_modules[\\/]@canton-network[\\/]core-wallet-ui-components[\\/]/, + priority: 10, + }, + { + name: 'vendor', + test: /node_modules[\\/]/, + maxSize: 400 * 1024, + }, + ], + }, + }, }, }, }); diff --git a/app/web/vitest.config.ts b/app/web/vitest.config.ts index 7af4df47..fe303cd2 100644 --- a/app/web/vitest.config.ts +++ b/app/web/vitest.config.ts @@ -1,12 +1,12 @@ import { defineConfig } from 'vitest/config'; import react from '@vitejs/plugin-react'; -import path from 'path'; +import { resolve } from 'node:path'; export default defineConfig({ plugins: [react()], resolve: { alias: { - '@': path.resolve(__dirname, './src'), + '@': resolve(import.meta.dirname, './src'), }, }, test: { diff --git a/docker-compose.yml b/docker-compose.yml index 0fd65256..9371ca16 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,15 @@ -version: "3.9" - services: backend: build: context: . dockerfile: Dockerfile.backend - ports: - - "8080:8080" + # Internal only: browsers enter through nginx on :80. Operators who need a + # direct diagnostic port can add a loopback-bound override explicitly. + expose: + - "8080" environment: PORT: "8080" + HOST: "0.0.0.0" CANTON_LEDGER_URL: "${CANTON_LEDGER_URL}" CANTON_LEDGER_TOKEN: "${CANTON_LEDGER_TOKEN}" CANTON_OPERATOR: "${CANTON_OPERATOR}" @@ -20,10 +21,19 @@ services: CANTON_DEX_PACKAGE_ID: "${CANTON_DEX_PACKAGE_ID}" CANTON_ALLOC_FACTORY_CID: "${CANTON_ALLOC_FACTORY_CID}" CANTON_SETTLE_FACTORY_CID: "${CANTON_SETTLE_FACTORY_CID}" + CANTON_LP_ALLOC_FACTORY_CID: "${CANTON_LP_ALLOC_FACTORY_CID:-}" + CANTON_LP_SETTLE_FACTORY_CID: "${CANTON_LP_SETTLE_FACTORY_CID:-}" DB_PATH: "/app/data/operator.db" INDEXER_INTERVAL_MS: "${INDEXER_INTERVAL_MS:-5000}" OPERATOR_ADMIN_TOKEN: "${OPERATOR_ADMIN_TOKEN}" - ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-http://localhost:80}" + DEX_OPERATOR_API_TOKEN: "${DEX_OPERATOR_API_TOKEN}" + DEX_READ_ONLY: "${DEX_READ_ONLY:-0}" + DEX_CALLER_JWT_SECRET: "${DEX_CALLER_JWT_SECRET:-}" + DEX_CALLER_JWT_AUDIENCE: "${DEX_CALLER_JWT_AUDIENCE:-}" + DEX_HOSTED_RFQ_RELAY: "${DEX_HOSTED_RFQ_RELAY:-0}" + # Browsers serialize the default HTTP port as http://localhost (without + # :80), so this must match the actual Origin header exactly. + ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-http://localhost}" volumes: - backend-data:/app/data restart: unless-stopped @@ -34,9 +44,24 @@ services: dockerfile: Dockerfile.frontend args: VITE_API_BASE: "${VITE_API_BASE:-}" + VITE_DOCS_URL: "${VITE_DOCS_URL:-https://srikanth-bitdynamics.github.io/Canton-Dex-Reference-Implementation/}" + VITE_APP_VERSION: "${VITE_APP_VERSION:-v0.6.0}" VITE_WC_PROJECT_ID: "${VITE_WC_PROJECT_ID:-}" VITE_CANTON_NETWORK_ID: "${VITE_CANTON_NETWORK_ID:-canton:devnet}" - VITE_CANTON_LEDGER_URL: "${VITE_CANTON_LEDGER_URL:-}" + VITE_CANTON_SYNCHRONIZER: "${VITE_CANTON_SYNCHRONIZER:-}" + VITE_CANTON_DEX_PACKAGE_ID: "${VITE_CANTON_DEX_PACKAGE_ID:-#canton-dex-trading}" + VITE_ENABLE_SDK: "${VITE_ENABLE_SDK:-0}" + VITE_WALLET_GATEWAY_URL: "${VITE_WALLET_GATEWAY_URL:-}" + VITE_WALLET_GATEWAY_NAME: "${VITE_WALLET_GATEWAY_NAME:-}" + VITE_WALLET_SHOW_FULL_CATALOG: "${VITE_WALLET_SHOW_FULL_CATALOG:-0}" + VITE_ENABLE_PARTYLAYER: "${VITE_ENABLE_PARTYLAYER:-0}" + VITE_ENABLE_HOSTED_RFQ: "${VITE_ENABLE_HOSTED_RFQ:-0}" + VITE_PARTYLAYER_APP_NAME: "${VITE_PARTYLAYER_APP_NAME:-Canton DEX}" + VITE_PARTYLAYER_NETWORK: "${VITE_PARTYLAYER_NETWORK:-canton:devnet}" + VITE_PARTYLAYER_WALLET_IDS: "${VITE_PARTYLAYER_WALLET_IDS:-console,nightly,send}" + VITE_PARTYLAYER_CONNECT_TIMEOUT_MS: "${VITE_PARTYLAYER_CONNECT_TIMEOUT_MS:-180000}" + VITE_PARTYLAYER_REGISTRY_URL: "${VITE_PARTYLAYER_REGISTRY_URL:-}" + VITE_PARTYLAYER_REGISTRY_CHANNEL: "${VITE_PARTYLAYER_REGISTRY_CHANNEL:-stable}" ports: - "80:80" depends_on: diff --git a/docs/README.md b/docs/README.md index 2eadab29..661df616 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,13 +1,25 @@ # Canton DEX — Documentation -A full-stack, **Token Standard V2 (CIP-0112)** reference DEX for the Canton -Network: Daml contracts, an operator backend, a React dApp with a CIP-0103 -wallet boundary, tests, and operator runbooks, covering RFQs, prefunded -orders, constant-product pools, swaps, and LP tokens. - -New here? Read **[Understand the design in 15 minutes](concepts/design-tour.md)**, -then use **[Getting Started](getting-started.md)** to run the full stack locally -without a Canton participant. +A full-stack code reference for a **Token Standard V2 (CIP-0112)** DEX on the +Canton Network: Daml contracts, an operator backend, a React dApp with a +CIP-0103 wallet boundary, tests, and operator runbooks. It covers RFQs, +prefunded orders, constant-product pools, swaps, and LP tokens. + +**Rendered site:** +[srikanth-bitdynamics.github.io/Canton-Dex-Reference-Implementation](https://srikanth-bitdynamics.github.io/Canton-Dex-Reference-Implementation/). +The site is published from `main`; changes on a branch become public after they +are merged and the GitHub Pages workflow finishes. + +New to Canton and Daml, but familiar with AMMs? Follow the canonical path below. +It is the only ordered newcomer curriculum in this documentation. + +> **Three run modes.** The local browser preview uses a TypeScript in-memory +> ledger and Mock Wallet; it does not settle value. Daml Script tests execute +> real Daml semantics without a participant. The default live proof starts a +> throwaway DPM sandbox and proves JSON Ledger API value movement without a +> browser or wallet. A live browser write needs the larger configured +> environment. [Getting started](getting-started.md) keeps these modes and their +> success criteria separate. > **Standards note.** This reference implements the Canton Network Token > Standard **V2 (CIP-0112)** — the privacy/performance/accounting revision of @@ -18,14 +30,39 @@ without a Canton participant. --- +## Canonical newcomer learning path + +Follow these steps in order. The glossary is a companion, not another step. + +| Step | Read or run | You are done when… | +|---:|---|---| +| 1 | [Canton and Daml primer](concepts/canton-daml-primer.md) | You can distinguish a party from a participant, a template from a contract, and DEX state from token value. | +| 2 | [Overview](concepts/overview.md) | You can explain the system boundary and the operator → wallet → operator swap authority sequence. | +| 3 | [Getting started](getting-started.md) | You have installed the tools and run the preview, the Daml proof, and the throwaway live-Canton proof without confusing their boundaries. | +| 4 | [AMM-first walkthrough](tutorials/amm-first-walkthrough.md) | You can trace `x*y=k` through `PoolState`, slices, allocation, and atomic settlement. | +| 5 | [15-minute design tour](concepts/design-tour.md) | You can name the actors and the four workflow families. | +| 6 | [Architecture](concepts/architecture.md) | You can locate market state, token custody, off-ledger orchestration, and the trust boundaries. | +| 7 | [Workflow design](concepts/workflows.md) | You can follow swap, liquidity, order, and RFQ state transitions. | +| 8 | [Make your first AMM code change](tutorials/make-your-first-amm-change.md) | A focused Daml test, the full suite, and the live sandbox proof pass after your edit. | +| 9 | [Builder guide](guides/builder-guide.md) | You can identify every layer affected by the extension you want to build. | + +Keep the [Glossary](concepts/glossary.md) open while reading. If Daml syntax +itself is new, the primer links the official language tutorial before asking +you to edit source. + +--- + ## Find your path | I want to… | Read, in order | |---|---| -| **Run it locally** | [Getting Started](getting-started.md) | -| **Learn DEX and TSv2 from Daml** | [15-minute Design Tour](concepts/design-tour.md) → [Glossary](concepts/glossary.md) → [Workflows](concepts/workflows.md) → [Builder Guide](guides/builder-guide.md) | -| **Understand the design** | [15-minute Design Tour](concepts/design-tour.md) → [Architecture](concepts/architecture.md) → [Workflows](concepts/workflows.md) | -| **Build on / extend it** | [Getting Started](getting-started.md) → [Builder Guide](guides/builder-guide.md) → [HTTP API](reference/http-api.md) | +| **Learn Canton/Daml from an AMM mental model** | Follow the [canonical newcomer learning path](#canonical-newcomer-learning-path) without skipping proof boundaries. | +| **Preview the UI locally** | [Getting started — Mode 1](getting-started.md#mode-1-run-the-browser-preview) | +| **Prove the Daml contracts locally** | [Getting started — Mode 2](getting-started.md#mode-2-run-the-daml-engine-proofs) → [Testing](reference/testing.md) | +| **Prove value movement on real Canton** | [Getting started — Mode 3](getting-started.md#mode-3-run-the-default-live-canton-proof) → [Local Canton from a clean clone](guides/localnet.md) | +| **Integrate a persistent/testnet environment** | [Local Canton](guides/localnet.md) → [Run on a testnet](guides/run-on-testnet.md) → [Validator test plan](guides/validator-test-plan.md) | +| **Understand the design** | [Overview](concepts/overview.md) → [15-minute Design Tour](concepts/design-tour.md) → [Architecture](concepts/architecture.md) → [Workflows](concepts/workflows.md) | +| **Build on / extend it** | Complete the [canonical newcomer learning path](#canonical-newcomer-learning-path), then use the [HTTP API](reference/http-api.md) as a lookup reference. | | **Operate a venue** | [Deployment](guides/deployment.md) → [Operator Guide](guides/operator-guide.md) → [Operator Runbook](guides/operator-runbook.md) | | **Integrate a registry** | [Registry Integration](guides/registry-integration.md) → [Choice Context](guides/choice-context.md) → [Allocation Surface](reference/allocation-surface.md) | | **Trade in the dApp** | [Using the dApp](guides/using-the-dapp.md) | @@ -39,56 +76,68 @@ The docs follow the [Diátaxis](https://diataxis.fr/) model, separating learning (tutorial), tasks (how-to guides), understanding (concepts), and lookup (reference). -### Start here — tutorial -| Page | What it covers | -|---|---| -| **[Getting Started](getting-started.md)** | Clone → build → run the whole stack (Daml core, backend, dApp) locally against the in-memory dev ledger, then test and explore. **Start here.** | - ### Concepts — understand the design + | Page | Audience | What it explains | |---|---|---| +| **[Canton and Daml primer](concepts/canton-daml-primer.md)** | First-time Canton/Daml builder | The minimum ledger mental model needed to read this codebase. | | **[15-minute Design Tour](concepts/design-tour.md)** | Daml developer, reviewer | The shortest code-backed path through actors, contracts, authority, custody, and all four settlement flows. | | [Overview](concepts/overview.md) | Everyone | What the DEX is, the trust model, and how it maps onto Token Standard V2. | | [Architecture](concepts/architecture.md) | Builder, integrator | The system model, component boundaries, and executor-authority constraints. | | [Workflows](concepts/workflows.md) | Builder, integrator | The venue workflows, the actor model, and the design principles behind them. | -| [Liquidity & Custody](concepts/liquidity-and-custody.md) | Integrator | How the pool represents and custodies LP liquidity (operator-custodied, DvP at the boundary). | +| [Liquidity & Custody](concepts/liquidity-and-custody.md) | Integrator | How the pool represents and custodies LP liquidity (operator-custodied; delivery-versus-payment — DvP — at the boundary). | | [LP Tokens](concepts/lp-tokens.md) | Builder, integrator | Why LP tokens are a single, unversioned V2 instrument per pool. | | [Pricing](concepts/pricing.md) | Operator, integrator | Where prices come from — pool-derived, order book, RFQ — and the (absent) oracle attachment points. | | [Glossary](concepts/glossary.md) | Everyone | The key terms: allocation, commitment, iterated settlement, DvP, slice, registrar, and more. | | [Non-goals](concepts/non-goals.md) | Everyone | What the reference intentionally does not include, and why. | +### Tutorials — learn by following one path + +| Page | Audience | Outcome | +|---|---|---| +| [Getting started](getting-started.md) | First-time builder | Install the tools and run the preview, Daml-engine proofs, and live-Canton sandbox proof without confusing their boundaries. | +| [AMM-first walkthrough](tutorials/amm-first-walkthrough.md) | AMM developer new to Canton | Locate the quote math, map pool state to contracts, follow operator → trader → operator authority, and run arithmetic, choreography, and real-holding swap proofs. | +| [Make your first AMM code change](tutorials/make-your-first-amm-change.md) | First-time Daml contributor | Complete one reproducible red/green edit and assess its Daml, backend, UI, and live-ledger impact. | + ### Guides — do a task + | Page | Audience | Recipe | |---|---|---| | [Builder Guide](guides/builder-guide.md) | Builder | The contract surface, off-ledger layout, matcher logic, and extension patterns. | | [Using the dApp](guides/using-the-dapp.md) | Trader, LP | Swap, add/remove liquidity, place orders, accept an RFQ quote, read the portfolio. | | [Add a Trading Pair](guides/add-a-trading-pair.md) | Operator | List a new pair (e.g. `ETH/USDT`) on a running venue. | | [Add an LP or Instrument](guides/add-lp-or-instrument.md) | Builder, operator | Register a fungible asset or identify where gated/lifecycle behavior requires a custom registry. | -| [Deployment](guides/deployment.md) | Operator | Local dev, Docker Compose, testnet, environment variables, production checklist. | +| [Local Canton from a clean clone](guides/localnet.md) | Builder, integrator | Run the default throwaway DPM sandbox proof; optionally use a separately distributed DevKit for persistent LocalNet. | +| [Deployment](guides/deployment.md) | Operator | Local dev, default DPM sandbox, optional DevKit LocalNet, Docker Compose, testnet, environment variables, and production checklist. | | [Operator Guide](guides/operator-guide.md) | Operator | First-time deployment and day-to-day operations. | | [Operator Runbook](guides/operator-runbook.md) | Operator, SRE | Recovery procedures, observability, and failure modes. | | [Run on a Testnet](guides/run-on-testnet.md) | Operator | Point the operator backend and dApp at a Canton testnet. | | [Registry Integration](guides/registry-integration.md) | Integrator | What the DEX assumes from an asset registry, and how to swap in your own. | | [Choice Context](guides/choice-context.md) | Integrator | What the backend attaches to each transaction it submits (context + disclosure). | -| [Validator Test Plan](guides/validator-test-plan.md) | QA, validator | The live end-to-end validation checklist. | +| [Validator Test Plan](guides/validator-test-plan.md) | QA, validator | The live, boundary-labelled validation checklist. | ### Reference — look something up + | Page | Topic | |---|---| | [HTTP API](reference/http-api.md) | The operator-backend HTTP endpoints, wallet intents, and error codes. | | [Allocation Surface](reference/allocation-surface.md) | The V2 allocation surface this reference relies on (committed allocations, iterated settlement). | +| [Daml proof map](reference/daml-proof-map.md) | Named learning paths from one concept to its Daml choices and focused executable tests, with each fixture's limitations. | | [Testing](reference/testing.md) | The test strategy, suite coverage, and opt-in live-ledger drivers. | | [Ecosystem feedback](reference/ecosystem-feedback.md) | How the reference was evaluated externally, what was found, and what changed. | --- ## Also in the repo -- **[Getting Started](getting-started.md)** doubles as the local test-suite - reference (Daml, backend, and dApp commands with expected counts). + +- **[Getting started](getting-started.md)** is the local run-mode and component + check reference. It states the exact success signal and limitation for each + command. - The [Builder Guide](guides/builder-guide.md) walks through the four workflow families — pair listing, matched-trade/RFQ, prefunded orders, and pool/swap/LP — with file and test pointers. ## Governance + [Contributing](../CONTRIBUTING.md) · [Code of Conduct](../CODE_OF_CONDUCT.md) · [Security Policy](../SECURITY.md) · [License (Apache 2.0)](../LICENSE) diff --git a/docs/concepts/architecture.md b/docs/concepts/architecture.md index 3b5a434b..066860d7 100644 --- a/docs/concepts/architecture.md +++ b/docs/concepts/architecture.md @@ -1,16 +1,14 @@ # Canton DEX Architecture -Canton DEX keeps market logic — orders, pools, RFQ — in its own Daml contracts, -but it never moves value itself: every settlement runs through the Token -Standard V2 (CIP-0112) allocation and batch-settlement APIs, so the exchange has -no bespoke token-escrow contract; pool custody is expressed through standard -V2 allocations. This page is the map of that -split. [Non-goals](non-goals.md) records what the reference deliberately leaves -out, and why. - -If this is your first code read, begin with the -[15-minute design tour](design-tour.md). It follows one value movement at a time -and links back into the templates and tests. +This is Step 6 of the +[canonical newcomer learning path](../README.md#canonical-newcomer-learning-path). +Complete the [15-minute design tour](design-tour.md) first. + +Canton DEX keeps market logic—orders, pools, and RFQs—in its Daml contracts. +Those contracts do not hold or move token value. Every settlement uses Token +Standard V2 (CIP-0112) allocations and batch settlement, including pool +custody. This page maps that boundary. [Non-goals](non-goals.md) records what +the reference deliberately leaves out. ## The three layers @@ -39,7 +37,7 @@ Three bands, top to bottom: - **Off-ledger orchestration** indexes the ledger, matches orders, prices pools, and submits operator-controlled choices. In the self-custodial order, swap, and LP flows it cannot lock a user's holdings; those funds are locked only by - an allocation the holder authors. The hosted RFQ relay is an explicitly + an allocation the holder authors. The operator-mediated RFQ path is an explicitly separate demo authority model, described below. - **On-ledger DEX contracts** enforce market rules — order limits, cancellation, pool accounting, and RFQ acceptance — and drive settlement, @@ -55,7 +53,7 @@ The trust boundary is the pair of dashed edges crossing into the ledger. For self-custodial flows the operator drives DEX choices under its own authority, but it can neither fund a trade nor bypass the validation those choices perform on-ledger. [The executor-control constraint](#the-executor-control-constraint) -makes that boundary precise. The hosted RFQ relay instead requires trader +makes that boundary precise. The operator-mediated RFQ path instead requires trader act-as rights and must not be mistaken for the self-custodial path. ## What settles value: the Token Standard V2 spine @@ -80,8 +78,10 @@ Two properties of the V2 allocation surface are load-bearing here: `Registry.V2` is the reference registry implementing these interfaces for the in-script tests, the testnet harness, and the live DEX. It is not privileged: any registry that implements the same V2 holding/allocation/settlement APIs can -back a traded instrument, so the DEX treats `InstrumentId` and registry-supplied -choice context as the stable integration boundary, not this template. The +back a traded instrument. A listed base/quote pair currently keeps both ids under +one registry admin; the LP registrar may differ. The DEX treats `InstrumentId` +and registry-supplied choice context as the stable integration boundary, not +this template. The guarantees the DEX relies on are enforced inside `SettlementFactory_SettleBatch`: allocation-to-leg coverage (exactly one allocation authorizes each side of each leg) and per-instrument sender/receiver balance across the whole batch. @@ -105,7 +105,7 @@ validated DEX choice → V2 batch settlement**. Each workflow has a named choice because its validation differs; value movement itself always ends at the same Token Standard settlement interface. -### Follow one order end to end +### Follow one order from intent through settlement ```mermaid flowchart LR @@ -306,36 +306,23 @@ The write surface is explicit rather than uniform: Ongoing market transitions use named choices that recheck their inputs. - Order funding, swap funding, and LP add/remove author trader allocations through the wallet before an operator choice can settle them. -- The hosted RFQ routes directly submit as the hosted trader, and RFQ accept +- The operator-mediated RFQ routes submit as the configured trader, and RFQ accept submits as both trader and operator. This requires corresponding ledger - rights and is a demo relay boundary, not a self-custodial wallet path. + rights and is an authority-boundary example, not a self-custodial wallet path + or a public relay service. The dApp (`app/web`) makes these paths visible through a wallet-provider boundary and a separate operator API client; neither path changes the on-ledger choice authorization. -## What proves it end to end - -- [`EndToEndTests.daml`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) - — fast choreography and authority checks against `MockRegistry`: order - placement → operator bind → trader-funded `Order_Fund`, RFQ accept, - swap construction, OTC settlement, and atomic order roll-forward. This suite - does not prove value movement because its fixture has no holdings. -- [`RegistryConservationTests.daml`](../../trading-tests/CantonDex/Tests/RegistryConservationTests.daml) - — the settlement spine rejects any batch whose allocations do not cover its - legs exactly or whose per-instrument sender/receiver totals do not balance, - and proves roll-forward funding stays within real locked backing across - iterations. -- [`PoolStateInvariantTests.daml`](../../trading-tests/CantonDex/Tests/PoolStateInvariantTests.daml) - — `PoolRules_ReconcileState` holds across a full add → swap → remove - lifecycle, and catches an omitted slice, a desynced operator-fabricated - `PoolState`, or a slice from a different pool. -- [`RealRegistryDvpTests.daml`](../../trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml) - — add, remove, swap, and cross-admin matched-trade settlement against an - upstream context-requiring V2 registry plus the reference LP registry. -- [`RfqSettlementTests.daml`](../../trading-tests/CantonDex/Tests/RfqSettlementTests.daml) - — RFQ buy and sell flows with exact holding deltas, no residual locks after a - successful settlement, and explicit expiry behavior for unsettled allocations. +## Where the proof lives + +Use the [Daml proof map](../reference/daml-proof-map.md) to connect an +architecture claim to its source choice and focused Daml Script test. Use the +[testing reference](../reference/testing.md) to understand the difference +between mock choreography, real-holding tests, backend/UI tests, and live +Canton proofs. Keeping the suite catalog in those reference pages avoids +duplicating volatile test names here. --- @@ -360,8 +347,9 @@ Three concrete upstream inputs shaped the architecture: Two further principles run through the design: it is **workflow-first** (the shape of choices and state transitions matters more than AMM feature parity — -see [Workflows](workflows.md)), and it trades **arbitrary `InstrumentId` pairs**, -not hardcoded "cash vs asset" families. +see [Workflows](workflows.md)), and it trades **arbitrary base/quote ids under +one registry admin**, not hardcoded "cash vs asset" families. Pairing two asset +admins is an explicit [app-layer limitation](non-goals.md#one-registry-admin-per-pair). ### Reference: reserves integrity in full @@ -471,4 +459,6 @@ canton-dex/ --- -**Where to read next:** [Workflows](workflows.md) · [Pricing](pricing.md) · [Liquidity & Custody](liquidity-and-custody.md) · [Glossary](glossary.md) · [All docs](../README.md) +**Next canonical step:** [Workflow design](workflows.md). Use +[Pricing](pricing.md), [Liquidity and custody](liquidity-and-custody.md), and +the [Glossary](glossary.md) as topic references. diff --git a/docs/concepts/canton-daml-primer.md b/docs/concepts/canton-daml-primer.md new file mode 100644 index 00000000..ea30686e --- /dev/null +++ b/docs/concepts/canton-daml-primer.md @@ -0,0 +1,333 @@ +# Canton and Daml primer for DEX builders + +This is Step 1 of the +[canonical newcomer learning path](../README.md#canonical-newcomer-learning-path). +It assumes you understand AMM reserves, `x*y=k`, swaps, fees, and LP shares, +but have not built a Canton application. + +This primer teaches the ledger concepts used by this repository. It is not a +complete Daml language course. Before editing Daml, complete Digital Asset's +official +[Get started with Daml](https://archived.docs.digitalasset.com/build/3.5/tutorials/get-started/index.html) +tutorial and +[basic contracts lesson](https://archived.docs.digitalasset.com/build/3.5/tutorials/smart-contracts/contracts.html). +Installation comes later in Step 3, +[Getting started](../getting-started.md#prerequisites). + +By the end, you should be able to answer four questions while reading code: + +1. What data is a contract carrying? +2. Which party can see it and which party must authorize a change? +3. Which choice archives or creates contracts? +4. Is the code changing DEX state, Token Standard value, or only an off-ledger + projection? + +## The shortest mental model + +Canton is the distributed-ledger system. Daml is the language and ledger model +used to define application contracts and their authorized transitions. + +```mermaid +flowchart LR + User[Trader] --> Wallet[Wallet] + Operator[Operator backend] --> API[Participant Ledger API] + Wallet --> API + API --> Daml[Daml contracts and choices] + API <--> Sync[Synchronizer] + Daml --> Visible[Per-party visible ledger state] +``` + +- A **party** is the on-ledger identity that authorizes actions. Trader, DEX + operator, asset admin, and LP registrar are distinct logical roles and are + normally separate parties in production; an explicitly documented local + learning setup may let some control roles share one party. +- A **participant** is the Canton node through which hosted parties read their + visible ledger state and submit commands. +- A **synchronizer** coordinates compatible participant transactions. It does + not make every contract globally visible like a public-chain full node. +- A **Daml contract** is an immutable instance of a template. +- A **choice** is a permitted transition on a contract. Its controller must + authorize the exercise. +- A **transaction** is atomic: all commands and nested choices commit, or none + do. + +The frontend does not become a ledger client merely because it can call the +operator backend. A self-custodial write crosses the trader's wallet because +only the trader can authorize trader-controlled commands. + +## Templates become contracts + +A Daml `template` combines data, visibility, authorization, and operations. A +shortened excerpt of +[`DexPair.daml`](../../trading/CantonDex/Dex/DexPair.daml) illustrates all four: + +```daml +template DexPair with + operator : Party + admin : Party + baseInstrumentId : Text + quoteInstrumentId : Text + active : Bool + where + signatory operator -- authorizes creation; always sees the contract + observer admin -- sees the contract; need not authorize creation + + choice DexPair_SetActive : ContractId DexPair + with newActive : Bool + controller operator -- only the operator authorizes this transition + do create this with active = newActive +``` + +Read it from top to bottom: + +1. `DexPair` is the schema for one market listing. +2. A created instance gets a contract ID, often called a `cid` in this repo. +3. The `operator` is the signatory; `admin` is an observer. +4. `DexPair_SetActive` is a consuming choice by default. Exercising it archives + the old pair contract and creates a successor with the new flag. + +That archive-and-create pattern is how immutable contracts represent state +updates. Do not look for a database-style in-place mutation. + +### Signatory, observer, and controller are different roles + +| Role | Question it answers | In the excerpt | +|---|---|---| +| Signatory | Who authorizes contract creation and is a stakeholder? | `operator` | +| Observer | Which additional stakeholder sees the contract? | `admin` | +| Controller | Who authorizes this choice exercise? | `operator` | + +Visibility is deliberate. A contract that is visible to the operator is not +automatically visible to every trader. Conversely, being able to see a +contract does not grant authority to exercise every choice on it. + +## Commands become one atomic transaction + +A client submits commands such as “create this template” or “exercise this +choice.” Choices can fetch other contracts and exercise nested choices. Canton +commits the resulting transaction only if authorization, visibility, +preconditions, and contract freshness all hold. + +A Daml Script test expresses the submitting authority explicitly: + +```daml +pairCid <- submit operator $ createCmd DexPair with ... + +newPairCid <- submit operator $ exerciseCmd pairCid DexPair_SetActive with + newActive = False +``` + +The important word is `operator` after `submit`. Replacing it with an unrelated +trader should fail because the choice controller is the operator. Tests use +this property to document both the happy path and forbidden paths. + +### Consuming and nonconsuming choices + +- A **consuming choice** archives the contract it is exercised on. It may + create a successor, as `DexPair_SetActive` does. +- A **nonconsuming choice** leaves that contract active. It is useful for a + stable rules contract that validates an operation without replacing itself. + +Do not assume “nonconsuming” means read-only. A nonconsuming choice may still +exercise other contracts and create or archive application state inside the +same transaction. + +## Parties are not services or users + +Keep these three concepts separate: + +| Concept | Example in this repo | Meaning | +|---|---|---| +| Human/application user | person using the Trade page | Off-ledger identity and session | +| Daml party | `trader`, `operator`, `lpRegistrar` | Ledger identity named in contracts and authorization | +| Canton participant | node exposing the Ledger API | Hosts parties, validates/submits commands, and stores their visible ledger state | + +A backend credential can submit as a party only when the participant grants +the corresponding ledger rights. Writing `actAs: [trader]` in a request does +not manufacture trader authority. + +Real Canton party IDs normally contain a hint and fingerprint, for example +`alice::1220…`. Short names such as `trader-demo` in the browser preview are +seed labels, not production party IDs. + +## Packages, DARs, and the Ledger API + +Daml source is built into a **DAR** (Daml Archive). A DAR contains one or more +compiled packages and their dependencies. A Canton participant must know the +packages before it can create those templates or exercise their choices. + +This repository separates three representations: + +```text +trading/**/*.daml + │ dpm build + ▼ +trading/.daml/dist/canton-dex-trading-0.1.4.dar + │ upload / vet for the target network + ▼ +Canton participant + │ JSON Ledger API + ▼ +services/operator-backend +``` + +- `daml.yaml` pins the SDK version and declares DAR dependencies. +- `dpm build` compiles the package (`dpm`, the Daml Package Manager, is the + SDK's build-and-test CLI used throughout this repo). +- Uploading a DAR makes package code available to a participant; it does not + create parties, holdings, pools, or liquidity. +- The backend's production ledger adapter sends JSON Ledger API commands and + reads transaction/contract data visible to its ledger user. + +The shortest proof that this package works on a real Canton process is the +repository's DPM sandbox runner. From the repository root, run: + +```bash +bash scripts/run-dpm-sandbox-proof.sh +``` + +It starts the Canton sandbox bundled with the pinned SDK, uploads the package +closure, and runs a live holdings/allocation/DvP driver. It is intentionally +throwaway. Its operator, asset admin, and LP registrar share the bootstrap +party, while the LP/trader and swapper are separately allocated so real value +moves between counterparties. Read +[Local Canton from a clean clone](../guides/localnet.md) before treating that +proof as evidence for any broader integration. + +## The Active Contract Set is current state + +The **Active Contract Set (ACS)** is the set of contracts that have been +created and not archived, as visible to the querying party. For an AMM, the +interesting active contracts include: + +- `Pool`: immutable pool configuration; +- `PoolState`: aggregate reserves used for pricing; +- `PoolSlice`: committed reserve inventory; +- `PoolRules`: stable choices for swap validation and execution; +- Token Standard `Holding` and `Allocation` contracts. + +The ACS is not a globally readable SQL table. Results depend on the querying +party's visibility. The backend indexer projects ledger events into a database +for API reads, but that database is a derived view, not the authorization or +settlement source of truth. + +## Why a Canton AMM needs Token Standard contracts + +The DEX contracts define market intent and validation. Token Standard V2 +contracts represent and move value. This separation is the central design of +the repository. + +| AMM idea | Daml/Token Standard representation | +|---|---| +| Trader's balance | one or more `Holding` contracts for an instrument | +| Permission to use exact funds for a trade | trader-authored `Allocation` tied to settlement terms | +| Pool reserves used for pricing | `PoolState.reserves` | +| Pool inventory that backs those reserves | committed allocation slices represented by `PoolSlice` | +| Atomic input-for-output exchange | `SettlementFactory_SettleBatch` inside the pool swap transaction | +| LP share | a Token Standard V2 LP instrument held in ordinary `Holding` contracts | + +An allocation is intentionally narrower than an ERC-20 router allowance. It +locks identified backing for a particular settlement specification and names +the authorized settlement context. The operator can execute a valid settle; it +cannot silently rewrite the trader's signed legs. + +## One swap, in Canton terms + +For a BTC-to-USDC swap, the flow is: + +```mermaid +sequenceDiagram + actor T as Trader + participant D as dApp + participant O as Operator + participant W as Wallet + participant L as Canton / Daml + D->>O: Request quote and Daml-built allocation specification + O->>L: Exercise PoolRules_RequestSwap + L-->>O: Exact input/output legs bound to a pool snapshot + O-->>D: Wallet intent + disclosed context + D->>W: Ask trader to authorize allocation + W->>L: AllocationFactory_Allocate as trader + L-->>D: Trader allocation contract / correlated update + D->>O: Settle using that allocation + O->>L: PoolRules_Swap as operator + L->>L: Validate quote, settle batch, update state and slices atomically + L-->>T: Updated visible holdings +``` + +There are two authorities because there are two decisions: + +- The trader authorizes the exact value locked from the trader's holdings. +- The operator authorizes execution against the venue's pool under on-ledger + rules. + +If the pool changed after the quote, the bound contract IDs are stale and the +transaction fails rather than silently repricing the signed trade. + +## “In memory” means two different things here + +This distinction prevents a common first-day misunderstanding: + +| Name used in the repo | Engine | Enforces Daml? | Holds Token Standard value? | Runs Canton? | +|---|---|---:|---:|---:| +| Backend `InMemoryLedger` | TypeScript map + selected handlers | No | No | No | +| Daml Script runner | Daml ledger engine | Yes | Yes, when the fixture creates real `Holding`s | No participant process | +| DPM sandbox proof | Real throwaway Canton process + JSON Ledger API | Yes | Yes | Yes, one local sandbox process | +| Optional DevKit LocalNet / remote testnet | Persistent Canton/Splice services | Yes | Yes | Yes | + +The browser preview uses the first row. `dpm test` uses the second. The default +live proof uses the third. DevKit is only an optional, separately distributed +manager for the fourth row; neither the DEX source nor its DARs depend on it at +runtime. Passing one row is not evidence that the next row is configured. + +## Map the repository before reading details + +```text +app/web/ user interface and wallet handoff + │ HTTP + ▼ +services/operator-backend/ orchestration, indexing, matching, ledger adapter + │ JSON Ledger API in live mode + ▼ +trading/CantonDex/Dex/ market-state templates and choices + │ nested Daml choices + ▼ +trading/CantonDex/Registry/ reference Token Standard holdings and settlement +``` + +`trading-tests/` drives the bottom two layers directly with Daml Script. The +tests are therefore the best executable contract documentation, but they do +not include the React dApp or HTTP backend. + +## A first reading exercise + +Open [`DexPair.daml`](../../trading/CantonDex/Dex/DexPair.daml) and answer: + +1. Which fields define the market and fee schedule? +2. Who signs the contract? +3. Who observes it? +4. Which choices can change it? +5. Does each choice mutate the old contract, or create a successor? + +Then open the beginning of +[`PoolWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/PoolWorkflowTests.daml). +Its header explains what the mock-registry fixture proves, what it does not +prove, and which pool scripts to read first. Use the +[Daml proof map](../reference/daml-proof-map.md) to find the real-holding proof +for each design claim. + +## You are ready to continue when… + +You can explain these statements in your own words: + +- A template is code; a contract is an active instance with a contract ID. +- A party supplies ledger authority; a participant is a node, not an identity. +- A choice describes a legal transition; its controller must authorize it. +- Contract visibility is party-scoped, not globally broadcast. +- The DEX validates market state, while Token Standard factories move value. +- Mock Wallet contract IDs prove a UI handoff only. +- Daml Script can prove contract behavior without proving the HTTP/live-network + integration. + +**Next canonical step:** [Overview](overview.md). Keep the +[Glossary](glossary.md) open as a companion reference. diff --git a/docs/concepts/design-tour.md b/docs/concepts/design-tour.md index 827a20dc..b094baa5 100644 --- a/docs/concepts/design-tour.md +++ b/docs/concepts/design-tour.md @@ -1,11 +1,14 @@ # Understand the design in 15 minutes -This page is the shortest path from Daml knowledge to the Canton DEX design. It -explains which contracts carry market state, which party authorizes each step, -and where Token Standard V2 moves value. Follow the links only when you need the +This page is the shortest path from the core Daml vocabulary to the Canton DEX +design. It explains which contracts carry market state, which party authorizes +each step, and where Token Standard V2 moves value. If terms such as template, +choice, controller, party, contract id, or active contract set are new, first +read the [Canton and Daml primer](canton-daml-primer.md); it assumes no Canton +background. Then return here and follow deeper links only when you need the detail behind a statement. -## 1. Start with the boundary +## Start with the boundary The DEX decides whether a market action is valid. A token registry owns holdings and performs value movement. @@ -30,7 +33,7 @@ The recurring workflow is: The operator can decide when to propose an action. It cannot author a trader's allocation or settle transfer legs outside the checks in the DEX choice. -## 2. Know the actors +## Know the actors | Actor | What it controls | What it cannot do alone | |---|---|---| @@ -39,11 +42,12 @@ allocation or settle transfer legs outside the checks in the DEX choice. | Asset registry admin | Registry implementation, factories, context, and token policy | Change a trader's signed DEX intent | | LP registrar | LP instrument policy and mint/burn recording | Move reserve assets without the pool settlement path | -The hosted RFQ demo is different: its relay has act-as rights for hosted parties. -That convenience is not the self-custodial authority model used by wallet-funded -orders, swaps, and liquidity. +The operator-mediated RFQ example is different: its backend ledger user has +act-as rights for configured parties. That authority model is not the +self-custodial path used by wallet-funded orders, swaps, and liquidity, and the +repository does not expose it as a public relay service. -## 3. The Token Standard settlement spine +## The Token Standard settlement spine A `Holding` is spendable token value. An `Allocation` locks holdings for one settlement and describes the sides its authorizer permits. A @@ -63,7 +67,7 @@ not a privileged registry. Production assets may come from another registry that implements the same V2 APIs. Read [Registry Integration](../guides/registry-integration.md) for the exact assumptions. -## 4. Pair and governance state +## Pair and governance state `DexPair` is the operator-signed listing record. It names one registry admin, the base and quote instrument ids, enabled trading modes, and fees. The operator @@ -81,12 +85,17 @@ Read: - Workflow map: [Active workflows](workflows.md#active-workflow-map) - Guide: [Add a trading pair](../guides/add-a-trading-pair.md) -## 5. Signed pool swaps +## Signed pool swaps `PoolRules_RequestSwap` reads a precise pool snapshot and returns one allocation specification containing both the trader's input side and every pool-to-trader output side. The wallet signs that complete specification. +A pool's reserves are not held as one balance per side: each side is a set of +many small `PoolSlice` allocations (detailed in the next section). A swap +consumes only an ordered few of them — the *output slice list* below — and leaves +the rest untouched. + `PoolRules_Swap` then: 1. requires the same pool state, input slice, output slice list, and slippage @@ -104,7 +113,7 @@ Read: - Math: [Pricing](pricing.md) - Proofs: `testPoolSwapViaRequestSwap` and `testRealRegistryDvpSwapSettles` -## 6. Liquidity and pool custody +## Liquidity and pool custody The pool is split so each concern remains small: @@ -124,28 +133,18 @@ batch per admin and passes each registry its own choice context. ### Why pool slices have no deadline -Reserve slices are operator-authored with `committed = true` and -`settlementDeadline = None`. Under the V2 withdrawal rule this means the -authorizer cannot call `Allocation_Withdraw` later. This is intentional for the -reference's long-lived inventory: the authorizer path and an LP cannot withdraw -a routine slice. The operator remains the allocation executor and can cancel it -when recovering or shutting down the pool. - -The consequence is explicit operator custody: - -- the LP holder is not the reserve allocation authorizer and has no unilateral - slice withdrawal; -- routine LP redemption requires the operator and LP registrar; -- the operator is the settlement executor and can cancel reserve allocations; -- if either service party disappears, the reference has no trustless LP exit. - -Adding an arbitrary deadline would not solve holder exit. It would instead give -the operator authorizer a future withdrawal path and require a safe slice-renewal -protocol. A production fork must choose and audit its own governed execution, -allocation renewal, and emergency redemption design. See +Reserve slices are `committed = true` with `settlementDeadline = None`, so under +the V2 withdrawal rule no one — not even the LP — can unilaterally call +`Allocation_Withdraw` on a routine slice. The operator holds custody: it is the +settlement executor and the only party that can release reserves, and routine LP +redemption needs the operator and LP registrar together. If either disappears, +the reference has no trustless LP exit. + +This is a deliberate long-lived-custody choice. The full rationale — including +why simply adding a deadline would not give holders an exit — is in [Liquidity and Custody](liquidity-and-custody.md#availability-and-the-lp-exit-boundary). -## 7. Prefunded orders +## Prefunded orders An order has two objects: an operator-signed `Order` containing market terms and a trader-authored allocation containing the reserved funds. @@ -177,10 +176,10 @@ Read: - Atomic fill: [`OrderMatchExecution.daml`](../../trading/CantonDex/Dex/OrderMatchExecution.daml) - Proof: `testOrderMatchRollsOrdersForwardAtomically` -## 8. RFQ and OTC settlement +## RFQ and OTC settlement An RFQ records a trader request and dealer quotes. `Rfq_Accept` jointly requires -the hosted trader and operator, records the ranking in a `PolicyReceipt`, and +the trader and operator, records the ranking in a `PolicyReceipt`, and creates a `MatchedTrade`. Each counterparty then authors an allocation and `MatchedTrade_Settle` groups the transfer legs by registry admin before calling that admin's settlement factory. @@ -194,7 +193,7 @@ Read: - Settlement: [`MatchedTrade.daml`](../../trading/CantonDex/Dex/MatchedTrade.daml) - Real holdings proof: `testRfqBuySettlesAgainstRealHoldings` -## 9. Cross-registry settlement +## Cross-registry settlement Factory contract ids are not sufficient. Before a registry choice, the operator fetches that admin's choice context and disclosed contracts off-ledger. Context is @@ -215,7 +214,7 @@ The evidence is intentionally both positive and negative: Read [Choice Context](../guides/choice-context.md) for backend assembly and submission details. -## 10. Active and compatibility surfaces +## Active and compatibility surfaces Public source can contain declarations that are not active workflow APIs. Code comments use one of these markers: @@ -233,7 +232,7 @@ flows use `CantonDex.Registry.V2` or another V2 registry. `Order_Adjust` and `Order_RecordPartialFill` are `[RETIRED]`; use `OrderMatchExecution_Execute`. -## 11. Choose the next detailed page +## Choose the next detailed page | If you want to understand | Read next | |---|---| @@ -243,3 +242,6 @@ flows use `CantonDex.Registry.V2` or another V2 registry. `Order_Adjust` and | Registry assumptions and context | [Registry Integration](../guides/registry-integration.md) | | What tests prove | [Testing](../reference/testing.md) | | Deliberate limitations | [Non-goals](non-goals.md) | + +**Next canonical step:** [Architecture](architecture.md). Use the other rows +above as topic references when you need their detail. diff --git a/docs/concepts/glossary.md b/docs/concepts/glossary.md index 9a03bf19..a544a057 100644 --- a/docs/concepts/glossary.md +++ b/docs/concepts/glossary.md @@ -1,9 +1,156 @@ # Glossary -Key terms used across the Canton DEX docs and code. Each entry is a one-line -definition; where it helps, it links to the Daml module that defines the term, -the test that exercises it, and the concept doc that covers it in depth. Source -paths are relative to the repo root (`trading/`, `trading-tests/`). +Key terms used across the Canton DEX docs and code. Start with **Canton and Daml +foundations** if this is your first Canton application; the second section is a +lookup for the Token Standard and exchange design. Where useful, entries link +to the defining Daml module, an executable test, or a deeper concept page. + +For a connected explanation rather than isolated definitions, read the +[Canton and Daml primer](canton-daml-primer.md). + +## Canton and Daml foundations + +### Active Contract Set (ACS) + +The contracts that have been created and not archived, as visible to the party +making the query. The ACS is current ledger state, not a globally readable +table: two parties can see different subsets. The backend indexer projects ACS +and transaction events into its off-ledger read model. + +### Canton + +The distributed-ledger system on which this application runs. Canton connects +participant nodes through synchronizers while preserving party-scoped +visibility; Daml defines the contracts and transactions participants process. + +### Canton DevKit + +An optional, separately distributed development tool that can manage a +persistent Docker-based Splice LocalNet. It is not required by the DEX source, +DARs, backend, or default live proof. If DevKit is unavailable, use the +repository's [DPM sandbox proof](../guides/localnet.md#path-a-portable-dpm-sandbox-proof). + +### Choice + +A named operation defined on a Daml template or interface. Exercising a choice +can fetch, create, archive, or exercise other contracts in one transaction, but +its [controller](#controller) must authorize it. A choice is consuming by +default; a `nonconsuming choice` leaves its target contract active. + +### Command + +A client's request to create a contract or exercise a choice. One submission +can contain multiple commands; the resulting Daml transaction either commits +atomically or fails as a whole. + +### Contract / contract ID (CID) + +An immutable on-ledger instance of a [template](#template). Its contract ID +identifies that exact active instance. When a consuming choice archives a +contract and creates its successor, the successor has a new ID. Values such as +`#mock-…:0` returned by Mock Wallet are UI placeholders, not Canton contract +IDs. + +### Controller + +The party or parties whose authority is required to exercise one Daml choice. +For example, `DexPair_SetActive` is controlled by the DEX operator. A party +that can see the contract is not necessarily its choice controller. + +### Daml + +The smart-contract language and ledger model used by this reference. A Daml +template declares contract data, stakeholders, and choices; the engine checks +authorization and atomic transitions. + +### Daml Script + +A Daml library and runner for allocating test parties, submitting commands, +querying contracts, and asserting results. `dpm test` runs this repository's +Script declarations in a Daml ledger engine. It enforces Daml semantics but +does not, by itself, start a Canton participant, backend, or browser. + +### DAR (Daml Archive) + +The build artifact containing compiled Daml packages and dependencies. Running +`dpm build` in `trading/` produces the DEX DAR. Uploading a DAR makes its code +available to a participant; it does not create application contracts or seed +liquidity. + +### DPM sandbox + +The real Canton sandbox process bundled with the Daml SDK selected by DPM. The +repository's default live proof starts it temporarily, uploads the package +closure, runs a JSON Ledger API DvP driver, and removes its state after success. +It is a one-process proof, not a persistent Splice LocalNet. See +[Local Canton](../guides/localnet.md#path-a-portable-dpm-sandbox-proof). + +### JSON Ledger API + +The HTTP/JSON API used by this repository's live backend adapter to submit Daml +commands and read ledger updates from a Canton participant. The local dev +server replaces this adapter with a TypeScript `InMemoryLedger`, so it does not +exercise the JSON Ledger API. + +### LocalNet + +A local network used for Canton/Splice development. In these docs, **DevKit +LocalNet** means the optional persistent Docker-managed environment; it is +distinct from the default throwaway [DPM sandbox](#dpm-sandbox). Neither is a +production topology. + +### Observer + +A contract stakeholder explicitly granted visibility without being required to +authorize its creation. Observing a contract does not automatically grant +authority to exercise its choices. + +### Package / package ID + +A compiled unit of Daml code with a content-derived package ID. Template IDs on +a live ledger include the package identity. The repository's package name and +version help humans find the DAR, but deployments must use the package IDs +actually uploaded and vetted on their network. + +### Participant + +A Canton node that hosts parties, exposes Ledger APIs, validates submissions, +and stores the ledger data visible to its hosted parties. A participant is +infrastructure; it is not the same thing as a [party](#party). + +### Party + +A logical on-ledger identity that can authorize Daml actions and be named as a +stakeholder. Traders, the DEX operator, the asset admin, and the LP registrar +are parties. Real Canton party IDs normally include a fingerprint such as +`alice::1220…`; `trader-demo` is only a local seed label. + +### Signatory + +A party that authorizes a Daml contract's creation and remains a stakeholder +with visibility while it is active. Signatories are declared in the template's +`where` block. + +### Synchronizer + +Canton infrastructure that coordinates transaction sequencing and confirmation +between connected participants. It does not turn every participant into a +public full node or make every contract visible to everyone. + +### Template + +A Daml definition containing the fields, signatories, observers, and choices +for one kind of contract. `PoolState` is a template; each live pool-state +contract is an instance with its own contract ID. + +### Transaction + +The atomic result of one submission: all creates, exercises, nested choices, +and archives commit together or none commit. A pool swap relies on this so +Token Standard settlement, reserve-slice updates, and `PoolState` replacement +cannot partially succeed. + +## Token Standard and DEX terms ### Allocation A Token Standard V2 contract that locks a holder's [holding](#holding) for one @@ -50,6 +197,14 @@ traditional-accounting revision of CIP-0056 that adds the allocation + settlement surface this DEX is built on. Often written "Token Standard V2" or "TSv2". +### Boundary slice + +The last reserve slice in the ordered set that a swap or liquidity removal draws +on to cover an amount. Earlier slices in the set are consumed in full; the +boundary slice is usually only partially drawn, so its unused remainder is +re-wrapped into a fresh `PoolSlice`. Selecting an ordered prefix this way keeps +each swap touching only a few slices rather than the whole pool. + ### Committed allocation An [allocation](#allocation) authored with `committed = True`, so the authorizer cannot unilaterally withdraw it before its deadline and the executor has an @@ -110,8 +265,8 @@ only supply and knows nothing about pools or orders. See [LP Tokens](lp-tokens.m The venue-signed trade contract [`Rfq_Accept`](#rfq-request-for-quote) emits: it carries the transfer legs plus an optional operator-signed [`PolicyReceipt`](#policyreceipt) and settles via a per-admin `SettleBatch`. -Template [`MatchedTrade`](../../trading/CantonDex/Dex/MatchedTrade.daml); proven -end-to-end in +Template [`MatchedTrade`](../../trading/CantonDex/Dex/MatchedTrade.daml); its +allocation and batch-settlement behavior is proven in [`RfqSettlementTests`](../../trading-tests/CantonDex/Tests/RfqSettlementTests.daml). ### Mint / burn account @@ -134,9 +289,10 @@ mint/burn mechanism is proven as part of atomic add/remove settlement in ### Operator The venue operator: it orchestrates matching, binds orders, and submits the settlement batches it is authorized to submit. It cannot settle a trader's -holdings without that trader's allocation. The hosted RFQ relay is a separate -authority model in which the backend ledger user is explicitly granted act-as -rights for hosted parties. +holdings without that trader's allocation. The operator-mediated RFQ path is a +separate authority model in which the backend ledger user is explicitly granted +act-as rights for configured parties; it is not a public relay supplied by the +repository. ### Over-lock Locking more backing than a settlement strictly needs. Token Standard V2 accepts @@ -194,4 +350,7 @@ See [CIP-0112](#cip-0112). --- -**Where to read next:** [Architecture](architecture.md) · [Workflows](workflows.md) · [Allocation Surface](../reference/allocation-surface.md) · [All docs](../README.md) +**Where to read next:** [Canton and Daml primer](canton-daml-primer.md) · +[AMM-first walkthrough](../tutorials/amm-first-walkthrough.md) · +[Architecture](architecture.md) · [Workflows](workflows.md) · +[Allocation Surface](../reference/allocation-surface.md) · [All docs](../README.md) diff --git a/docs/concepts/liquidity-and-custody.md b/docs/concepts/liquidity-and-custody.md index 1cfd44a5..f37851d1 100644 --- a/docs/concepts/liquidity-and-custody.md +++ b/docs/concepts/liquidity-and-custody.md @@ -117,7 +117,7 @@ transaction — so holdings and reserves change co-atomically, or nothing change other, updating both reserves in the same choice. [`PoolLiquidityRulesTests.daml`](../../trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml) -drives these end to end against the reference registry: an add funds base+quote +drives each Daml settlement path against the reference registry: an add funds base+quote and mints the LP holding in one flow; a remove delivers base+quote to the *holder* (not the operator) and burns the LP tokens; a stale supply quote aborts the settle. @@ -200,4 +200,6 @@ renewal problem. See [Non-goals](non-goals.md#lp-redemption-has-an-explicit-live the unmatched excess is refunded to the provider in the same batch and never reaches `reserves`. -**Where to read next:** [LP Tokens](lp-tokens.md) · [Pricing](pricing.md) · [Registry Integration](../guides/registry-integration.md) · [All docs](../README.md) +**Where to read next:** [LP Tokens](lp-tokens.md) · [Pricing](pricing.md) · +[Non-goals](non-goals.md) · [Registry Integration](../guides/registry-integration.md) · +[All docs](../README.md) diff --git a/docs/concepts/non-goals.md b/docs/concepts/non-goals.md index e672e9eb..76910ec3 100644 --- a/docs/concepts/non-goals.md +++ b/docs/concepts/non-goals.md @@ -21,7 +21,7 @@ and points at the guide or contract where the excluded work would live. | Fair ordering and MEV resistance | The operator privately observes orders and chooses match timing and submission order | A production sequencing, auction, or independently attested matching design | | A rich instrument lifecycle | Token Standard V2 standardizes the holding, not lifecycle; the DEX needs only a holding | The registry that administers the `InstrumentId` — [add an instrument](../guides/add-lp-or-instrument.md) | | A privileged reference registry | `Registry.V2` is a convenience so the DEX runs standalone, not the mechanism value settles through | Any conforming TSv2 registry (Amulet, or another) | -| Self-custody onboarding | The hosted relay is a testnet convenience, not a production wallet integration | The user's own compatible wallet or a deployment-specific delegation/co-submission flow | +| Self-custody onboarding | The included signing relay is a development diagnostic, not a production wallet or public onboarding service | The user's own compatible wallet or a deployment-specific delegation/co-submission flow | | Trustless LP emergency redemption | Reserve slices are operator-authored and removal is co-controlled by the operator and LP registrar | A production pool-governance and emergency-exit design | | Operational hardening | HA, secrets management, and a rate-limited gateway are an operator's deployment decisions | Whoever runs an instance — [operator runbook](../guides/operator-runbook.md) | | Production off-ledger services | The on-ledger contracts are the specification; the backend and indexer are one implementation of the surface around them | The integrator's own service — [architecture](architecture.md#off-ledger-services-what-they-may-and-may-not-do) | @@ -33,7 +33,7 @@ framework that a caller parameterises into arbitrary flows. The settlement pattern — allocate, then settle a batch atomically through the registry's `SettlementFactory_SettleBatch` — is meant to be read and reused, but the templates encode the DEX's own rules: constant-product pricing, price-time order -priority, best-execution RFQ ranking. Lifting that pattern into a general engine +priority, and deterministic RFQ eligibility ranking. Lifting that pattern into a general engine is a fork's job, not a configuration flag. See [architecture.md](architecture.md). ## One registry admin per pair @@ -79,10 +79,11 @@ atomically against both traders' funding allocations, and `OrderMatchExecution_Execute` re-checks the fill against both orders' own limit prices, quantities, instruments, and bound allocations — so a buggy or malicious off-ledger matcher cannot settle a fill the traders never agreed to. Proven by -[EndToEndTests.daml](../../trading-tests/CantonDex/Tests/EndToEndTests.daml): -`testMatchedTradeFullSettle` (two trader allocations settle in one operator batch) -and `testOrderMatchEnforcesLimitPrice` (`OrderMatchExecution_Execute` refuses a -fill outside either order's limit price). +[TradeWorkflowTests.daml](../../trading-tests/CantonDex/Tests/TradeWorkflowTests.daml) +proves that two trader allocations settle in one operator batch. +[OrderWorkflowTests.daml](../../trading-tests/CantonDex/Tests/OrderWorkflowTests.daml) +proves that `OrderMatchExecution_Execute` refuses a fill outside either order's +limit price. ## Fair ordering and private MEV @@ -116,40 +117,40 @@ stays at the minimum it needs. ## The reference registry is one option, not the mechanism -`CantonDex.Registry.V2` is a self-contained reference registry so the DEX can run -end to end without depending on an external one. It is not the settlement -mechanism, and it is not privileged. The dApp and operator reach any conforming -TSv2 registry through its factories, choice context, and disclosure; the reference +`CantonDex.Registry.V2` is a self-contained reference registry so the DEX can +run a complete local settlement flow without depending on an external one. It +is not the settlement mechanism, and it is not privileged. The dApp and +operator reach any conforming TSv2 registry through its factories, choice +context, and disclosure; the reference does not assume its own registry is present, nor that every registry exposes the same conveniences. [architecture.md](architecture.md#what-settles-value-the-token-standard-v2-spine) and [registry-integration.md](../guides/registry-integration.md) set out exactly -what a registry must provide. On the public testnet the pair's assets happen to be -issued by this registry. Integrating another conforming registry also requires -its factory discovery, choice context, disclosures, and metadata endpoint. - -## The hosted testnet is a demo surface, not a wallet - -The public deployment lets a visitor with no wallet trade, by minting a hosted -demo party and relaying its signatures through a fixed, allowlisted set of choices -under per-IP and daily caps. This is explicitly a testnet convenience, not -self-custody: the walletless connect options are marked **DEV** and are never -preselected in a testnet or production build -([using-the-dapp.md](../guides/using-the-dapp.md#connecting-a-wallet)). A real user -brings their own wallet (PartyLayer or the dapp-sdk) and signs for themselves; the -hosted relay exists only so the reference flows can be exercised from a browser -without one. The `/v1/testnet/*` relay surface and the faucet's per-IP party -quota are documented in -[ecosystem-feedback.md](../reference/ecosystem-feedback.md). - -**Current deployment status.** On the public testnet at -`testnet-dex.bitdynamics.cc`, every tester is onboarded as a hosted party on the -operator's (BitDynamics) validator, and every traded asset (`dBTC`, `dUSD`, and the -pool's LP token) is issued locally by the deployment's own Token Standard V2 -registry. This deployment choice does not change the application boundary: -self-custodial users connect through a compatible wallet and registry, while a -hosted party authorizes only the allowlisted demo operations exposed by the -relay. Registry choice context and disclosures still determine whether a given -external instrument can participate in a settlement. +what a registry must provide. A deployment may issue its demo assets through +this registry; integrating another conforming registry also requires its factory +discovery, choice context, disclosures, and metadata endpoint. + +## The development relay is not a wallet + +The repository includes `POST /v1/wallet/submit` only for local developer +diagnosis. It is disabled by default, requires `DEX_DEV_WALLET_RELAY=1`, is +registered by the dApp only in a development build, and restricts submissions +to `DEX_DEV_RELAY_PARTIES`. The production-oriented testnet server does not +enable it. It does not create parties, mint faucet assets, impose public-user +quotas, or implement a `/v1/testnet/*` surface. + +That relay is not self-custody: the backend forwards commands with its ledger +credential and therefore needs permission to act for every requested party. A +real deployment must instead use a compatible wallet (PartyLayer or a +CIP-0103 provider), or deliberately design and secure its own delegation or +co-submission service. The repository neither provisions nor promises a public +hosted deployment. See [connecting a wallet](../guides/using-the-dapp.md#connecting-a-wallet) +and the [historical ecosystem feedback](../reference/ecosystem-feedback.md). + +The separately named `DEX_HOSTED_RFQ_RELAY` option is narrower: it can enable +the existing RFQ create/cancel/accept routes in `testnet-server.ts`, with +mandatory caller-JWT binding. It still does not create or fund parties, publish +a hostname, or add a `/v1/testnet/*` API. Whoever enables it owns the custodial +authority, identity, abuse-prevention, and operations design. ## LP redemption has an explicit liveness dependency @@ -182,8 +183,8 @@ reference settlement flow. The reference includes an operator runbook covering deployment, recovery, and observability ([operator-runbook.md](../guides/operator-runbook.md)), but it is not -a hardened production service. There is no HA, no rate-limited public gateway -beyond the testnet caps, no secrets-management integration, and the operator's +a hardened production service. There is no HA, rate limiting, public gateway or +faucet, secrets-management integration, and the operator's authority is a single party. These are an operator's deployment decisions, deliberately left to whoever runs an instance rather than baked into the reference — the runbook's own [out-of-scope @@ -194,7 +195,7 @@ line. The operator backend and indexer are a working reference, not a prescription. The indexer is a single-writer SQLite projection sized for a testnet; the backend is -one Node process. They show what an integrator needs to read and relay, not the +one Node process. They show what an integrator needs to read and orchestrate, not the only way to build it. The on-ledger contracts are the specification; the off-ledger services are one implementation of the surface around them ([architecture.md](architecture.md#off-ledger-services-what-they-may-and-may-not-do)). diff --git a/docs/concepts/overview.md b/docs/concepts/overview.md index 75692c2a..c1fad090 100644 --- a/docs/concepts/overview.md +++ b/docs/concepts/overview.md @@ -1,26 +1,26 @@ # Overview -This is your first stop. It says what Canton DEX is, shows the whole system on -one diagram, and points you at the doc that answers your next question. +This is Step 2 of the +[canonical newcomer learning path](../README.md#canonical-newcomer-learning-path). +Complete the [Canton and Daml primer](canton-daml-primer.md) first. This page +shows what the DEX does, where authority sits, and how its main pieces connect. ## What Canton DEX is -Canton DEX is a runnable **Token Standard V2 (CIP-0112) reference exchange** for -the Canton Network. An exchange has two separate jobs: decide the terms of a -trade, then move both sides' assets without either party taking settlement risk. -This reference shows four ways to decide the terms: +Canton DEX is a runnable **Token Standard V2 (CIP-0112) reference exchange** on +Canton. An exchange first agrees the terms, then moves both assets atomically. +This reference shows four ways to agree the terms: - an **automated market maker (AMM)** calculates a price from two pool reserves; - an **order book** crosses compatible buy and sell limit orders; - a **request for quote (RFQ)** lets selected dealers quote a larger trade; and - an **OTC matched trade** records terms the two parties already agreed. -All four use the same value-movement boundary. The holder locks funds in a Token -Standard V2 allocation, the DEX choice validates the market-specific terms, and -the registry settles every transfer leg atomically. There is no custom -off-ledger balance model. Self-custodial swap, order, and liquidity flows keep -trader authority in the wallet; the hosted RFQ demo uses an explicitly -documented operator relay. +All four move value the same way. The holder locks funds in a Token Standard V2 +allocation. A DEX choice validates the terms. The registry then settles every +leg atomically. There is no custom off-ledger balance model. For swaps, orders, +and liquidity, the trader keeps authority in the wallet. The RFQ demo uses a +separate operator-mediated authority model. The repo ships the Daml package, operator backend, React dApp with a CIP-0103 wallet boundary, tests, and runbooks. Its demo stack runs without a Canton @@ -49,18 +49,21 @@ Suppose a trader wants to sell `0.1 BTC` into a BTC/USDC pool: `PoolSlice` contracts reference the committed allocations that actually back those reserves. 2. The backend reads `PoolState` to show an estimated USDC output. - `PoolRules_RequestSwap` returns the exact input-allocation specification the - wallet must authorize; it does not fix the eventual execution price. + `PoolRules_RequestSwap` validates a named state and ordered slice snapshot, + calculates the exact output from that snapshot, and returns the complete + input-and-output allocation specification the wallet must authorize. 3. The trader's wallet locks `0.1 BTC` in a V2 allocation. The operator cannot create this allocation on the trader's behalf in the self-custodial flow. 4. The backend submits `PoolRules_Swap` with that allocation and the reserve - slices needed for the output. -5. The choice calculates the execution price from current state, checks the trader's - minimum output, verifies every allocation, and calls + slices bound into the request. +5. The choice re-derives the same output from the bound state, checks the + minimum, bound contract IDs, exact signed legs, and allocations, then calls `SettlementFactory_SettleBatch`. 6. BTC moves to the pool and USDC moves to the trader atomically. The choice recreates `PoolState` and binds the remaining reserve value to successor - slices. If any check fails, neither side moves. + slices. If the pool changed after the request, the bound contract IDs are + stale and the swap fails instead of silently repricing; the request must be + recreated. If any check fails, neither side moves. The other workflows change how terms are formed and what state is recreated; they do not invent a different custody or settlement mechanism. @@ -82,10 +85,11 @@ decentralized operator; [Non-goals](non-goals.md) explains each boundary. There are two submission paths, split by **who is allowed to sign what**. A wallet signs trader-authored allocations for orders, swaps, and liquidity. The -operator backend submits listing, matching, and settlement commands. The hosted -RFQ demo also relays trader-authority commands, so its ledger user must have -act-as rights for the hosted trader; that exception is not a self-custodial -wallet model. Both paths submit into `canton-dex-trading`, whose trading +operator backend submits listing, matching, and settlement commands. The +operator-mediated RFQ example also submits trader-authority commands, so its +ledger user must have act-as rights for the configured trader; that exception is +not a self-custodial wallet model or a public relay service. Both paths submit +into `canton-dex-trading`, whose trading surfaces settle through a Token Standard V2 registry. ```mermaid @@ -103,7 +107,7 @@ flowchart TB Trader -->|"reads + orchestration APIs"| Operator Trader -->|"signs trader-authority commands"| Wallet - Operator -->|"operator submissions + hosted RFQ relay"| Ledger + Operator -->|"operator submissions + mediated RFQ"| Ledger Wallet -->|"trader-authority submissions"| Ledger ``` @@ -144,6 +148,12 @@ allocate-then-settle-a-batch pattern: | **RFQ** | `Rfq` / `RfqQuote` → `Rfq_Accept` → `MatchedTrade` | the dealer's quoted price | | **OTC** | `MatchedTrade` → `MatchedTrade_Settle` | leg amounts both sides pre-agreed | +`DexPair.active` and `DexPair.tradingMode` tell off-ledger discovery and routing +which surfaces to expose. In this reference they are not on-ledger settlement +gates: `PoolRules` and `OrderMatchExecution` do not fetch `DexPair`. A production +fork that needs a ledger-enforced listing pause must bind and validate the pair +contract in its terminal choices. + Settlement is **grouped by registry admin**. One DEX choice can call one batch per admin inside the same Daml transaction, so every batch succeeds or the whole transaction aborts. `MatchedTrade_Settle` shows the shape: @@ -180,14 +190,15 @@ settlement rather than a call into a router. For DvP settlement, the operator cannot spend a trader's holdings without a trader-authored allocation. When a trader funds an order, adds liquidity, or authorizes a swap, the dApp composes that command and the trader's **wallet** -signs it over CIP-0103. The hosted RFQ UI uses a different trust model: its -backend co-submits as the hosted trader and operator, and therefore needs both -ledger authorities. [Architecture](architecture.md) draws these boundaries; +signs it over CIP-0103. The operator-mediated RFQ UI uses a different trust +model: its backend co-submits as the configured trader and operator, and +therefore needs both ledger authorities. [Architecture](architecture.md) draws these boundaries; [Workflows](workflows.md) shows how each flow choreographs them. -## How to read these docs +## Reference map -Read top to bottom for the design, or jump to the row that matches your question. +The canonical learning order lives in the [documentation index](../README.md#canonical-newcomer-learning-path). +Use this table only to look up a topic while reading: | Doc | What you'll learn | |---|---| @@ -197,6 +208,7 @@ Read top to bottom for the design, or jump to the row that matches your question | [Pricing](pricing.md) | Where every executable price comes from (pool curve, limit price, quote) and why there is no oracle. | | [LP Tokens](lp-tokens.md) | Why each pool's LP share is a single, unversioned V2 instrument. | | [Liquidity & Custody](liquidity-and-custody.md) | How the pool custodies reserves as committed slices and crosses the LP boundary via DvP. | +| [Daml proof map](../reference/daml-proof-map.md) | Exact source choices, focused Daml Script tests, and commands for each design claim. | | [Glossary](glossary.md) | The vocabulary: allocation, commitment, iterated settlement, DvP, slice, registrar. | | [Non-goals](non-goals.md) | What the reference leaves out on purpose, and why. | @@ -205,21 +217,13 @@ Read top to bottom for the design, or jump to the row that matches your question > do their own security review, operational hardening, compliance work, and > version-compatibility checks. -The tests separate fast workflow choreography from real-value settlement: - -- **AMM pool** — [`testPoolSwapEndToEnd`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) - checks the choice choreography against `MockRegistry`, while - [`testRealRegistryDvpSwapSettles`](../../trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml) - proves exact value movement against a context-requiring V2 registry. -- **Order book** — [`testOrderFundingFlow`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) - proves intent → operator binding → trader-authored allocation → funded - order; [`testPartialFillUsesRolledFundingBudget`](../../trading-tests/CantonDex/Tests/RegistryConservationTests.daml) - proves a partial fill retains real locked backing. -- **RFQ** — [`testRfqBuySettlesAgainstRealHoldings`](../../trading-tests/CantonDex/Tests/RfqSettlementTests.daml) - proves the accepted quote, policy receipt, exact balance deltas, and lock - cleanup against real holdings. -- **OTC** — [`testMatchedTradeSettlesPerAdminLegSubsets`](../../trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml) - settles a cross-admin trade atomically against real registry holdings. +## Where the executable proof lives + +The [Daml proof map](../reference/daml-proof-map.md) connects each design claim +to its current source choice and focused test. The +[testing reference](../reference/testing.md) explains what mock choreography, +real-holding Daml Script, backend, UI, and live-Canton tests each prove. Test +names stay there so this concept page remains readable when suites move. --- @@ -238,4 +242,5 @@ the exact Splice release is recorded in [Allocation Surface](../reference/allocation-surface.md) reference records the committed-allocation and iterated-settlement semantics the pool depends on. -**Where to read next:** [Getting Started](../getting-started.md) · [Architecture](architecture.md) · [Workflows](workflows.md) · [All docs](../README.md) +**Next canonical step:** [Getting started](../getting-started.md). +Keep the [Glossary](glossary.md) open as a companion reference. diff --git a/docs/concepts/workflows.md b/docs/concepts/workflows.md index 82e7c2e6..9878d495 100644 --- a/docs/concepts/workflows.md +++ b/docs/concepts/workflows.md @@ -1,11 +1,14 @@ # Canton DEX workflow design -Each state transition has a named app choice. A terminal, value-moving choice -validates the workflow's business rules and delegates settlement to Token -Standard V2. It may call more than one `SettlementFactory_SettleBatch` when the -instruments have different registry admins, but those calls remain atomic inside -one Daml transaction. The app contracts own market state; the registry owns -holdings and settlement. +This is Step 7 of the +[canonical newcomer learning path](../README.md#canonical-newcomer-learning-path). +Complete [Architecture](architecture.md) first. + +Each state transition has a named app choice. A value-moving choice validates +the business rules, then asks Token Standard V2 to settle. Different registry +admins may require more than one `SettlementFactory_SettleBatch`, but all +batches remain atomic inside one Daml transaction. App contracts own market +state; the registry owns holdings and settlement. ## The common workflow in five steps @@ -60,7 +63,7 @@ may exercise them. | Place order | `OrderFundingRequest_Bind` | `AllocationFactory_Allocate` | `Order_Fund` | pending order becomes funded | | Match orders | funded buy + sell orders | already prefunded | `OrderMatchExecution_Execute` | atomic fill; each remainder rolls forward | | Cancel order | funded or partially filled order | already prefunded | `Order_Cancel` | order closes and remaining funding unlocks | -| Accept RFQ | `Rfq` + `RfqQuote` | `Rfq_Accept` under the hosted authority model | `Rfq_Accept` | `MatchedTrade` and policy receipt are created; no value moves yet | +| Accept RFQ | `Rfq` + `RfqQuote` | `Rfq_Accept` under the operator-mediated authority model | `Rfq_Accept` | `MatchedTrade` and policy receipt are created; no value moves yet | | Settle RFQ / OTC | `MatchedTrade` allocation requests | each counterparty authors its allocation | `MatchedTrade_Settle` | bilateral legs settle atomically | ## Actors and core contracts @@ -77,6 +80,12 @@ the LP-token policy (`LPTokenPolicy`). This is a template boundary, not a custom Daml-interface boundary: the DAR implements upstream Token Standard V2 interfaces but defines no app-facing interface of its own. +`DexPair.active` and `DexPair.tradingMode` are listing metadata for off-ledger +discovery and routing. They are deliberately absent from the value-moving table +above: neither `PoolRules` nor `OrderMatchExecution` fetches a pair contract, so +changing those fields does not itself block a direct Daml settlement. Bind and +validate `DexPair` in terminal choices if a fork needs an on-ledger market gate. + ## The settlement shape every workflow shares Two mechanics recur below and are worth stating once, because they are the @@ -145,12 +154,8 @@ settleResult <- exercise factoryCid V2.SettlementFactory_SettleBatch with extraArgs ``` -Proven in -[`EndToEndTests.daml`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) — -`testPoolSwapEndToEnd` (reserves move, the consumed input slice is replaced by -its next-iteration slice, sibling slices stay untouched) and -`testPoolSwapViaRequestSwap` (the spec `PoolRules_RequestSwap` emits settles -end to end). +For the focused choreography and real-holding checks behind this section, see +[Daml proof map — AMM pool](../reference/daml-proof-map.md#amm-pool). ## Add and remove liquidity @@ -169,12 +174,23 @@ sequenceDiagram LP->>L: BatchingUtility_ExecuteBatch Note over LP,L: one wallet approval: Accept + 3 Allocate actions D->>O: POST /v1/pools/add-liquidity/settle (allocation cids) + O->>L: PreviewAddAllocations + O->>O: discover exact allocation factories + choice contexts + O->>L: allocate operator/registrar sides + O->>L: PreviewAddSettlement + O->>O: discover exact settlement factories + choice contexts O->>L: PoolLiquidityRules_SettleAddLiquidity Note over O,L: base/quote batch under pool.admin,
LP mint batch under pool.lpRegistrar L-->>O: funds in pool, LP tokens minted, PoolState rewritten ``` -`PoolLiquidityRules_SettleAddLiquidity` runs the split-admin DvP: the LP's +The previews are read-only Daml choices. They return the exact canonical V2 +choice arguments, which the backend sends to each registry's operation-specific +off-ledger discovery endpoint before exercising the real allocate or settle +choice. This avoids guessing a factory contract or reusing context from a +different operation. + +`PoolLiquidityRules_SettleAddLiquidity` then runs the split-admin DvP: the LP's committed deposits and LP-mint receipt settle together, the operator's receiver allocations roll forward into the two new `PoolSlice`s, and the registrar mints LP tokens to the provider. Only the ratio-matched part of an off-ratio deposit @@ -210,12 +226,8 @@ LP has no unilateral exit if the operator or registrar becomes unavailable. See [Availability and the LP exit boundary](liquidity-and-custody.md#availability-and-the-lp-exit-boundary) and [LP redemption has an explicit liveness dependency](non-goals.md#lp-redemption-has-an-explicit-liveness-dependency). -Proven in -[`PoolLiquidityRulesTests.daml`](../../trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml) — -`testDvpAddLiquidity` (LP funds base+quote and receives real LP holdings in one -flow), `testDvpAddOffRatioRefundsExcess` (the unmatched leg is refunded, not -donated), `testDvpRemoveDeliversToHolder` (base+quote go to the holder, LP burns), -and `testDvpMultiSliceRemove` (a redemption draws across multiple slices). +The add, refund, remove, and full-redemption proofs are cataloged in +[Daml proof map — AMM pool](../reference/daml-proof-map.md#amm-pool). ## Order lifecycle @@ -285,11 +297,8 @@ expiry, instruments, and backing, but cannot prove fair intake ordering or stop censorship and private reordering among valid fills. This distinction is documented as [Fair ordering and private MEV](non-goals.md#fair-ordering-and-private-mev). -Proven in -[`EndToEndTests.daml`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) — -`testOrderMatchEnforcesLimitPrice` (a fill outside `[ask, bid]` is rejected) and -`testOrderMatchRollsOrdersForwardAtomically` (both orders roll onto the minted -allocations and the trade is recorded, in one transaction). +For the limit, roll-forward, backing, and cancellation proofs, see +[Daml proof map — Resting orders](../reference/daml-proof-map.md#resting-orders). ## RFQ and OTC block trades @@ -299,8 +308,8 @@ ranked. ```mermaid sequenceDiagram - actor T as Hosted trader - actor Dl as Hosted dealer + actor T as Trader + actor Dl as Dealer participant O as Operator backend participant L as Ledger T->>O: POST /v1/rfq (create Rfq) @@ -321,18 +330,25 @@ ranks the considered quotes, records the winner and its rank in a published policy was applied, not that the price was good), and copies the RFQ's `expiresAt` onto the trade's `settlementDeadline`. +`RfqQuote.tier` is dealer-declared in this reference. The operator observes the +quote and endorses the considered set by co-authorizing `Rfq_Accept`; there is no +separate on-ledger tier-administration contract. Policy v2.0 ranks tier, later +expiry, earlier posting time, then dealer party id; price is deliberately not a +ranking key, and the trader still chooses which considered quote to accept. + The included RFQ page covers creation, quote review, and acceptance through -hosted-party relay routes: the backend ledger user -must have act-as rights for the trader (and dealer when it authors quotes), while +operator-mediated API routes: the backend ledger user must have act-as rights +for the trader (and dealer when it authors quotes), while accept also needs operator authority. This is distinct from the wallet-authored allocation flow used by pools and orders. A self-custodial deployment must -replace the relay with a wallet, delegation, or co-submission mechanism that -supplies the same controllers. +replace that example with a wallet, delegation, or co-submission mechanism that +supplies the same controllers. The repository does not provision a public RFQ +relay or party-onboarding service. The page's **Accepted** tab means that `Rfq_Accept` created the `MatchedTrade`; it does not mean balances moved. The following allocation requests and `MatchedTrade_Settle` are available through the Daml and operator-service flow -and are covered by the settlement tests, but the hosted RFQ page does not drive +and are covered by the settlement tests, but the RFQ page does not drive those later steps. ```daml @@ -349,13 +365,15 @@ that trade cannot settle after the deadline; their owners must cancel or withdraw them to release the locked holdings. Integrators therefore need to leave enough time between acceptance, wallet funding, and settlement. -Proven in -[`RfqSettlementTests.daml`](../../trading-tests/CantonDex/Tests/RfqSettlementTests.daml), -which runs against real `Registry.V2` holdings — -`testRfqBuySettlesAgainstRealHoldings` (balances and the rank-1 receipt are -exactly as expected, no locks stranded) and -`testExpiryBetweenAcceptAndSettleBlocksTheSettle` (past the inherited deadline -the settle fails and the funds stay locked). +For receipt, real-holding, deadline, and cancellation proofs, see +[Daml proof map — RFQ and OTC](../reference/daml-proof-map.md#rfq-and-otc). + +### Explicit exits and recovery choices + +Failure and abandonment are explicit choices, not hidden background cleanup. +The [resting-order](../reference/daml-proof-map.md#resting-orders) and +[RFQ/OTC](../reference/daml-proof-map.md#rfq-and-otc) proof tables identify the +controller and resulting contract/fund-state checks for each exit. ## Pool lifecycle @@ -366,11 +384,15 @@ emergency stop: stateDiagram-v2 [*] --> Unfunded: pool created Unfunded --> Active: first add-liquidity settles - Active --> Active: swap / add / remove + Active --> Active: swap / add / partial remove + Active --> Unfunded: final LP removal Active --> Paused: PoolRules_Pause Paused --> Active: PoolRules_Resume ``` +The mock lifecycle, real first-funding, and complete-redemption checks are in +[Daml proof map — AMM pool](../reference/daml-proof-map.md#amm-pool). + --- ## Reference @@ -378,9 +400,12 @@ stateDiagram-v2 ### Secondary workflows - **Pair listing.** `DexOperator` creates a `DexPair` recording the base/quote - `InstrumentId`s, fee model, and trading mode (RFQ, order book, or pool). There - is no separate `DexRules` admission contract yet; a production fork can add one - if listing needs multi-party approval. + `InstrumentId`s, fee model, and mode (`TM_OrderBook`, `TM_Pool`, or `TM_Both`). + `active` and `tradingMode` guide off-ledger listing/routing only; they are not + fetched by the active settlement choices. There is no separate `DexRules` + admission contract yet; a production fork can add one if listing needs + multi-party approval. Source and focused checks are in + [Daml proof map — Pair listing metadata](../reference/daml-proof-map.md#pair-listing-metadata). - **Pool creation.** `DexOperator` creates a `Pool` for a `DexPair` and the LP instrument definition (an `InstrumentConfig` in the reference registry). The pool starts `Unfunded` with a constant-product invariant until @@ -430,4 +455,7 @@ interfaces and no separate `DexRules` governance contract. --- -**Where to read next:** [Architecture](architecture.md) · [Liquidity and custody](liquidity-and-custody.md) · [Non-goals](non-goals.md) · [Pricing](pricing.md) · [Builder Guide](../guides/builder-guide.md) · [Allocation Surface](../reference/allocation-surface.md) · [All docs](../README.md) +**Next canonical step:** [Make your first AMM code change](../tutorials/make-your-first-amm-change.md). +Use [Liquidity and custody](liquidity-and-custody.md), +[Pricing](pricing.md), [Non-goals](non-goals.md), and the +[Allocation Surface](../reference/allocation-surface.md) as topic references. diff --git a/docs/getting-started.md b/docs/getting-started.md index 942447ea..2ad7060b 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,199 +1,402 @@ -# Local Setup & Testing +# Getting started: choose what you want to prove -One page to clone, build, run, test, and explore the whole reference DEX on -your machine: the Daml core, the operator backend, the dApp, and the scripts. -The local path needs **no Canton participant**: the dev backend ships an -in-memory ledger, so you can have the full stack up in a few minutes. +This is Step 3 of the +[canonical newcomer learning path](README.md#canonical-newcomer-learning-path). +Steps 1–2 establish the Canton/Daml vocabulary and system boundary. This page +installs the tools and turns that model into three increasingly realistic +local proofs. -> Quick start -> ```bash -> git clone https://github.com/srikanth-bitdynamics/Canton-Dex-Reference-Implementation.git && cd Canton-Dex-Reference-Implementation -> bash scripts/run-local-daml-tests.sh # Daml build + tests -> (cd services/operator-backend && npm ci && npm run dev) # backend → :8080 -> (cd app/web && cp .env.example .env.local && npm ci && npm run dev) # dApp → :5173 -> ``` +This repository has three useful local experiences, but they do not prove the +same thing. Start by choosing the result you need: -## What's in the repo +| Mode | What you run | What it proves | What it does **not** prove | +|---|---|---|---| +| **1. Browser preview** | React dApp + operator backend + seeded `InMemoryLedger` | The screens render, reads and quotes are wired, and wallet intents have the expected shape | Daml authorization, wallet signatures, Token Standard allocations, or value settlement | +| **2. Daml-engine tests** | `dpm test` through the repository scripts | Daml choices, party authorization, atomicity, rounding, and value conservation in the Daml Script runner | The browser, backend, JSON Ledger API, or a multi-node Canton deployment | +| **3. Live Canton proof** | `scripts/run-dpm-sandbox-proof.sh` | A real throwaway Canton process, JSON Ledger API, package upload, distinct LP/swapper parties, and add → quote-bound swap → partial remove DvP (delivery-versus-payment) settlement | Browser/backend HTTP integration, external-wallet compatibility, production-grade rights/topology, persistent state, or production readiness | -| Path | Component | Stack | -|---|---|---| -| `trading/` | `canton-dex-trading` Daml package — pool/swap/LP, orders, RFQ, matched-trade, reference V2 registry | Daml 3.5 | -| `trading-tests/` | in-script test suites for the Daml core | Daml | -| `services/operator-backend/` | operator HTTP API, JSON-LAPI driver, idempotency, indexer, recovery; in-memory dev ledger | TypeScript / Node | -| `app/web/` | the dApp — Trade / Pools / Orders / RFQ / Portfolio / Admin + wallet layer | TypeScript / React / Vite | -| `scripts/` | build, smoke, registry-bootstrap, and LocalNet/testnet drivers | bash / ts-node | -| `vendor/splice/dars/` | canonical Splice 0.6.12 Token Standard release DARs (committed build inputs) | Daml | -| `docs/` | architecture, workflows, operator runbook, deployment, this page | — | - -> **One-command sanity check.** After installing (below), `bash -> scripts/e2e-smoke.sh` boots the in-memory backend, exercises every key -> endpoint, verifies the responses, and exits non-zero on any failure — no Canton -> participant needed. +Within this step, run Mode 1, then Mode 2, then Mode 3. You may jump directly +to a mode when you only need its proof, but a first-time reader should keep the +order. Mode 3 is a separate throwaway Canton proof; it does not turn the Mode 1 +browser preview into a live wallet dApp. + +If `template`, `choice`, `party`, `participant`, or `DAR` are still unfamiliar, +pause and return to Step 1, the +[Canton and Daml primer](concepts/canton-daml-primer.md). ## Prerequisites -| Tool | Version | For | -|---|---|---| -| DPM | latest ([install](https://docs.digitalasset.com/build/3.4/dpm/dpm.html)); resolves the pinned **SDK 3.5.2** automatically | building + testing the Daml core | -| Node.js | **24+** | backend + dApp | -| npm | 10+ | install/test | -| (optional) Docker | recent | only for the real-Canton paths below | -The Token Standard dependencies are the **canonical Splice 0.6.12 release -DARs**, committed under `vendor/splice/dars/` (the exact package ids the -network vets — see `vendor/splice/VENDOR_PIN.md`). No extra download or -source build is needed; `dpm build` consumes them directly. Refresh them for a -newer Splice release with `scripts/fetch-splice-dars.sh`. +### For the browser preview ---- +- [Node.js 24 or newer](https://nodejs.org/en/download). +- npm 10 or newer (installed with Node.js). +- [Git](https://git-scm.com/downloads/). +- `curl` is optional, but useful for checking the backend independently of the + browser. -## 1. Daml core — `trading/` +Check the installed versions: ```bash -bash scripts/run-local-daml-tests.sh +node --version # expected: v24.x.x or newer +npm --version # expected: 10.x.x or newer +git --version ``` -This builds the `canton-dex-trading` DAR (against the committed canonical -Token Standard DARs) and runs the suites. Or by hand: + +
+ +### Additional tools for Daml builds, tests, and the live proof + +- A JDK 17 or newer. CI uses + [Eclipse Temurin 17](https://adoptium.net/temurin/releases/?version=17). +- [DPM](https://archived.docs.digitalasset.com/build/3.5/dpm/manual-install.html), the + Daml Package Manager. +- The Daml SDK pinned by this repository: 3.5.2. +- Bash and `curl` for the default live-Canton proof. + +Digital Asset keeps the version-pinned 3.5 manuals in its official documentation +archive. The links above intentionally use that archive so their commands match +this repository's SDK instead of a newer toolchain. + +If Daml syntax itself is new, complete Digital Asset's official +[Get started with Daml](https://archived.docs.digitalasset.com/build/3.5/tutorials/get-started/index.html) +tutorial and its +[basic contracts lesson](https://archived.docs.digitalasset.com/build/3.5/tutorials/smart-contracts/contracts.html) +before the first code-change tutorial. The repository primer explains this +application's mental model; the official tutorial teaches the language. + +After installing Java and DPM, install the pinned SDK once: + +```bash +java -version +dpm --version +dpm install 3.5.2 +``` + +`dpm --version` reports the DPM version, not the Daml SDK version. The +`sdk-version: 3.5.2` entries in `trading/daml.yaml` and +`trading-tests/daml.yaml` select the installed SDK when those packages build. + +The Token Standard dependencies are committed DAR files under +`vendor/splice/dars/`; a first build does not need to download or compile +Splice source. Their release and package IDs are recorded in +[`../vendor/splice/VENDOR_PIN.md`](../vendor/splice/VENDOR_PIN.md). + +## Mode 1: run the browser preview + +The preview uses seeded TypeScript objects, not a Canton participant. Keep the +backend and frontend running in separate terminals: each development server is +a foreground process. + +### 1. Clone and install + +Run these one-time setup commands in any terminal: ```bash -(cd trading && dpm build) # produces canton-dex-trading-0.1.4.dar -(cd trading-tests && dpm test) # every script should report "ok" +git clone https://github.com/srikanth-bitdynamics/Canton-Dex-Reference-Implementation.git +cd Canton-Dex-Reference-Implementation + +(cd services/operator-backend && npm ci) +(cd app/web && npm ci && cp .env.example .env.local) ``` -This exercises the V2-native templates (pool/swap/LP, orders, RFQ, -matched-trade), the reference registry (`Registry/V2.daml`) implementing V2 -Holding/Allocation/Settlement, and the conservation/invariant tests. ---- +If you already cloned the repository, start from its root and run only the two +parenthesized install commands. -## 2. Operator backend — `services/operator-backend/` +### 2. Terminal 1 — start the backend + +From the repository root: -In-memory dev ledger, no Canton needed: ```bash cd services/operator-backend -npm ci -npm run dev # listens on http://localhost:8080 +ALLOWED_ORIGINS=http://localhost:5173 npm run dev ``` -On boot it seeds a demo BTC/USDC pair + pool and a demo trader with holdings. -Smoke it: + +`ALLOWED_ORIGINS` is required. The backend denies cross-origin browser access +when this allowlist is absent; the fact that `curl` works does not mean the +browser is allowed to read the same endpoint. + +Leave the process running. A successful start ends with lines like: + +```text +[operator-backend] dev server listening at http://127.0.0.1:8080 +[operator-backend] parties: operator=operator-demo, lpRegistrar=lp-registrar-demo, admin=admin-demo, trader=trader-demo +``` + +The backend seeds: + +- one active `BTC/USDC` pair and constant-product pool; +- two reserve slices per side; +- `0.2500000000 BTC` and `5000.0000000000 USDC` for `trader-demo`. + +### 3. Terminal 2 — start the dApp + +Open a second terminal at the repository root: + ```bash -curl -s http://localhost:8080/v1/pairs | python3 -m json.tool -curl -s http://localhost:8080/v1/pools | python3 -m json.tool +cd app/web +npm run dev +``` + +Vite prints a local URL, normally: + +```text +Local: http://localhost:5173/ ``` -> Port note: `localhost:8080` can collide with Docker’s IPv6 bind on macOS. If -> `/v1/pairs` returns "method not allowed", run on another port and point the -> dApp at it: `PORT=8091 npm run dev` and set `VITE_API_BASE=http://127.0.0.1:8091`. -### Exercising write paths in demo mode +Open . The Trade and Pools pages should show the seeded +`BTC/USDC` market. Connect **Mock Wallet (dev)** to view the seeded +`trader-demo` portfolio. The header must say `in-memory preview`, the status pill +must say `Preview · no Canton`, and the page warning must state that wallet +actions do not settle token value. Those labels are part of the safety boundary. -Read paths (`/v1/pairs`, `/v1/pools`, `/v1/holdings`, `/v1/swaps/quote`) work -with no configuration. **State-changing routes** — `/v1/pools/swap*`, -`/v1/rfq`, `/v1/orders/*`, `/v1/admin/*` — are auth-gated and return **401** -unless an operator token is configured or the dev bypass is on. To exercise -writes against the in-memory demo, set one flag: +### 4. Terminal 3 — verify the boundary + +Use a third terminal to distinguish a backend problem from a browser problem: ```bash -DEX_DEV_OPEN=1 npm run dev +curl -sS http://localhost:8080/v1/status +curl -sS http://localhost:8080/v1/pairs +curl -sS http://localhost:8080/v1/pools ``` -`DEX_DEV_OPEN=1` opens the operator-write gate **and** (because the dev server -seeds bare-hint parties like `trader-demo`) auto-relaxes party validation. It -does not emulate wallet signatures or fabricate V2 allocations: allocation- -backed writes return `501 not_supported` on the in-memory ledger. Use the local -Canton flow below to exercise a real swap, order, or liquidity settlement. +The status response contains the following stable fields; `slot` and +`serverTime` change on every run: -Demo-mode flags (in-memory dev server only; never set in production): +```json +{"network":"preview:in-memory","slot":0,"synced":true,"serverTime":""} +``` -| Env | Effect | -|---|---| -| `DEX_DEV_OPEN=1` | open the operator-write gate; also auto-allows the seeded bare parties | -| `DEX_ALLOW_BARE_PARTIES` | override the bare-party relaxation (`=0` to force strict `hint::hexfingerprint`) | -| `DEX_DEV_WALLET_RELAY=1` | enable the dev wallet-relay endpoint | -| `DEX_OPERATOR_API_TOKEN` | require this bearer token on writes instead of the open bypass | +The pair and pool responses are JSON arrays containing `BTC`, `USDC`, and +`BTC-USDC`. If those commands succeed but the dApp reports a network error, +check that Terminal 1 includes exactly the origin printed by Vite in +`ALLOWED_ORIGINS`. + + + +### What is safe to explore in this mode -> A swap is always `/v1/pools/swap/request` → wallet-authorized allocation → -> `/v1/pools/swap`. There is no synthetic single-step settlement path. +Use the preview to: + +- inspect seeded pairs, pool reserves, prices, holdings, and order-book views; +- request a swap quote and observe fee and price-impact changes; +- inspect the screens and the wallet handoff sequence; +- see which HTTP calls the dApp makes in the browser developer tools. + +Do not use it as evidence that a trade settled. The Mock Wallet waits briefly, +logs the intent, and returns fake contract IDs such as `#mock-…:0`. It has no +key and signs nothing. The backend's `InMemoryLedger` implements selected +TypeScript handlers and does not enforce Daml authorization or Token Standard +value conservation. + +Write routes are deliberately closed by default. Without an operator token or +the development bypass, a state-changing request returns: + +```json +{"error":"state-changing routes require DEX_OPERATOR_API_TOKEN to be configured (or DEX_DEV_OPEN=1 for the dev server)","code":"unauthorized"} +``` + +with HTTP status `401`. + +To inspect more of the UI's write orchestration, stop Terminal 1 with +`Ctrl+C` and restart it with the explicit development-only bypass: -Tests + typecheck: ```bash -npm run typecheck # tsc, clean -npm test # node:test +ALLOWED_ORIGINS=http://localhost:5173 DEX_DEV_OPEN=1 npm run dev +``` + +This bypass opens the non-admin operator-write gate and permits the seeded +short party names. Administrative `/v1/admin/*` routes still require +`OPERATOR_ADMIN_TOKEN`. The bypass does not create wallet signatures or V2 +allocations. Canonical swap, order-funding, and liquidity paths can reach an +unimplemented multi-step Daml choice and return HTTP `501` with: + +```json +{"error":"choice … is not implemented by the in-memory dev ledger. This flow requires a real Canton participant…","code":"not_supported","requestId":"…"} ``` ---- +That is an expected boundary of Mode 1, not a completed exchange flow. Never +set `DEX_DEV_OPEN=1` outside this local dev server. -## 3. dApp — `app/web/` +## Mode 2: run the Daml-engine proofs + +From the repository root: ```bash -cd app/web -cp .env.example .env.local # then edit (see Wallets below) -npm ci -npm run dev # Vite dev server → http://localhost:5173 +dpm install 3.5.2 +bash scripts/run-local-daml-tests.sh +``` + +The script first builds +`trading/.daml/dist/canton-dex-trading-0.1.4.dar`, then runs the +`trading-tests` package. A successful run includes: + +```text +==> Building canton-dex-trading (deps: vendor/splice/dars/*.dar) +canton-dex-trading built successfully. +… +testRealRegistryDvpSwapSettles: ok ``` -Open `http://localhost:5173` → the Trade / Pools / Orders / RFQ / Portfolio / -Admin pages render the seeded backend state. Connect **Mock Wallet (dev)** to -exercise the full trade/LP/order flows with deterministic cids and no external -wallet. -Tests: +At this revision, the package declares 111 Daml Script tests. Every displayed +test must end in `ok`, and the command must exit with status 0. +Workflow-specific mock-registry modules prove choreography without holdings; +real-holding suites prove value movement inside the Daml engine. The +[testing reference](reference/testing.md) explains that distinction, and the +[Daml proof map](reference/daml-proof-map.md) lists focused commands. + +To run only the real-holding swap proof after the DAR has been built: + ```bash -npm test # vitest +cd trading-tests +dpm test -p testRealRegistryDvpSwapSettles ``` -### Wallet options (set in `app/web/.env.local`) -| Provider | Enable | Notes | -|---|---|---| -| **Mock (dev)** | (always available in dev) | deterministic cids; best for local UI testing | -| **WalletConnect** | `VITE_WC_PROJECT_ID=` | web3-native path; get an id at cloud.reown.com | -| **CIP-0103 SDK** | `VITE_ENABLE_SDK=1` | `@canton-network/dapp-sdk`; needs a CIP-0103 wallet | -| **PartyLayer** | `VITE_ENABLE_PARTYLAYER=1` | `VITE_PARTYLAYER_WALLET_IDS=console,nightly,send[,loop]` | -| Token-standard relay | dev builds only | operator co-signs; labelled "dev only" — not for prod | +This mode runs the Daml engine in the Script test runner. It is materially +stronger than the TypeScript `InMemoryLedger`, but it is still not a running +Canton participant and does not exercise the browser or JSON Ledger API. -Backend API base is `VITE_API_BASE` (default `http://localhost:8080`). +Step 4, [Trace one AMM swap from formula to Daml settlement](tutorials/amm-first-walkthrough.md), +will unpack what that test proves after you complete the live checkpoint below. ---- + -## Scripts reference (`scripts/`) -| Script | What it does | -|---|---| -| `run-local-daml-tests.sh` | build the DAR + run the Daml test suites | -| `e2e-smoke.sh` | quick end-to-end smoke across the stack | -| `bootstrap-registry.ts` | create the asset-admin and LP-registrar `Registry.V2` contracts and register configured instruments | -| `localnet-dvp-e2e.ts` | LP add / swap / remove DvP round-trip on a LocalNet (`npm run localnet:dvp-e2e` from the backend) | -| `testnet-v2registry-trade.ts` | drive a V2 registry trade against a testnet participant | -| `fetch-splice-dars.sh` | refresh the committed TSv2 DARs from a Splice release | -| `build-trading-surface.sh` | build the `canton-dex-trading` surface | -| `deploy-testnet.sh` | upload the DAR + seed a pair/pool on a testnet participant | - ---- - -## Running the full test suite -| Component | Command | Expected | +## Mode 3: run the default live-Canton proof + +The default live path uses the Canton sandbox bundled with the pinned DPM SDK. +It requires no Canton DevKit, Docker, external wallet, or pre-existing network. +From the repository root run: + +```bash +bash scripts/run-dpm-sandbox-proof.sh +``` + +The script performs the integration work that Mode 2 deliberately skips: + +1. installs SDK 3.5.2 idempotently and builds the DEX DAR; +2. reserves all six Canton ports and starts a throwaway `dpm sandbox` on those + loopback ports; +3. waits for the JSON Ledger API to become ready; +4. creates one unrestricted user only inside this unauthenticated local + sandbox, then uses three parties: the bootstrap operator/admin/LP-registrar, + a distinct LP/trader, and a distinct swapper; +5. uploads the current trading DAR selected by `trading/daml.yaml`; that DAR + carries its Token Standard dependency closure; +6. creates real registry, holding, pool, slice, and LP-policy state; +7. executes add liquidity → quote-bound swap → half-LP removal through the + JSON Ledger API; +8. checks balances, exact reserves, reserve-slice reconciliation after every + phase, LP holding/supply/policy consistency, `x*y` nondecrease, + reserve-per-LP, and total value conservation; +9. stops Canton and removes the temporary state after a pass (logs are kept on + failure). + +The visible phases include: + +```text +==> Installing the pinned SDK and building the DEX +==> Starting throwaway Canton sandbox on reserved loopback ports +==> Uploading the package closure +==> Running the live-Canton DvP proof +==> PASS: portable live-Canton proof completed + The throwaway sandbox is now stopping; no persistent ledger state remains. +``` + +If a phase fails, the script exits non-zero and prints the preserved temporary +directory containing `canton.log` and `canton.stdout.log`. + +### What this live proof establishes + +This is the first local mode that starts Canton and submits through the real +JSON Ledger API. A pass establishes that the current package closure uploads +and that real V2 holdings move atomically across an add, a snapshot-bound swap, +and a partial LP redemption. The assertions cover both accounting state and +its backing slices, not merely successful command submission. + +The LP/trader and swapper are separate from the operator and from each other. +Operator, asset admin, and LP registrar deliberately share the bootstrap +party; one unrestricted sandbox-only user can act for all three parties. +Authentication is disabled and its bearer value is a non-secret placeholder. +The proof therefore does **not** establish: + +- production-grade separation of operator, admin, and registrar credentials; +- the operator HTTP server or React browser path; +- a CIP-0103, PartyLayer, WalletConnect, or other external wallet; +- a persistent Splice LocalNet or multi-participant topology; +- production identity, security, operations, governance, or compliance. + +Read [Local Canton from a clean clone](guides/localnet.md) for every phase and +failure mode. That guide also documents an **optional** persistent DevKit +LocalNet. `canton-devkit` is a separately distributed development helper; the +DEX code and DARs have no runtime dependency on it. If it is not already +available in your environment, use the DPM sandbox path. + +### From live proof to live browser integration + +A real browser settlement is a larger deployment. It additionally needs full +Canton party IDs and separated ledger rights, long-lived registry and market +state, backend package/contract configuration, credentials, explicit CORS, and +a compatible wallet that returns enough correlation data for settlement. +Continue with: + +- [Run on a testnet](guides/run-on-testnet.md) — participant-backed backend and + wallet configuration. +- [Deployment](guides/deployment.md) — backend/Docker environment and bootstrap + options. +- [Validator test plan](guides/validator-test-plan.md) — validate the configured + live system rather than assuming it works. +- [Testing reference](reference/testing.md) — the proof boundary of every test + layer and live driver. + +Do not call the browser path complete until a real trader party's pre-trade and +post-trade holdings differ by the expected amounts and the corresponding Canton +transaction is visible to the authorized parties. + +## Repository map for a first code read + +| Path | Read it for | Skip on the first pass | |---|---|---| -| Daml core | `bash scripts/run-local-daml-tests.sh` | every script reports `ok` | -| Backend | `cd services/operator-backend && npm run typecheck && npm test` | clean | -| dApp | `cd app/web && npm test` | clean | -| End-to-end (in-memory) | `bash scripts/e2e-smoke.sh` | green | - -## Optional: run against a real Canton ledger -The dev backend is in-memory. To run on real Canton: -- **LocalNet**: a self-contained Canton + Splice network on one host; build the - DAR, upload it + the V2 DARs, seed a pair/pool, point the backend at the - participant (`CANTON_LEDGER_URL`), and run `npm run start`. See - `docs/guides/deployment.md`. -- **Testnet**: `scripts/deploy-testnet.sh` uploads the DAR + seeds; record the - vetted package id + seed CIDs in `docs/guides/run-on-testnet.md`. - ---- +| `trading/CantonDex/Dex/` | DEX templates and choices: pair, pool, swap, liquidity, order, RFQ | registry internals | +| `trading/CantonDex/Registry/V2.daml` | Reference holdings, allocations, and batch settlement | detailed choice context until the workflow is clear | +| `trading-tests/CantonDex/Tests/` | Executable examples and invariants | boilerplate fixtures; start from the named tests in the AMM tutorial | +| `services/operator-backend/src/` | HTTP orchestration, matcher, ledger adapters | production recovery on the first pass | +| `app/web/src/` | Pages, wallet intents, and API calls | individual wallet-provider implementations | +| `vendor/splice/dars/` | Pinned binary Token Standard dependencies | do not try to learn Daml from binary DARs | + +The [Canton and Daml primer](concepts/canton-daml-primer.md) explains how these +layers meet. The [glossary](concepts/glossary.md) is the lookup page for names +encountered in code. + +## Component checks + +These checks are useful after the first preview. They are independent; none of +them turns Mode 1 into a real Canton settlement. + +| Component | Command from repository root | Success signal | Limitation | +|---|---|---|---| +| Daml | `bash scripts/run-local-daml-tests.sh` | every Daml test is `ok`; exit 0 | Script runner, not a participant | +| Live Canton | `bash scripts/run-dpm-sandbox-proof.sh` | ends with `==> PASS: portable live-Canton proof completed`; exit 0 | unrestricted throwaway user and direct JSON API driver; no browser, external wallet, or operator HTTP server | +| Backend | `cd services/operator-backend && npm run typecheck && npm test` | TypeScript exits cleanly; TAP ends with `# fail 0` | mocked/in-memory ledgers unless live tests are explicitly configured | +| dApp | `cd app/web && npm test && npm run build` | Vitest reports all tests passed; Vite writes `dist/` | mocked browser/API environment | +| HTTP smoke | `bash scripts/backend-http-smoke.sh` | ends with `==> All backend HTTP smoke checks passed` | selected reads, quote, and auth gate only; no browser, wallet, or settlement | + +Run `npm ci` in the backend and dApp directories before their component checks. +The HTTP smoke script expects backend dependencies to be installed already; +the DPM sandbox proof installs them itself when its `tsx` runner is absent. ## Troubleshooting -| Symptom | Fix | + +| Symptom | Meaning and fix | |---|---| -| backend `/v1/*` → "method not allowed" | Docker owns `:8080`; use `PORT=8091 npm run dev` + `VITE_API_BASE=http://127.0.0.1:8091` | -| dApp can’t reach backend (CORS) | start backend with `ALLOWED_ORIGINS=http://localhost:5173` | -| dev relay wallet needs a party | set `VITE_CANTON_DEFAULT_PARTY=trader-demo` (dev only) | -| `dpm: command not found` | install DPM (see prerequisites link) and re-open the shell | -| stale `node_modules` after branch switch | `rm -rf node_modules && npm ci` | - -See also: [Overview](concepts/overview.md), [Architecture](concepts/architecture.md), -[Workflows](concepts/workflows.md), the [Builder Guide](guides/builder-guide.md) -workflow tour, [Operator Runbook](guides/operator-runbook.md), and the full -[documentation index](README.md). +| Browser says network/CORS error, but `curl` works | Restart the backend with `ALLOWED_ORIGINS=http://localhost:5173`; use the exact origin Vite printed. | +| A write returns `401` | Expected in the default preview. Use `DEX_DEV_OPEN=1` only if you intentionally want the local write-orchestration preview. | +| A flow returns `501 not_supported` | Expected when it needs an allocation-backed Daml choice absent from the TypeScript in-memory ledger. Use Mode 2 to prove the contract or Mode 3 for a real-ledger proof. | +| A result contains `#mock-…:0` | It came from Mock Wallet; it is not a Canton contract ID and proves no submission occurred. | +| `/v1/*` says “method not allowed” on port 8080 | Another process, often Docker, owns the port. Start the backend with `PORT=8091 ALLOWED_ORIGINS=http://localhost:5173 npm run dev`, then set `VITE_API_BASE=http://127.0.0.1:8091` in `app/web/.env.local` and restart Vite. | +| `dpm: command not found` | Install DPM using the prerequisites link, then open a new shell. | +| DPM cannot find SDK 3.5.2 | Run `dpm install 3.5.2`, then retry from a directory containing the relevant `daml.yaml`. | +| The DPM sandbox proof fails | Use the preserved log directory printed by the script; check Java 17, local memory, and port-binding errors. | +| Native npm dependency fails to install | Confirm Node 24 is active, remove only that component's `node_modules`, and rerun `npm ci` in the same component. | + +**Next canonical step:** [AMM-first walkthrough](tutorials/amm-first-walkthrough.md). +Use the [testing reference](reference/testing.md) when you need the complete +proof matrix, or return to [all documentation](README.md). diff --git a/docs/guides/add-a-trading-pair.md b/docs/guides/add-a-trading-pair.md index f3ce958c..0dcd9840 100644 --- a/docs/guides/add-a-trading-pair.md +++ b/docs/guides/add-a-trading-pair.md @@ -51,6 +51,7 @@ Operator-signed, submitted by the operator backend: ```bash curl -X POST http://localhost:8080/v1/admin/pairs \ + -H "Authorization: Bearer $OPERATOR_ADMIN_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "baseInstrumentId": "ETH", @@ -90,6 +91,7 @@ needs: ```bash curl -X POST http://localhost:8080/v1/admin/pools \ + -H "Authorization: Bearer $OPERATOR_ADMIN_TOKEN" \ -H 'Content-Type: application/json' \ -d '{ "baseInstrumentId": "ETH", @@ -132,9 +134,11 @@ add-liquidity DvP used for every later deposit: ## Step 4 — Surface and verify -The dApp's `/v1/pairs` returns the new pair on the next backend tick; the Pools page -shows the pool once it is seeded. For the pair to appear on the trader's Trade page, -`active` must be `true` and `tradingMode` must be `TM_OrderBook` or `TM_Both`. +The dApp's `/v1/pairs` returns the new listing and the Pools page shows the pool +once it is seeded. The current Trade page is pool-driven: it reads active pools +and does not filter them through `DexPair.active` or `tradingMode`. Treat those +fields as discovery metadata unless your application adds an off-ledger filter +or an on-ledger terminal-choice gate. ```bash curl -s http://localhost:8080/v1/pairs | jq '.[] | select(.baseInstrumentId=="ETH")' diff --git a/docs/guides/builder-guide.md b/docs/guides/builder-guide.md index 4180e085..2dc9505e 100644 --- a/docs/guides/builder-guide.md +++ b/docs/guides/builder-guide.md @@ -1,9 +1,11 @@ # Builder guide -How to read and extend this reference. Start after -[Getting Started](../getting-started.md) (which runs the stack) and the -[Overview](../concepts/overview.md) and [Architecture](../concepts/architecture.md) -(which explain the design). +This is Step 9, the final step in the +[canonical newcomer learning path](../README.md#canonical-newcomer-learning-path). +Complete [Make your first AMM code change](../tutorials/make-your-first-amm-change.md) +first. This guide helps you plan a behavior-changing extension without +crossing the DEX, Token Standard, registry, backend, or wallet boundaries by +accident. ## Three layers, one boundary @@ -57,23 +59,32 @@ oracle integration, custody, and a compliance/KYC layer. Those belong in forks o deployment-specific services, not the shared templates. See [Non-goals](../concepts/non-goals.md). +## Before extending the AMM + +Do not start a second learning route here. Follow the canonical path through +the tested first-change tutorial, then use the +[Daml proof map — AMM pool](../reference/daml-proof-map.md#amm-pool) to locate +the exact source choice and smallest proof for the behavior you plan to alter. +The [allocation surface](../reference/allocation-surface.md) is the lookup page +for the Token Standard contracts beneath those choices. + ## The four workflow families -The Daml test suite exercises four families. Reading them in order is the fastest -way to understand the venue; each lists its contracts, its entry choice, and the -test that proves it. +The Daml test suite exercises four families. Treat the sections below as a +builder's lookup map; the newcomer curriculum remains the canonical path in the +documentation index. ### A. Pair and instrument listing Register a tradable pair, and for pool mode its instruments. - `Dex/DexPair.daml` — the listing: base + quote instrument ids, fee model, trading - mode (`OrderBook` / `Pool` / `Both`), and an `active` flag. + mode (`OrderBook` / `Pool` / `Both`), and an `active` flag. The mode and flag + guide off-ledger discovery/routing; they are not fetched by `PoolRules` or + `OrderMatchExecution` and therefore are not on-ledger settlement gates. - `Registry/V2.daml` — the reference registry's V2 interfaces plus its registry-specific `InstrumentConfig` (precision, supply bookkeeping, placeholder requirement records, optional ISIN/CUSIP). -- Proven by - [`RegistryConservationTests.daml`](../../trading-tests/CantonDex/Tests/RegistryConservationTests.daml) - and [`PoolLiquidityRulesTests.daml`](../../trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml). +- Source and focused checks: [Daml proof map — Pair listing metadata](../reference/daml-proof-map.md#pair-listing-metadata). ### B. OTC and RFQ settlement A bilateral block trade settles as one atomic batch. @@ -84,9 +95,7 @@ A bilateral block trade settles as one atomic batch. - `Dex/Rfq.daml` + `PolicyReceipt.daml` — trader RFQ, dealer quotes, then a joint `Rfq_Accept` that emits a `MatchedTrade` carrying an operator-signed `PolicyReceipt` in `SettlementInfo.meta`. -- Proven by - [`EndToEndTests.daml`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) - (`testMatchedTradeFullSettle`, `testRfqAcceptProducesMatchedTradeWithReceipt`). +- Source and focused checks: [Daml proof map — RFQ and OTC](../reference/daml-proof-map.md#rfq-and-otc). ### C. Resting orders backed by a V2 allocation A limit order rests in the book, funded by the trader's own locked allocation. @@ -99,9 +108,7 @@ A limit order rests in the book, funded by the trader's own locked allocation. uncommitted, allowing the trader to withdraw through the standard allocation interface if the venue is unavailable; a later match then fails safely. - `Dex/OrderMatchExecution.daml` — the atomic match (see the matcher section below). -- Proven by - [`EndToEndTests.daml`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) - (`testOrderFundingFlow`, `testOrderRemainderFundingArithmetic`). +- Source and focused checks: [Daml proof map — Resting orders](../reference/daml-proof-map.md#resting-orders). ### D. Constant-product pool An AMM whose reserves are committed allocations. @@ -116,10 +123,7 @@ An AMM whose reserves are committed allocations. pair), co-signed by `operator` and `lpRegistrar`. - `Lp/Policy.daml` + `Lp/Instrument.daml` — the LP token, owned by `lpRegistrar`, keyed by a `V2.InstrumentId`, and unaware of pools or orders. -- Proven by - [`EndToEndTests.daml`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) - (`testPoolFullLifecycle`, `testPoolSwapEndToEnd`) and - [`PoolLiquidityRulesTests.daml`](../../trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml). +- Source and focused checks: [Daml proof map — AMM pool](../reference/daml-proof-map.md#amm-pool). ## The off-ledger matcher: where a fork does most of its work @@ -159,8 +163,9 @@ go through the connected wallet over the CIP-0103 dApp standard specifications, the dApp composes the command, and the wallet signs and submits. `Rfq_Accept` is jointly controlled by trader and operator; deployments must provide both authorities through wallet/delegation or an explicitly enabled -co-submission path. The included RFQ page uses the last option for hosted demo -parties; it is not part of the wallet-intent surface. +co-submission path. The included RFQ page demonstrates the last option with +configured parties; it is not part of the wallet-intent surface or a public +relay service. Read endpoints (`/v1/pools`, `/v1/trades`, …) are operator-observed and served from the backend's indexer cache. Keep self-custodial allocation writes on the wallet @@ -180,9 +185,9 @@ named allocation in one transaction. Deploy that DAR alongside the DEX DAR. |---|---| | Add a trading pair (BTC/EUR, ETH/USDT, …) | Create a `DexPair`; add a `Pool` for pool mode. See [Add a trading pair](add-a-trading-pair.md). | | Issue a new LP token or lifecycle-rich instrument (vested, dividend-bearing) | See [Add an LP or instrument](add-lp-or-instrument.md). | -| Use a different registry | Swap `CantonDex.Testing.MockRegistry` for the real registry's `AllocationFactory` + `SettlementFactory`. See [Registry integration](registry-integration.md). | +| Use a different registry | Keep the DEX services behind `registry-client`, then configure discovery, choice context, disclosures, and metadata for the target registry. `CantonDex.Testing.MockRegistry` appears only in Daml test fixtures and is not the deployed backend. See [Registry integration](registry-integration.md). | | Add a pricing curve (StableSwap, weighted) | Add curve-specific configuration and rules, then reuse the V2 allocation and settlement pattern. No generic curve interface is defined by this package. | -| Add a fee policy | Extend `Pool.feeBps` / `DexPair.feeModel` and the `constantProductOut` quote math. | +| Change the executable pool fee | Update `Pool.feeBps` and the `constantProductOut` quote math. Mirror the value into `DexPair.feeModel` only where off-ledger listing consumers need it; that record does not gate or price `PoolRules_Swap`. | | Add an RFQ policy (oracle-weighted, multi-tier) | `Rfq.policyCmp` defines the ordering used by `applyPolicyPairs`; bump `policyVersion`/`policyHash` and mirror it in `app/web/src/services/rfq-policy.ts`. | | Point at a different participant | Set `CANTON_LEDGER_URL`, `CANTON_LEDGER_TOKEN`, `CANTON_SYNCHRONIZER`. See [Run on a testnet](run-on-testnet.md). | @@ -192,21 +197,14 @@ contracts own asset semantics. ## Your first change -A concrete loop for extending a choice — say, adding an optional referral party -to a swap: - -1. **Edit the Daml.** Append an `Optional` field (e.g. `referral : Optional - Party`) to `Pool`, or add a clearly named referral choice to `PoolRules` — see - [Upgrade discipline](#upgrade-discipline) for why additions go at the end of - the record. -2. **Build the DAR:** `(cd trading && dpm build)`. -3. **Run the tests:** `(cd trading-tests && dpm test)`. The suite includes - `EndToEndTests.daml::testPoolSwapEndToEnd`, which exercises the full swap path - your change touches. -4. **Verify the boundary:** run `bash scripts/run-local-daml-tests.sh`, then - exercise the affected HTTP and wallet path. A separate package can consume - the DAR as a data dependency, but this repository does not claim a generic - pool interface that makes curve implementations interchangeable. +Use [Make your first AMM code change](../tutorials/make-your-first-amm-change.md) +for the complete red/green loop: exact edits, focused test, layer-impact check, +full local suite, and live sandbox proof. + +For later changes, distinguish a new choice from a new template field. A new +choice can leave existing contract construction sites intact. A new field +changes the serialized template shape and every construction site must supply +it; follow [Upgrade discipline](#upgrade-discipline) before making that edit. ## Upgrade discipline @@ -220,14 +218,20 @@ fresh lineage. ## Testing ```bash -cd trading-tests && dpm test # in-script Daml suites +cd trading-tests +dpm test # every in-script Daml suite +dpm test -p testDexPairLifecycleUpdates # one named design proof +dpm test --files CantonDex/Tests/LifecycleChoiceTests.daml ``` -The commands and expected outcomes are in [Getting Started](../getting-started.md). +Use `-p ` while reading one workflow, then run the whole suite before +handoff. Exact source/test links and focused commands are in the +[Daml proof map](../reference/daml-proof-map.md); broader commands and expected +outcomes are in [Getting Started](../getting-started.md). Testnet smoke test: ```bash -node --import tsx scripts/testnet-v2registry-trade.ts # real V2-standard trade +npm --prefix services/operator-backend run live:matched-trade # real V2-standard trade ``` Keep deployment-specific responsibilities outside the reference core — custody, @@ -278,6 +282,9 @@ app/web/ ## Where to read next -- **Reference:** [HTTP API](../reference/http-api.md) · [Allocation surface](../reference/allocation-surface.md) -- **Deeper design:** [Workflows](../concepts/workflows.md) · [Liquidity and custody](../concepts/liquidity-and-custody.md) +You have completed the canonical newcomer path. Choose the task that matches +your extension: + +- **Reference:** [HTTP API](../reference/http-api.md) · [Allocation surface](../reference/allocation-surface.md) · [Daml proof map](../reference/daml-proof-map.md) +- **Deeper design:** [Liquidity and custody](../concepts/liquidity-and-custody.md) · [Pricing](../concepts/pricing.md) · [Non-goals](../concepts/non-goals.md) - **Recipes:** [Add a trading pair](add-a-trading-pair.md) · [Add an LP or instrument](add-lp-or-instrument.md) diff --git a/docs/guides/choice-context.md b/docs/guides/choice-context.md index 645d866e..869fa9e4 100644 --- a/docs/guides/choice-context.md +++ b/docs/guides/choice-context.md @@ -1,245 +1,346 @@ -# Choice context and disclosure retrieval - -The operator submits every transaction under its own party. But the holdings a -settlement archives are signed `signatory admin, owner` — a registry admin the -operator never sees — and the Token Standard V2 factory choices take a context -argument the operator cannot compute for itself. So each registry-touching -submission carries two riders sourced from the asset registry: a **choice -context** threaded into the choice's `extraArgs.context`, and a set of -**disclosed contracts** threaded into the ledger submission's -`disclosedContracts`. One module — the operator backend's -[`registry-client`](../../services/registry-client/src/index.ts) — is the single -place both come from, so cache invalidation stays correct. - -This is the reference registry-client integration contract, not a Token Standard -V2 endpoint specification. It mirrors the Registry Utility guide's "Note: Before -the command is submitted by the UI, an API call is being made (in the -background) to an endpoint to retrieve required additional choice context -(including disclosure)..." pattern. - -## The two riders - -| Rider | Threaded into | Why the operator needs it | -|---|---|---| -| **Choice context** (`context.values`) | `choiceArgument.extraArgs.context` | The registry computes it (disclosed config, featured-app rights, rate limits). Self-registries return it empty, but the choice's `ExtraArgs` shape still requires the field. | -| **Disclosed contracts** | submission `disclosedContracts` | The factory contracts, registry config, and admin-signed holdings the choice fetches are invisible to the operator's party. Disclosure hands the participant the created-event blobs it needs to validate them without `readAs`. | +# Choice context and registry discovery + +This guide explains how the DEX discovers a Token Standard V2 factory, obtains +the context required for one specific choice, and supplies disclosed contracts +to Canton. Read [Registry integration](registry-integration.md) first if the +registry boundary is new to you. + +The important rule is: + +> A registry lookup belongs to one concrete operation. Send that operation's +> choice arguments, use the returned factory and context for that operation, +> and do not reuse the response for a later choice. + +The repository follows the operation-specific V2 OpenAPI committed under +[`vendor/splice/token-standard`](../../vendor/splice/token-standard/). It does +not invent admin-wide generic factory or context endpoints. + +## 1. The problem in one picture + +The operator knows what it wants to settle, but it does not own the asset +registry. The registry may require configuration, permissions, or credential +contracts that the operator cannot see. ```mermaid -flowchart LR - subgraph reg["Asset registry — off-ledger HTTP"] - E1["/registry/factories/:admin"] - E2["/registry/choice-context/:admin"] - end - subgraph rc["registry-client — TTL caches"] - F["getFactories
→ { factoryCid, disclosure }"] - C["getChoiceContext
→ { context, disclosure }"] - end - A["operator submission:
extraArgs.context +
[...factories.disclosure, ...ctx.disclosure]"] - L["JSON Ledger API
extraArgs + disclosedContracts"] - X["on-ledger factory choice
Allocate / SettleBatch"] - E1 --> F --> A - E2 --> C --> A - A --> L --> X +sequenceDiagram + participant App as dApp or operator + participant Daml as Daml preview choice + participant Registry as Registry V2 HTTP API + participant Canton as Canton participant + + App->>Daml: Build the exact candidate choice argument + Daml-->>App: SettlementFactory_SettleBatch argument + App->>Registry: POST { choiceArguments } + Registry-->>App: factoryId + choiceContext + disclosedContracts + App->>Canton: Exercise with factory/context + disclosures + Canton->>Canton: Revalidate current contracts and settle atomically ``` -## Where the riders are assembled +Allocation creation is slightly different: the dApp already has the allocation +specification, selected holding CIDs, timestamp, and actors, so it constructs +the candidate `AllocationFactory_Allocate` argument directly. Settlement flows +use a Daml preview because Daml, not TypeScript, owns the authoritative batch. -One helper turns the registry's `ChoiceContextRef` into the `extraArgs` shape the -choices take — [`fetchChoiceContext`](../../services/operator-backend/src/ledger/choice-context.ts), -shared by the pool, order, and matched-trade services: +## 2. Three values that must stay together + +One factory lookup returns a normalized +[`FactoryChoiceContextRef`](../../services/registry-client/src/types.ts): ```typescript -export async function fetchChoiceContext( - registry: RegistryClient, - admin: Party, -): Promise { - const ctx = await registry.getChoiceContext(admin); +{ + factoryCid, + context: { values: { /* registry-defined */ } }, + disclosure: [ /* created-event blobs */ ] +} +``` + +| Value | Where it goes | Why it is needed | +|---|---|---| +| `factoryCid` | The Daml factory choice | Selects the registry contract that implements allocate or settle. | +| `context.values` | `choiceArgument.extraArgs.context` | Carries registry-defined data for this operation. | +| `disclosure` | The Ledger API submission's `disclosedContracts` | Makes otherwise invisible contracts available for transaction validation. | + +The small +[`asChoiceContext`](../../services/operator-backend/src/ledger/choice-context.ts) +helper only converts the normalized response into Daml's `ExtraArgs` shape: + +```typescript +export function asChoiceContext(ctx: ChoiceContextRef) { return { - extraArgs: { context: ctx.context, meta: { values: {} } }, + extraArgs: { + context: ctx.context, + meta: { values: {} }, + }, disclosure: ctx.disclosure, }; } ``` -At each submit site, the factory disclosure and the choice-context disclosure are -merged into one array and the context is passed through as `extraArgs`. From the -pool swap ([`pool/index.ts`](../../services/operator-backend/src/pool/index.ts), -`PoolRules_Swap`): +Discovery remains at each call site. That makes it difficult to accidentally +ask for context without the operation's exact arguments. + +## 3. Canonical V2 HTTP endpoints + +The client is +[`services/registry-client/src/index.ts`](../../services/registry-client/src/index.ts). +Its source OpenAPI files are +[`allocation-instruction-v2.yaml`](../../vendor/splice/token-standard/splice-api-token-allocation-instruction-v2/openapi/allocation-instruction-v2.yaml) +and +[`allocation-v2.yaml`](../../vendor/splice/token-standard/splice-api-token-allocation-v2/openapi/allocation-v2.yaml). + +| Operation | Method and path | Request body | +|---|---|---| +| Find an allocation factory | `POST /registry/allocation-instruction/v2/allocation-factory` | `{ "choiceArguments": }` | +| Find a settlement factory | `POST /registry/allocation/v2/settlement-factory` | `{ "choiceArguments": }` | +| Cancel one allocation | `POST /registry/allocations/v2/{allocationId}/choice-contexts/cancel` | `{ "meta": { ... } }` | +| Withdraw one allocation | `POST /registry/allocations/v2/{allocationId}/choice-contexts/withdraw` | `{ "meta": { ... } }` | + +The factory endpoints return the upstream wire shape: + +```json +{ + "factoryId": "#factory-cid", + "choiceContext": { + "choiceContextData": { "values": {} }, + "disclosedContracts": [] + } +} +``` + +The registry client validates this untrusted response and normalizes +`factoryId`, `choiceContextData`, and `disclosedContracts`. A bare TypeScript +cast is not used. + +### Why responses are not cached + +Choice context may depend on the exact allocation, holdings, actors, deadline, +or current registry state. Two calls with the same admin are not evidence that +the second operation can reuse the first response. The HTTP client performs a +fresh lookup for every operation. + +There is also no 404-to-empty fallback. A missing canonical endpoint is an +integration error; silently inserting empty context could turn a registry +policy failure into a confusing ledger rejection. + +## 4. Allocation creation: dApp to registry to wallet + +For a swap, order, or liquidity request, the operator first returns settlement +terms and an allocation specification. The wallet chooses the holdings it will +lock. The dApp then builds the candidate allocation choice: ```typescript -const factories = await this.registry.getFactories(pool.admin); -const ctx = await this.choiceContext(pool.admin); -// ... -this.ledger.submit({ - actAs: [this.operatorParty], - readAs: input.swapperAccount.owner ? [input.swapperAccount.owner] : [], - disclosure: [...factories.disclosure, ...ctx.disclosure], - command: { - kind: "exercise", - choice: "PoolRules_Swap", - argument: { /* ... */ extraArgs: ctx.extraArgs }, - }, +const choiceArguments = { + settlement, + allocation, + requestedAt, + inputHoldingCids, + actors, + extraArgs: EMPTY_EXTRA_ARGS, +}; + +const surface = await operator.getAllocationFactory({ + admin: allocation.admin, + choiceArguments, }); ``` -The submitter's last step drops that `disclosure` verbatim into the JSON Ledger -API command ([`ledger/json-api.ts`](../../services/operator-backend/src/ledger/json-api.ts)): +The dApp calls the backend proxy +`POST /v1/registry/allocation-factory`. The proxy passes the same +`choiceArguments` to `RegistryDiscovery.getAllocationFactory`; it does not +reconstruct or simplify them. The returned context replaces the empty +placeholder when the wallet authors the actual +`AllocationFactory_Allocate` command. + +```mermaid +flowchart LR + R["Operator returns
settlement + allocation spec"] + W["Wallet selects
input holdings"] + A["dApp builds complete
Allocate candidate"] + P["DEX backend proxy"] + G["Registry allocation-factory
endpoint"] + S["Wallet signs and submits
Allocate"] + R --> W --> A --> P --> G --> P --> S +``` + +The trader, not the operator, authorizes the wallet submission. The backend +proxy discovers data; it does not grant trader authority. + +**Code:** [`app/web/src/services/ledger.ts`](../../app/web/src/services/ledger.ts) +and +[`services/operator-backend/src/http/index.ts`](../../services/operator-backend/src/http/index.ts). + +## 5. Settlement: preview, discover, execute + +Settlement arguments contain exact transfer legs and allocation CIDs. Building +them independently in TypeScript would duplicate security-sensitive Daml +logic. Each supported settlement flow obtains the candidate +`SettlementFactory_SettleBatch` argument from Daml before querying the registry. + +### Pool swap + +1. `PoolRules_PreviewSwapSettlement` reads the current pool and returns the + candidate settlement batch. +2. The backend calls `getSettlementFactory(pool.admin, previewResult)`. +3. `PoolRules_Swap` receives that factory, its context, and disclosures. +4. The real choice re-reads current state and enforces quote binding, + constant-product calculation, allocation binding, and minimum output. + +**Code:** [`PoolRules.daml`](../../trading/CantonDex/Dex/PoolRules.daml) and +[`pool/index.ts`](../../services/operator-backend/src/pool/index.ts). + +### Matched trade + +`MatchedTrade_PreviewSettlement` returns one exact batch argument per registry +admin. The backend performs one settlement-factory lookup per admin, keeps each +context with its own batch, merges disclosures by contract ID, and exercises +`MatchedTrade_Settle`. + +**Code:** [`MatchedTrade.daml`](../../trading/CantonDex/Dex/MatchedTrade.daml) +and +[`matched-trade/index.ts`](../../services/operator-backend/src/matched-trade/index.ts). + +### Order match + +The backend create-and-exercises an ephemeral +`OrderMatchExecution_PreviewSettlement` wrapper. That value-free transaction +leaves no active wrapper contract. It then performs registry discovery and +create-and-exercises a fresh `OrderMatchExecution_Execute` wrapper. + +The execute choice does not trust the earlier preview: it revalidates the live +orders and allocations, settles both funding allocations, rolls forward any +remainders, and records the trade in one value-moving transaction. + +**Code:** +[`OrderMatchExecution.daml`](../../trading/CantonDex/Dex/OrderMatchExecution.daml) +and [`order/index.ts`](../../services/operator-backend/src/order/index.ts). + +## 6. Cancellation and withdrawal are allocation-specific + +Cancel and withdraw context is queried with an allocation ID: ```typescript -disclosedContracts: req.disclosure ?? [], +const context = await registry.getAllocationCancelContext( + admin, + allocationCid, +); +``` + +A matched trade with three allocations performs three lookups, even if two +allocations have the same admin. The resulting `ExtraArgs` values remain paired +with their allocation CIDs. Treating context as one cached value per admin +would lose that binding. + +The order cancellation path performs the same lookup for its funding +allocation. When a pending order has no allocation, no registry allocation is +being cancelled, so empty `ExtraArgs` is sufficient for the app choice. + +## 7. Atomic add/remove liquidity boundary + +This is the one workflow where the standard HTTP preflight cannot be performed +with exact arguments in the current design. + +`PoolLiquidityRules_SettleAddLiquidity` and +`PoolLiquidityRules_SettleRemoveLiquidity` create operator/registrar +allocations and immediately settle them inside the same Daml transaction. Their +contract IDs do not exist before that transaction. The standard settlement +factory endpoint expects the candidate `SettleBatch` argument, including those +allocation IDs. + +```mermaid +flowchart TD + Q["HTTP preflight needs
future allocation CIDs"] + T["Atomic Daml transaction
creates those CIDs"] + Q -. "CIDs do not exist yet" .-> T + T --> C["Create temporary allocations"] + C --> S["Settle them immediately"] ``` -Each disclosed contract carries a base64 `createdEventBlob` — Canton's -disclosed-contract field — threaded through unchanged; the operator never -inspects or rewrites it. - -For a **cross-registry** trade, the merge is per admin: the operator groups legs -by their instrument's admin, fetches each admin's factories and context -separately, and concatenates the disclosures needed by the single transaction. -Context selection is keyed by admin, never by list position, so every batch -receives its own registry context. Disclosed contracts are transaction-wide, -deduplicated by contract id, and have no positional settlement meaning (see -[`matched-trade/index.ts`](../../services/operator-backend/src/matched-trade/index.ts), -`MatchedTrade_Settle`). On-ledger the context rides all the way down: the -registry's `SettlementFactory_SettleBatch` forwards `arg.extraArgs` into each -`Allocation_Settle` it exercises (see -[`Registry/V2.daml`](../../trading/CantonDex/Registry/V2.daml), -`settlementFactory_settleBatchImpl`). - -**Proven by:** -[`registry-client.test.ts`](../../services/operator-backend/test/registry-client.test.ts) -— `getChoiceContext` fetches, caches (one HTTP call for two reads), and falls -back to empty context + no disclosure on a 404; -[`matched-trade.test.ts`](../../services/operator-backend/test/matched-trade.test.ts) -— a two-admin settle threads each admin's `extraArgs.context` into its own -`SettlementBatchV2`, includes every required disclosure exactly once, and does -not assign meaning to disclosure-array order; and -[`pool.test.ts`](../../services/operator-backend/test/pool.test.ts) — split-admin -add and remove map distinct pool-admin and LP-registrar contexts to the matching -choice fields while deduplicating their shared disclosure. - -## Endpoints the operator queries - -Per registry, the operator backend fetches: - -| Example lookup | Returns | Used by | -|--------------------------------------------|-------------------------------------------|---------| -| `GET /registry/factories/:admin` | `(AllocationFactory, SettlementFactory)` CIDs + disclosure | `PoolRules_Swap`, matched-trade settle | -| `GET /registry/choice-context/:admin` | `ChoiceContextRef` (`context` + disclosure) | Pool, MatchedTrade, any registry-touching token-standard choice | - -These endpoints are examples for this reference implementation. A production -registry may use different paths, payloads, or discovery mechanisms as long as -the operator backend can produce the disclosed contracts and choice context -required by the registry's Token Standard V2 choices. The operator-backend's -`registry-client` module is the single integration point. - -## Disclosure retrieval and caching - -The `registry-client` owns two caches: - -1. Allocation/Settlement factory CIDs (plus disclosure) per admin. Stale on admin - re-publish, when the registry archives + recreates. -2. Choice-context refs per admin, honouring `choiceContextTtlMs` when configured. - -The factory cache holds entries until it is flushed. The client exposes -`invalidateAll()` for a full flush after a known factory archive or re-publish. -There is no registry-side event stream driving invalidation, so an integration -must explicitly flush the cache when its registry republishes factories. - -Registry responses are never trusted via a bare cast: `fetchJson` runs each -payload through a shape validator and raises `RegistryError("malformed", ...)` on -a mismatch (see [`registry-client/src/validate.ts`](../../services/registry-client/src/validate.ts)). - -## Failure modes the backend must handle - -| Failure | Recovery | +The repository handles this limitation explicitly: + +- `FixedRegistryClient` supports the configured reference self-registry. Its + factory CIDs are deployed with the operator, its context is empty, and its + required disclosures are known before the transaction. +- The generic HTTP `RegistryClient` throws + `RegistryError("unsupported", ...)` before an add/remove settlement is + submitted. It does not send placeholder CIDs and does not pretend a 404 means + empty context. +- A context-requiring external registry needs a workflow redesign for atomic + liquidity settlement. One option is a recoverable prepare-then-settle + protocol with explicit expiry, cancellation, idempotency, and cleanup. An + interactive transaction-authoring design is another possibility if the + selected Canton/wallet stack can supply registry data at the correct stage. + Either approach changes the protocol and must be threat-modelled; it is not a + configuration switch in this reference. + +This limitation applies to the backend's **atomic add/remove settlement +integration**, not to allocation discovery, swaps, matched trades, order +matches, or allocation cancellation. + +The Daml tests against a context-requiring registry prove that the Daml choices +thread context correctly when it is supplied. They do not manufacture a way +for an HTTP client to know future contract IDs. + +## 8. Disclosure handling + +The backend passes normalized disclosure to the JSON Ledger API as +`disclosedContracts`. When a transaction has several registry operations, +[`mergeDisclosures`](../../services/operator-backend/src/ledger/disclosure.ts) +deduplicates identical entries by contract ID. It rejects two different +payloads claiming the same contract ID. + +Disclosure is transaction-wide. Its array position has no relationship to a +settlement batch; batch-to-context association stays in the choice argument. + +## 9. Failure behavior + +The client raises a typed `RegistryError` and fails closed: + +| Kind | Meaning | Expected response | +|---|---|---| +| `not-found` | A canonical endpoint returned 404 | Fix registry routing/deployment; do not submit empty context. | +| `auth` | Registry returned 401 or 403 | Refresh or correct registry credentials. | +| `transport` | Other non-success HTTP response | Retry only according to operator policy; the error is marked retryable. | +| `malformed` | JSON or response shape is invalid | Treat the registry response as untrusted and stop. | +| `factory-stale` | A fixed registry has no mapping for the admin | Correct the deployment's per-admin factory map. | +| `unsupported` | Standards-correct discovery is impossible for this workflow | Redesign or use the documented self-registry path; never substitute placeholders. | + +## 10. Executable proofs + +| Question | Proof | |---|---| -| Factory CID stale | Refetch from `factories/:admin`; backoff on repeated failures | -| Choice-context disclosure stale | Flush the registry client, refetch, and retry once | -| Settlement batch rejected by factory | Cancel the trade, surface to operator monitoring | - -The `registry-client` module raises a typed `RegistryError` — with a `kind` -(`factory-stale`, `auth`, `transport`, or `malformed`) and a -`retryable` flag — so the calling code path can recover correctly. - -## Reference: choice-context-bearing arguments - -Each registry-touching choice the DEX exercises has a context shape the operator -must satisfy. Listed here as `(choice, required context)` pairs. - -### Allocation creation - -`V2.AllocationFactory.AllocationFactory_Allocate` - -Required inputs: -- `actors : [Party]` — the trader (for prefunded order or trade - allocation) or operator (for committed pool-fund allocation). -- `allocation : V2.AllocationSpecification` — with `admin` set - correctly; `nextIterationFunding` for prefunded shapes; `committed = - True` for pool-fund shapes. -- `requestedAt : Time` — current ledger time (operator passes through - from the request). -- `inputHoldingCids : [ContractId V2.Holding]` — chosen by the - trader's wallet from their ACS to cover the funding amount. -- `extraArgs.context` — registry-specific context (typically empty for - test registries; production may carry credential proofs or rate - limits). - -### Allocation request acceptance - -`V2.AllocationRequest_Accept` (on `TradeAllocationRequest` or -`OrderAllocationRequest`) - -Required inputs: -- `actors : [Party]` — typically `[trader]`. Operator can also accept if - the implementation allows. -- `extraArgs.context` — empty for the reference self-registry; production - registries may require their own context fields. - -The wallet composes this with `AllocationFactory_Allocate` in the -same submission to avoid creating duplicate allocations. - -### Settlement - -`V2.SettlementFactory.SettlementFactory_SettleBatch` - -Required inputs: -- `settlement : V2.SettlementInfo` — exactly the - `mkTradeSettlementInfo` output (or `poolSettlement`). -- `transferLegs : [V2.TransferLeg]` — the legs being settled, in the - order the allocations expect. -- `allocations : [V2.FinalizedAllocation]` — every allocation whose - authorizer participates in the legs. For iterated settlement, each - finalized allocation carries any settlement-time - `extraTransferLegSides` and the desired `nextIterationFunding`. -- `actors : [Party]` — `[venue/operator]`. -- `extraArgs.context` — registry-supplied choice context for the - allocation admin. Self-registries may return empty context. - -### Iterated settlement - -`V2.FinalizedAllocation.extraTransferLegSides` and -`V2.FinalizedAllocation.nextIterationFunding` on -`SettlementFactory_SettleBatch`. - -Required inputs: -- `extraTransferLegSides` — concrete settlement leg-sides supplied by - the app choice once the trade or pool action is known. -- `nextIterationFunding` — `Some` when the settlement should create a - next-iteration allocation for remaining pool/order funding; `None` - when the allocation terminates at this settlement. -- `extraArgs.context` — registry-supplied choice context for the - settlement admin. Self-registries may return empty context. - -### Registry administration is separate - -The DEX does not mint, burn, or transfer base/quote holdings through custom app -choices. The reference registry's `Registry_Mint` and `Registry_Burn` choices -are bootstrap/admin utilities; peer-to-peer transfers use the standard -`V2.TransferFactory` and `V2.TransferInstruction` interfaces. A different -registry may require choice context for those operations, but that context is -not part of a DEX settlement request. +| Is the exact request body sent, normalized, and never cached? | [`registry-client.test.ts`](../../services/operator-backend/test/registry-client.test.ts) | +| Does a two-admin trade keep preview arguments, contexts, and disclosures separate? | [`matched-trade.test.ts`](../../services/operator-backend/test/matched-trade.test.ts) | +| Does order matching preview before one atomic value-moving execute? | [`match-leg-shape.test.ts`](../../services/operator-backend/test/match-leg-shape.test.ts) and [`order-fill-recording.test.ts`](../../services/operator-backend/test/order-fill-recording.test.ts) | +| Is the fixed atomic-liquidity path explicit, and does generic HTTP discovery fail before submission? | [`pool.test.ts`](../../services/operator-backend/test/pool.test.ts) | +| Are split-admin Daml contexts kept in their correct fields? | `testDvpSettleThreadsBothAdminContexts` in [`ChoiceContextWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/ChoiceContextWorkflowTests.daml) | +| Does a context-requiring registry reject missing context? | `testRealRegistryDvpRejectsMissingContext` in [`RealRegistryDvpTests.daml`](../../trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml) | + +## Reference: Daml choice fields + +### `AllocationFactory_Allocate` + +- `settlement`: settlement identity and executors. +- `allocation`: the exact allocation specification. +- `requestedAt`: the operation timestamp. +- `inputHoldingCids`: holdings selected by the wallet. +- `actors`: parties authorizing allocation creation. +- `extraArgs`: registry context returned for this operation. + +### `SettlementFactory_SettleBatch` + +- `settlement`: the settlement identity. +- `transferLegs`: exact movements being settled. +- `allocations`: finalized allocations, including extra leg sides and any + next-iteration funding. +- `actors`: settlement executors. +- `extraArgs`: registry context returned for this batch. + +The DEX does not use custom base/quote mint, burn, or balance choices during a +trade. Issuance remains registry administration; the DEX composes allocation +and settlement surfaces. --- -**Where to read next:** [Registry Integration](registry-integration.md) · [Allocation Surface](../reference/allocation-surface.md) · [All docs](../README.md) +**Where to read next:** [Registry integration](registry-integration.md) · +[Allocation surface](../reference/allocation-surface.md) · +[Daml proof map](../reference/daml-proof-map.md) diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index 6dd6a1bd..76c43d02 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -1,14 +1,23 @@ # Deployment guide -Three ways to run the reference DEX, ordered by how much Canton you bring. -**Local dev** needs no participant at all; **Docker Compose** packages the whole -edge — backend plus nginx — in front of a remote Canton participant; **direct -testnet** runs that same backend under your own process supervisor. Pick one. - -Two invariants hold across all three: only the operator backend holds -`CANTON_LEDGER_TOKEN` and submits with operator authority, and it never signs as -a trader — add/remove liquidity, swaps, and order funding are authored by a -wallet. See the [wallet boundary](run-on-testnet.md#wallet-boundary). +Five ways to run the reference DEX, ordered by how much infrastructure you +bring. **Local dev** is an in-memory UI/read-model demo; the **DPM sandbox** is +the default reproducible live-ledger learning path; **DevKit LocalNet** is an +optional persistent Splice environment; **Docker Compose** packages the edge +in front of a remote participant; and **direct testnet** runs that backend +under your own process supervisor. Pick the mode that proves the boundary you +care about. + +In the packaged topology the participant credential is server-side; it is +never compiled into the dApp. Use separate least-privilege credentials where +your participant supports them: registry bootstrap needs the registry admins, +while runtime pool administration/settlement needs the operator and LP +registrar (plus the read rights described below). Trader allocations are +authored by a wallet. The arbitrary token-standard command relay is +development-only and is hard-disabled in `testnet-server.ts`. A narrower +hosted-RFQ authority relay exists as an explicit opt-in for custodial demos; it +is not self-custody and requires per-caller binding. See the +[authorization boundaries](run-on-testnet.md#6-wallet-and-http-authorization-boundaries). ## 1. Local dev (no Canton) @@ -36,7 +45,55 @@ write-gate flags, wallet options, and the test suites — is in [Local Setup & Testing](../getting-started.md); this page covers the real-Canton paths. -## 2. Docker Compose +## 2. DPM sandbox (default live Canton proof) + +This is the recommended learning and ledger-integration path. It requires the +pinned DPM SDK and Java 17, but it does **not** require Canton DevKit, Docker, a +pre-existing participant, or an external wallet: + +```bash +bash scripts/run-dpm-sandbox-proof.sh +``` + +The wrapper builds the current DAR, starts a throwaway SDK sandbox on six +reserved loopback ports, allocates a bootstrap operator/admin/LP-registrar +party plus distinct LP/trader and swapper parties, uploads the package closure, +and proves add liquidity → quote-bound +swap → half-LP removal through the JSON Ledger API. It asserts exact balances, +reserves, slice reconciliation, LP supply, `x*y`, reserve-per-LP, and total +value conservation, then tears the sandbox down after a pass. + +This is a direct-ledger integration proof. It deliberately bypasses the +operator HTTP server, React dApp, and wallet transport. See [Local Canton from +a clean clone](localnet.md#path-a-portable-dpm-sandbox-proof) for the phase log, +party model, failure artifacts, and exact proof boundary. + +## 3. DevKit LocalNet (optional persistent Canton) + +Use this only when your environment already provides the separately +distributed `canton-devkit` executable and Docker. The adapter starts or reuses +a named Splice/Canton LocalNet, maps its credential without printing the JWT, +allocates distinct live roles when overrides are absent, builds/uploads the +package closure, and runs the same DvP round trip: + +```bash +bash scripts/run-localnet-roundtrip.sh canton-dex +``` + +The instance remains available for contract inspection. Stop its containers +while preserving ledger volumes with: + +```bash +canton-devkit localnet down --name canton-dex +``` + +DevKit is a network lifecycle and credential adapter here; neither the DEX +application nor its DAR has a runtime dependency on it. See [Local Canton from +a clean clone](localnet.md#path-b-optional-persistent-devkit-localnet) for the +prerequisite check, role allocation, inspection commands, and destructive +cleanup warning. + +## 4. Docker Compose The packaged edge, for running against a remote Canton testnet or MainNet. Two containers come up: @@ -53,43 +110,56 @@ flowchart LR N -->|"serves Vite build"| B N -->|"/v1/* → proxy"| A["backend
testnet-server.ts :8080"] A -->|"SQLite"| V[("backend-data
volume")] - A -->|"JSON Ledger API
(operator authority)"| P[("Canton participant
CANTON_LEDGER_URL")] + A -->|"JSON Ledger API
(configured operator/LP rights)"| P[("Canton participant
CANTON_LEDGER_URL")] ``` -nginx is the only ingress. Operator-API traffic takes the path above; trader -wallet calls reach Canton directly from the browser and do not pass through the -backend. +nginx is the only published ingress: Compose uses `expose: 8080` for the +backend's private service-network port and publishes only frontend `:80`. +Operator-API traffic takes the path above; production wallet calls use the +selected wallet adapter rather than a participant token embedded in the +browser. ```bash cp services/operator-backend/.env.example .env -# Edit .env: CANTON_LEDGER_URL, CANTON_LEDGER_TOKEN, party ids, synchronizer, -# package id — see Environment variables below. +# Edit .env: ledger URL/token, party ids, package/synchronizer ids, asset and +# (when distinct) LP registry factory cids, and both HTTP write tokens. + +# Also add one production wallet configuration to .env, or export it for this +# Compose invocation. Example: +export VITE_ENABLE_PARTYLAYER=1 +export VITE_PARTYLAYER_NETWORK=canton:testnet +export VITE_PARTYLAYER_WALLET_IDS=console,nightly,send docker compose build docker compose up -d ``` Compose reads the repo-root `.env` for **both** the backend environment and the -frontend `VITE_*` build args (baked at build time — rebuild the frontend to -change them). See [`docker-compose.yml`](../../docker-compose.yml) for the exact -wiring. Persistent state lives in the `backend-data` volume (the SQLite indexer -DB). To wipe and restart fresh: +frontend's explicitly declared safe/public `VITE_*` build args. Rebuild the +frontend to change them. HTTP API bearer tokens are backend runtime variables, +never Vite build arguments. See [`docker-compose.yml`](../../docker-compose.yml) +for the exact wiring. Persistent state lives in the `backend-data` volume (the +SQLite indexer DB). + +The following command destroys the `backend-data` Docker volume, including the +local index and idempotency records. It does not roll back Canton ledger state: ```bash docker compose down -v && docker compose up -d ``` -## 3. Testnet deployment (no containers) +## 5. Testnet deployment (no containers) Run the same backend directly and manage the Node process yourself (systemd, pm2, fly.io, …). Two ways in. ### Automated: `deploy-testnet.sh` -[`scripts/deploy-testnet.sh`](../../scripts/deploy-testnet.sh) drives the full -first-time sequence against a participant: build DARs → upload → allocate the -operator / lpRegistrar / admin / demo-trader parties → run the registry -bootstrap → seed a BTC/USDC pair → health-check. +[`scripts/deploy-testnet.sh`](../../scripts/deploy-testnet.sh) runs only the +phases it can prove: build DARs → upload the package closure → run the registry +bootstrap. It does not allocate parties, start the backend, mint holdings, or +fund a pool. Exact allocated party ids must already exist and the participant +JWT must hold their rights. ```bash export CANTON_LEDGER_URL=... @@ -97,14 +167,27 @@ export CANTON_LEDGER_TOKEN=... export CANTON_OPERATOR=... export CANTON_LP_REGISTRAR=... export CANTON_ADMIN=... -export OPERATOR_ADMIN_TOKEN=... # for the seed step +export CANTON_DEX_PACKAGE_ID=... + +bash scripts/deploy-testnet.sh +``` + +Each default stage is skippable once proved: `DEPLOY_SKIP_BUILD=1`, +`DEPLOY_SKIP_UPLOAD=1`, `DEPLOY_SKIP_BOOTSTRAP=1`. The script stops on upload or +bootstrap failure and prints no success line for a suppressed error. + +After starting the backend, opt into pair plus **unfunded** pool creation: +```bash +DEPLOY_SKIP_BUILD=1 \ +DEPLOY_SKIP_UPLOAD=1 \ +DEPLOY_SKIP_BOOTSTRAP=1 \ +DEPLOY_SEED_MARKETS=1 \ bash scripts/deploy-testnet.sh ``` -Each stage is skippable once done: `DEPLOY_SKIP_BUILD=1`, `DEPLOY_SKIP_UPLOAD=1`, -`DEPLOY_SKIP_PARTIES=1`, `DEPLOY_SKIP_SEED=1`. Party allocation is idempotent, so -re-runs are safe. The script does not start the backend — do that separately. +That phase requires `OPERATOR_ADMIN_TOKEN`, checks backend health first, and +queries existing contracts before creating missing market metadata. ### Manual: run the backend @@ -114,7 +197,7 @@ npm install export CANTON_LEDGER_URL=... export CANTON_LEDGER_TOKEN=... # ... (see Environment variables below) -npm start # runs testnet-server.ts +npm run testnet # runs testnet-server.ts ``` The full walkthrough — smoke checks, package-hash alignment, and the PartyLayer @@ -132,7 +215,10 @@ export CANTON_LEDGER_TOKEN=... export CANTON_ADMIN=... export CANTON_LP_REGISTRAR=... export CANTON_OPERATOR=... -node --import tsx scripts/bootstrap-registry.ts +export CANTON_DEX_PACKAGE_ID=... + +cd services/operator-backend +node --import tsx ../../scripts/bootstrap-registry.ts ``` The script is idempotent: running it twice is a no-op. See @@ -144,20 +230,21 @@ optional: the pool's LP token is issued by this repository, and its allocation specs name the lpRegistrar as admin, which `Registry.V2` asserts against its own. Without it, add- and remove-liquidity cannot allocate, whatever the pool trades. -A second registry, under `CANTON_ADMIN`, is created only if you add a -`registryV2` block to -[`scripts/bootstrap-registry.json`](../../scripts/bootstrap-registry.json) (the -committed config has none). That one is for instruments a deployment mints -itself; a deployment whose users bring their own Token Standard V2 assets does -not need it. - -`CANTON_ALLOC_FACTORY_CID` and `CANTON_SETTLE_FACTORY_CID` are a single-registry -stopgap — the `FixedRegistry` in -[`testnet-server.ts`](../../services/operator-backend/src/testnet-server.ts) -returns them for every admin, standing in for the per-admin registry lookup the -design calls for. Unset, they default to `PENDING_*` placeholders. In a -deployment serving foreign tokens, each admin's factory cid comes from that -admin's own registry API, not from these variables. +A second registry under `CANTON_ADMIN` is always created when the admin differs +from the LP registrar. The optional `registryV2` config block overrides its +users and instrument list; otherwise the top-level `instruments` list is used. +When both roles are the same party, bootstrap reuses the single registry. + +The testnet server has an explicit per-admin map for the two reference +registrars. `CANTON_ALLOC_FACTORY_CID` / `CANTON_SETTLE_FACTORY_CID` identify +the asset admin's registry. When `CANTON_LP_REGISTRAR != CANTON_ADMIN`, the +separate `CANTON_LP_ALLOC_FACTORY_CID` / +`CANTON_LP_SETTLE_FACTORY_CID` pair identifies the LP registry. In the +reference `Registry.V2`, the same registry cid implements both interfaces, so +the two values within each pair are equal. Full/write mode refuses to start if +a required mapping is absent; explicit `DEX_READ_ONLY=1` may use display-only +`PENDING_*` placeholders. A venue listing arbitrary third-party admins should +replace this two-admin map with registry API discovery. ## Environment variables @@ -166,42 +253,64 @@ and [`app/web/.env.example`](../../app/web/.env.example) are the canonical lists (including the wallet-provider flags). The backend variables that matter for a real deployment: -**Required** — the backend exits at boot if any is missing: +**Always required** — both full and intentional read-only modes exit at boot if +any is missing: | Var | Purpose | |-----|---------| | `CANTON_LEDGER_URL` | JSON Ledger API base URL | -| `CANTON_LEDGER_TOKEN` | Bearer JWT for the participant (operator authority) | +| `CANTON_LEDGER_TOKEN` | Server-side participant JWT with the read/actAs rights needed by the enabled runtime flows | | `CANTON_OPERATOR` | Operator party id | | `CANTON_LP_REGISTRAR` | LP registrar party id | | `CANTON_ADMIN` | Asset admin party id | +| `CANTON_DEX_PACKAGE_ID` | Vetted DEX package hash or package-name prefix used to qualify every template id | + +**Required in full/write mode:** + +| Var | Purpose | +|-----|---------| +| `CANTON_ALLOC_FACTORY_CID` | Asset-admin AllocationFactory cid | +| `CANTON_SETTLE_FACTORY_CID` | Asset-admin SettlementFactory cid | +| `CANTON_LP_ALLOC_FACTORY_CID` | LP-registry AllocationFactory cid when LP registrar differs from asset admin | +| `CANTON_LP_SETTLE_FACTORY_CID` | LP-registry SettlementFactory cid when LP registrar differs from asset admin | +| `OPERATOR_ADMIN_TOKEN` | Bearer token for `/v1/admin/*` writes | +| `DEX_OPERATOR_API_TOKEN` | Bearer token for every other state-changing HTTP route | **Defaulted / optional:** | Var | Default | Purpose | |-----|---------|---------| | `CANTON_SYNCHRONIZER` | — | Synchronizer id for command submission | -| `CANTON_DEX_PACKAGE_ID` | — | Package hash prefix for template ids | -| `CANTON_ALLOC_FACTORY_CID` | `PENDING_ALLOC_FACTORY` | `FixedRegistry` AllocationFactory cid | -| `CANTON_SETTLE_FACTORY_CID` | `PENDING_SETTLE_FACTORY` | `FixedRegistry` SettlementFactory cid | | `CANTON_USER_ID` | `ledger-api-user` | JSON Ledger API user id | | `CANTON_NETWORK` | `canton:devnet` | Display label for the network | | `PORT` | `8080` | HTTP server port | +| `HOST` | `127.0.0.1` (`0.0.0.0` in the container) | HTTP bind address; keep loopback for a directly proxied process, bind all interfaces inside a container | | `DB_PATH` | `./data/operator.db` | SQLite indexer DB path (`/app/data/operator.db` in the container) | | `INDEXER_INTERVAL_MS` | `5000` | Indexer polling interval | -| `OPERATOR_ADMIN_TOKEN` | — | Bearer token for `/v1/admin/*`; unset leaves admin routes unprotected | -| `ALLOWED_ORIGINS` | — | CSV of CORS origins; unset allows all | - -**Frontend build args** (baked into the static build; see -[`docker-compose.yml`](../../docker-compose.yml) `args:`): `VITE_API_BASE`, -`VITE_CANTON_NETWORK_ID`, `VITE_CANTON_LEDGER_URL`, `VITE_WC_PROJECT_ID`. +| `DEX_READ_ONLY` | `0` | Set `1` to start intentionally without write tokens or factory cids; state-changing routes return 401 while read-only `POST /v1/swaps/quote` remains available. | +| `DEX_CALLER_JWT_SECRET` / `DEX_CALLER_JWT_AUDIENCE` | — | Optional party binding for private reads and trader-subject writes using `X-Caller-Token`. | +| `DEX_HOSTED_RFQ_RELAY` | `0` | Custodial opt-in for RFQ create/cancel/accept under hosted trader authority; requires caller JWT binding and participant rights for those traders. | +| `ALLOWED_ORIGINS` | — | Exact CSV CORS allowlist; unset is default-deny (no allow-origin header). | + +**Frontend build args** are public and baked into the static assets. Compose +declares the complete supported set under its `frontend.build.args`, including +API/docs/network metadata plus WalletConnect, dApp SDK, gateway, and PartyLayer +configuration. The canonical descriptions and safe defaults are in +[`app/web/.env.example`](../../app/web/.env.example); no participant or HTTP +API bearer token is an accepted production build argument. ## Production checklist -- [ ] `OPERATOR_ADMIN_TOKEN` set to a strong random value -- [ ] `ALLOWED_ORIGINS` narrowed to your dApp host (not unset / `*`) +- [ ] Separate strong `OPERATOR_ADMIN_TOKEN` and `DEX_OPERATOR_API_TOKEN` values set +- [ ] Tokens delivered through a trusted session/BFF or short-lived validator tab—not compiled as `VITE_*` +- [ ] `ALLOWED_ORIGINS` contains only the exact dApp host (unset denies all cross-origin browsers) +- [ ] Multi-user deployments enable caller binding so account/history reads and trader-subject writes are party-scoped - [ ] `CANTON_DEX_PACKAGE_ID` and `CANTON_SYNCHRONIZER` pinned to the vetted values -- [ ] `CANTON_ALLOC_FACTORY_CID` / `CANTON_SETTLE_FACTORY_CID` set to real cids (not the `PENDING_*` defaults) +- [ ] Asset factory pair set to the live asset registry cid; LP factory pair also set when the registrar differs +- [ ] `/v1/status` reports `synced: true` after a genuine participant ledger-end probe (not merely HTTP 200) +- [ ] Exactly one tested production wallet path enabled; no DEV-only provider or relay relied upon +- [ ] Hosted RFQ is either off on both tiers, or deliberately enabled with both `DEX_HOSTED_RFQ_RELAY=1` and `VITE_ENABLE_HOSTED_RFQ=1`, mandatory caller binding, and scoped trader rights +- [ ] Backend is private behind ingress and runs as the image's non-root `node` user - [ ] Indexer DB on a persistent volume (`backend-data` under Compose; `DB_PATH=/var/lib/dex/operator.db` bare) - [ ] Process supervisor restarts on crash (systemd / pm2 / `restart: unless-stopped`) - [ ] TLS terminated at your ingress in front of `:80` (Compose) or `:8080` (bare) diff --git a/docs/guides/localnet.md b/docs/guides/localnet.md new file mode 100644 index 00000000..b67c3bf0 --- /dev/null +++ b/docs/guides/localnet.md @@ -0,0 +1,199 @@ +# Local Canton from a clean clone + +This repository does **not** require Canton DevKit. It supports two local +network experiences with different boundaries: + +| Path | Additional prerequisite | What it proves | What it does not prove | +|---|---|---|---| +| **DPM sandbox proof (default)** | none beyond the pinned DPM SDK | Real Canton process, JSON Ledger API, current DEX DAR with its Token Standard closure, distinct LP/swapper parties, and add → swap → remove DvP settlement | Splice wallet/scan UIs, multi-participant topology, browser/backend HTTP, external wallet | +| **DevKit LocalNet (optional)** | a separately distributed `canton-devkit` executable and Docker | Full persistent Splice LocalNet services plus the same DEX live driver | Production topology/security and an automated browser-wallet test | + +The DEX application and its DARs have no runtime dependency on DevKit. The +optional script is a lifecycle and credential adapter: it starts or reuses the +named developer network, but the separately distributed `canton-devkit` +executable must already be installed. + +## Prerequisites + +Both paths need: + +- Node.js 24 or newer +- Java 17 +- DPM and the SDK version pinned in `trading/daml.yaml` +- `curl`, Bash, and npm + +Verify them from the repository root: + +```bash +node --version +java -version +dpm --version +curl --version +``` + +The default proof does not need Docker. The optional DevKit path does. + +## Path A: portable DPM sandbox proof + +Run: + +```bash +bash scripts/run-dpm-sandbox-proof.sh +``` + +The script performs these visible phases: + +1. Installs SDK 3.5.2 idempotently and builds `canton-dex-trading`. +2. Reserves all six Canton ports, releases them together, and starts the SDK's + `dpm sandbox` immediately on those concrete loopback ports. +3. Waits for `/v2/state/ledger-end`; readiness is proven, not assumed. +4. Creates one unrestricted user only inside this unauthenticated throwaway + sandbox. The bootstrap party is operator/admin/LP registrar; the script then + allocates a distinct LP/trader party and a distinct swapper party. +5. Uploads exactly the newly built trading DAR selected by + `trading/daml.yaml`; the DAR embeds its Token Standard dependency closure. +6. Runs the direct JSON-API driver through add liquidity, a quote-bound swap, + and redemption of half the LP position. +7. Checks exact balances and reserves, active-slice sums after every phase, LP + holding/supply/policy agreement, `x*y` nondecrease, reserve-per-LP, and + aggregate base/quote value conservation. +8. Stops Canton and removes its temporary state after a pass. + +The final checkpoint is: + +```text +==> PASS: portable live-Canton proof completed + The throwaway sandbox is now stopping; no persistent ledger state remains. +``` + +If a phase fails, the script preserves its temporary directory and prints the +path containing `canton.log` and `canton.stdout.log`. It never prints a JWT—the +DPM sandbox has authentication disabled and the placeholder bearer value is not +a credential. + +### Party and credential model + +The proof needs real counterparties: the LP/trader, swapper, and operator are +three distinct Canton parties. This prevents a deposit or swap from degenerating +into a transfer from a party to itself. The operator party also acts as asset +admin and LP registrar for this self-contained fixture, however, and the single +sandbox user has `CanExecuteAsAnyParty`, `CanReadAsAnyParty`, and +`ParticipantAdmin` rights. That is deliberately convenient throwaway setup, +not a production authorization model. + +Focused Daml tests cover finer-grained controller failures with separate +parties. A deployment sign-off must additionally prove its actual users, JWTs, +and least-privilege rights with the +[Validator Test Plan](validator-test-plan.md). + +### What this proof intentionally bypasses + +The driver submits JSON Ledger API commands directly. It does not start: + +- the operator HTTP server; +- the React dApp; +- a wallet extension or PartyLayer; +- a multi-participant Splice network. + +Passing it is live-ledger integration evidence, not browser full-stack E2E +evidence. The [testing boundary matrix](../reference/testing.md) +is the authoritative scope definition. + +## Path B: optional persistent DevKit LocalNet + +Use this only when your development environment already distributes +`canton-devkit`: + +```bash +command -v canton-devkit +canton-devkit version +``` + +If the first command prints nothing, skip this path. The repository does not +silently download or install an unpinned network manager. Use Path A, an +organization-approved DevKit installation, or the official Canton Network +Quickstart selected by your deployment team. + +Docker must be running. Then execute: + +```bash +bash scripts/run-localnet-roundtrip.sh canton-dex +``` + +The integration wrapper: + +1. runs `canton-devkit localnet doctor`; +2. starts or reuses the named `0.6.12` instance; +3. imports the app-provider endpoint and JWT inside the process without + printing the token; +4. discovers the ledger user's primary party; +5. builds/uploads the DEX package closure; and +6. allocates an LP/trader and swapper through the standard JSON Ledger API when + explicit party overrides are absent; and +7. executes the same add → quote-bound swap → half-LP-remove driver. + +Unlike Path A, it deliberately leaves the instance running so you can inspect +contracts and transactions: + +```bash +canton-devkit localnet status --name canton-dex +canton-devkit localnet contracts --help +canton-devkit localnet tx --help +``` + +Stop containers while preserving the instance volumes: + +```bash +canton-devkit localnet down --name canton-dex +``` + +The following is destructive and deletes that named instance's ledger state: + +```bash +canton-devkit localnet remove --name canton-dex +``` + +### Override the generated live parties + +By default the wrapper uses the app-provider primary party for operator/admin/ +LP-registrar and allocates missing LP/trader and swapper parties through the +JSON Ledger API. To exercise pre-provisioned parties instead, ensure the DevKit +ledger user can act as them, then run: + +```bash +DEX_LOCALNET_OPERATOR="" \ +DEX_LOCALNET_ADMIN="" \ +DEX_LOCALNET_TRADER="" \ +DEX_LOCALNET_SWAPPER="" \ +bash scripts/run-localnet-roundtrip.sh canton-dex +``` + +The LP registrar currently follows `DEX_LOCALNET_ADMIN` for the self-registry +test fixture. The trader and swapper must each differ from the operator. A +production deployment normally uses distinct roles and the participant-specific +setup in [Run against a Canton testnet](run-on-testnet.md). + +## Path C: bring your own participant + +Neither local launcher is required when you already have a participant. Export +the exact contract-party/package environment listed in +[Testing](../reference/testing.md#live-canton-probes), run the backend package +script from `services/operator-backend`, and treat every live probe as +state-mutating. For a long-lived deployment, follow +[Run against a Canton testnet](run-on-testnet.md). + +## Troubleshooting + +| Symptom | Meaning and action | +|---|---| +| `dpm: command not found` | Install DPM first; the portable proof cannot start Canton without the pinned SDK. | +| Java class-version/startup error | Activate Java 17 and rerun `java -version`. | +| Canton is not ready after 120 seconds | Read the preserved log directory printed by the proof; check memory and port-binding errors. | +| `/v2/packages` rejects a DAR | The target participant does not accept the committed dependency hash or the DEX DAR was not rebuilt. On a governed network, vet the exact package closure. | +| `USER_NOT_FOUND` | The driver user was not created on a manual participant. The portable script creates it automatically only in its throwaway sandbox. | +| `PERMISSION_DENIED` for `actAs` | The participant JWT user lacks rights for one of `CANTON_OPERATOR`, `CANTON_ADMIN`, `CANTON_LP_REGISTRAR`, or `CANTON_TRADER`. | +| `canton-devkit: command not found` | DevKit is optional; use the DPM sandbox proof or install it through an approved distribution. | + +--- + +**Where to read next:** [AMM-first walkthrough](../tutorials/amm-first-walkthrough.md) · [Testing](../reference/testing.md) · [Deployment](deployment.md) diff --git a/docs/guides/operator-guide.md b/docs/guides/operator-guide.md index d4c36971..a28530b4 100644 --- a/docs/guides/operator-guide.md +++ b/docs/guides/operator-guide.md @@ -47,9 +47,16 @@ bash scripts/fetch-splice-dars.sh bash scripts/build-trading-surface.sh ``` -Outputs `.daml/dist/canton-dex-*.dar`. +The current DEX DAR is written under `trading/.daml/dist/`; the deployment +script derives its exact filename from `trading/daml.yaml` so a stale DAR is +never selected by a broad glob. -### 2. Upload DARs, allocate parties, bootstrap the registry +### 2. Upload DARs and bootstrap the registries + +Allocate the operator, LP registrar, and asset-admin parties through your +participant first. This repository cannot make that participant-specific +governance decision for you. Then provide the exact allocated party ids and a +vetted DEX package hash (or a supported `#package-name` reference): ```bash export CANTON_LEDGER_URL=https://your-participant:7575 @@ -57,18 +64,32 @@ export CANTON_LEDGER_TOKEN=$(...) # JWT for ledger-api-user export CANTON_OPERATOR=op::1220::... export CANTON_LP_REGISTRAR=lp::1220::... export CANTON_ADMIN=admin::1220::... +export CANTON_DEX_PACKAGE_ID= -./scripts/deploy-testnet.sh +bash scripts/deploy-testnet.sh ``` -The script is idempotent. It uploads DARs, allocates the parties if they don't -exist, runs `bootstrap-registry.ts` to create reference-registry -`InstrumentConfig` contracts for BTC / USDC / ETH and the LP -instruments, and (if `OPERATOR_ADMIN_TOKEN` is set) seeds an initial BTC/USDC -pair. +The default run builds and uploads the exact DAR (including its embedded +dependency closure), then idempotently creates `Registry.V2` plus configured +`InstrumentConfig` contracts. It does **not** allocate parties, start the +backend, mint holdings, fund a pool, or create market metadata by default. +Record the final `assetRegistryCid` and `lpRegistryCid` values. A single +`Registry.V2` contract implements both factory interfaces for its admin, so +each allocation/settlement pair below uses the same registry cid: + +```bash +export CANTON_ALLOC_FACTORY_CID= +export CANTON_SETTLE_FACTORY_CID= +# Required only when CANTON_LP_REGISTRAR differs from CANTON_ADMIN: +export CANTON_LP_ALLOC_FACTORY_CID= +export CANTON_LP_SETTLE_FACTORY_CID= +``` -Skip flags for re-runs: `DEPLOY_SKIP_BUILD=1`, `DEPLOY_SKIP_UPLOAD=1`, -`DEPLOY_SKIP_PARTIES=1`, `DEPLOY_SKIP_SEED=1`. +Current re-run flags are `DEPLOY_SKIP_BUILD=1`, `DEPLOY_SKIP_UPLOAD=1`, and +`DEPLOY_SKIP_BOOTSTRAP=1`. After the backend is running, the separate opt-in +`DEPLOY_SEED_MARKETS=1` phase can create a pair plus an **unfunded** pool; it +still does not mint or deposit value. See [Run on a testnet](run-on-testnet.md) +for the complete order and checkpoints. ### 3. Start the operator backend @@ -76,8 +97,8 @@ Skip flags for re-runs: `DEPLOY_SKIP_BUILD=1`, `DEPLOY_SKIP_UPLOAD=1`, cd services/operator-backend cp .env.example .env # Fill in: CANTON_LEDGER_URL, CANTON_LEDGER_TOKEN, party ids, -# OPERATOR_ADMIN_TOKEN, DEX_OPERATOR_API_TOKEN, -# ALLOWED_ORIGINS, DB_PATH +# CANTON_DEX_PACKAGE_ID, asset/LP factory CIDs, +# OPERATOR_ADMIN_TOKEN, DEX_OPERATOR_API_TOKEN, ALLOWED_ORIGINS, DB_PATH npm install npm start ``` @@ -163,12 +184,16 @@ admin routes. Each maps to one choice on `DexPair`: | Action | Route | Choice | |---|---|---| -| Pause / resume trading | `POST /v1/admin/pairs/:cid/active` | `DexPair_SetActive { newActive }` | +| Change listing active metadata | `POST /v1/admin/pairs/:cid/active` | `DexPair_SetActive { newActive }` | | Change fees | `POST /v1/admin/pairs/:cid/fee-model` | `DexPair_UpdateFeeModel { newFeeModel }` | | Change order-book / pool mode | `POST /v1/admin/pairs/:cid/trading-mode` | `DexPair_UpdateTradingMode { newTradingMode }` | -Pausing toggles the `active` flag without archiving the pair record, so a -paused pair keeps its history and fee policy and can be resumed in place. +`DexPair.active`, `tradingMode`, and `feeModel` are listing/discovery metadata +in this revision. Updating them preserves the pair's history, but the pool and +order terminal choices do not fetch `DexPair`; therefore this flag alone is +**not** an on-ledger trading halt. Use `PoolRules_Pause` for pools, stop +off-ledger order routing, and add an explicit terminal-choice gate if your +production policy requires pair-wide enforcement. ### Create a pool diff --git a/docs/guides/operator-runbook.md b/docs/guides/operator-runbook.md index b5b4661c..ce5dd91e 100644 --- a/docs/guides/operator-runbook.md +++ b/docs/guides/operator-runbook.md @@ -13,9 +13,10 @@ Canton operational concern, not a DEX one — see [Out of scope](#out-of-scope-f ## Roles and party model -The reference deployment expects four distinct parties. Keeping them logically -separate is part of the design. Collapsing them is acceptable for a single- -operator dev instance but should not be the production posture. +The reference uses four logical roles and can involve many trader, LP, and +asset-admin parties. Keeping control roles separate is the recommended +production posture; a local learning instance may intentionally share a party +where the setup guide says so. | Party | Owns | Signs | | ------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------- | @@ -35,17 +36,17 @@ In rough order of dependency: 1. **Allocate parties.** `operator`, `lpRegistrar`, base-asset `admin`, quote-asset `admin`, and any traders / LPs you want to onboard. -2. **Bring up registries.** For each `admin`, create: - - `MockAllocationFactory` (or the production registry's allocation - factory) with `users` = the parties that will exercise on it - - `MockSettlementFactory` (or production) with the same `users` - - the registry-specific instrument definition for each instrument the admin - manages. In the reference registry this is `InstrumentConfig`; keep its - requirement lists empty unless your registry replaces the placeholder - verifier with issuer-authorized evidence checks +2. **Bring up real registries.** Run the idempotent + [`bootstrap-registry.ts`](../../scripts/bootstrap-registry.ts) path to create + `Registry.V2` plus each `InstrumentConfig`, or configure a conforming + external Token Standard V2 registry. `MockAllocationFactory` and + `MockSettlementFactory` are Daml-test fixtures only: they do not create or + move holdings and must not be used as a deployment recipe. When asset admin + and LP registrar differ, record both registry cids for the backend's + per-admin factory mapping. 3. **List trading pairs.** Operator creates a `DexPair` per pair with the - chosen `tradingMode` and `feeModel`. Pairs are toggled `active` to gate - trading without archiving the pair record. + chosen `tradingMode` and `feeModel`. These fields are listing metadata in + this revision; they do not independently gate pool/order terminal choices. 4. **Create LP infrastructure (per pool).** - `lpRegistrar` creates the LP token's registry-specific instrument definition. In the reference registry this is one `InstrumentConfig` @@ -58,14 +59,19 @@ In rough order of dependency: add-liquidity DvP request/allocate/settle flow as later LPs; the settle creates the first `PoolSlice` contracts and transitions the state to `PS_Active`. -6. **Open the order book / swap surface.** Once pools are funded and pairs - are active, traders may submit `OrderFundingRequest`, liquidity adds/removes - via the DvP `/request` flow, `Rfq`, etc. - -The dev / testnet path in -[`trading-tests/CantonDex/Tests/EndToEndTests.daml`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) -walks every step above against the mock registry — treat it as the canonical -bring-up script (it proves the full deploy sequence settles end to end). +6. **Open the order book / swap surface.** Once registries and holdings are + live, pools are funded, `PoolRules` is active, and the operator's off-ledger + routing policy allows the market, traders may submit `OrderFundingRequest`, + liquidity adds/removes via the DvP `/request` flow, `Rfq`, etc. + +The focused [`PoolWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/PoolWorkflowTests.daml), +[`OrderWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/OrderWorkflowTests.daml), +[`TradeWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/TradeWorkflowTests.daml), +and [`ChoiceContextWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/ChoiceContextWorkflowTests.daml) +walk DEX choices against mock factories, but those fixtures do not hold value. +They are not deployment validators. Use the [testnet guide](run-on-testnet.md) +for bring-up and the [Daml proof map](../reference/daml-proof-map.md) plus the +self-contained live AMM round trip for value movement. ## Operator-driven cleanup (on-ledger) @@ -146,7 +152,7 @@ operators do not need a parallel database to explain a trade. | Question | Where to look on-ledger | | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | Why did this RFQ accept go to this dealer? | `MatchedTrade.policyReceipt`, also folded into `SettlementInfo.meta` via `dex.policy.*` keys | -| What pair / fee policy applied at trade time? | `DexPair.feeModel`, `DexPair.tradingMode`, `DexPair.active` at the trade's `createdAt` | +| What pool fee was executed? | The immutable `Pool.feeBps` used by `PoolRules`; `DexPair.feeModel` is listing metadata and is not consumed by that choice | | Where did this pool's reserves come from? | Each `PoolSlice` is an `Allocation` CID, each carrying its admin, authorizer, and committed funding | | What's the current head slice / boundary candidate? | each active `PoolSlice` for the pool (query the ACS by `poolId`); the aggregate is `PoolState.reserves.baseAmount`/`quoteAmount` | | Did this trader's funding accept? | The `OrderAllocationRequest` archive event plus the corresponding `Allocation` create event | @@ -155,8 +161,9 @@ operators do not need a parallel database to explain a trade. Off-ledger telemetry the operator should also collect: -- **Latency** per workflow (`OrderFundingRequest_Bind` → `Order_Fund`, - `Rfq_Accept` → `MatchedTrade_Settle`, `PoolRules_Swap` end-to-end). +- **Latency** per explicitly named workflow boundary + (`OrderFundingRequest_Bind` → `Order_Fund`, `Rfq_Accept` → + `MatchedTrade_Settle`, or request → settlement around `PoolRules_Swap`). - **Failure counts** per choice, especially slippage rejections, allocation conservation failures, and registry choice-context rejections. - **Slice-count distributions** per pool side, to flag when consolidation @@ -370,12 +377,15 @@ guard. ## Single-operator dev shortcut -For local exploration, collapse `operator` / `lpRegistrar` / `admin` into one -party, and run the dev server with `DEX_DEV_OPEN=1` so the operator-token gate -is bypassed (in-memory dev only). Tests under `trading-tests/` show the -multi-party shape, but the same contracts compile and run with one party -signing everything. Production should keep the parties distinct so audit-trail -and key-management responsibilities stay decoupled, and must set +For local exploration, the control roles `operator` / `lpRegistrar` / `admin` +may share one party, and the in-memory dev server may use `DEX_DEV_OPEN=1` to +bypass the operator-token gate. Do not collapse a value-moving counterparty +into that party: a real registry rejects the self-transfer created when the LP +or swapper equals the operator. The portable sandbox proof therefore allocates +distinct LP/trader and swapper parties even though its three control roles +share the bootstrap party. Production should normally separate the control +roles too so audit-trail and key-management responsibilities stay decoupled, +and must set `DEX_OPERATOR_API_TOKEN` / `OPERATOR_ADMIN_TOKEN` — both gates fail closed otherwise, proven in [`auth.test.ts`](../../services/operator-backend/test/auth.test.ts) diff --git a/docs/guides/registry-integration.md b/docs/guides/registry-integration.md index ed6b58a0..b5f9bdce 100644 --- a/docs/guides/registry-integration.md +++ b/docs/guides/registry-integration.md @@ -1,4 +1,4 @@ -# Registry Prerequisites +# Registry integration prerequisites What the DEX assumes from an asset registry. Token Standard V2 standardizes the holding/allocation/settlement interfaces; it does not standardize a particular @@ -22,15 +22,15 @@ flowchart LR AF["AllocationFactory"] SF["SettlementFactory"] H[("Holding")] - CC(["Choice-context endpoint
(off-ledger)"]) + CC(["Operation-specific V2 endpoints
(off-ledger HTTP)"]) end W -->|"AllocationFactory_Allocate
locks holdings into an Allocation"| AF OB -->|"SettlementFactory_SettleBatch
atomic net settlement"| SF W -.->|"observe / select"| H AF --> H SF --> H - OB -.->|"fetch disclosures"| CC - CC -.->|"extraArgs"| SF + OB -.->|"POST exact choiceArguments"| CC + CC -.->|"factory + context + disclosures"| SF ``` Solid arrows are on-ledger interface choices; dashed arrows are off-ledger @@ -99,7 +99,7 @@ trades, the registry must provide: | Assumption | Where it shows up | |---|---| | `instrumentId` is stable across the instrument's lifetime | Order, Pool, MatchedTrade, Rfq all key on it | -| Factory and choice-context discovery is admin-controlled | The operator fetches these off-ledger and flushes its registry-client cache after a registry republishes factories or disclosures | +| Factory and choice-context discovery is admin-controlled | The app performs a fresh operation-specific V2 lookup with the concrete choice arguments; it does not reuse one admin-level cached context across operations | | Allocation creation can consume one or more holdings and return change | The trader's wallet selects holdings; the registry factory validates and locks them | | Allocation factory accepts arbitrary `AllocationSpecification` shapes (prefunded, with-legs, committed or uncommitted, with `nextIterationFunding`) | Orders require both deadline-committed and trader-withdrawable GTC shapes; pools require committed inventory | | Settlement factory enforces transfer-leg consistency with allocations | OTC / matched-trade settlement and `PoolRules_Swap` rely on the factory to validate, not the DEX | @@ -115,15 +115,25 @@ TTL; that does not imply another registry will accept the same lifetime. ## Registry API surface (Daml + OpenAPI) -Token Standard V2 registries are expected to expose both the Daml -interfaces and the standard OpenAPI endpoints (the specs ship alongside -each API package in `canton-network/splice` under `token-standard/`). The -reference registry implements the Daml surfaces used by this DEX; its -off-ledger integration is represented by the factory and choice-context -endpoints the backend's registry-client consumes -(see [Choice Context](choice-context.md)). A production registry should -implement the standard OpenAPI so V2-compliant wallets and apps can discover -factories and context without bespoke integration. +Token Standard V2 registries are expected to expose both the Daml interfaces +and the standard OpenAPI endpoints. The specs used here are committed beside +the vendored packages under [`vendor/splice/token-standard`](../../vendor/splice/token-standard/). +The backend client uses the canonical operation-specific POST endpoints for +allocation-factory discovery, settlement-factory discovery, and per-allocation +cancel/withdraw context. Every factory request includes the concrete Daml JSON +`choiceArguments`; responses are runtime-validated and are not cached. See +[Choice context](choice-context.md#3-canonical-v2-http-endpoints) for the exact +paths, bodies, and response shape. + +The configured reference self-registry is a deliberate adapter, not a second +HTTP protocol. `FixedRegistryClient` resolves deployed factory CIDs per admin +and returns empty context. This is also the only backend adapter currently able +to drive atomic add/remove liquidity: those Daml choices create temporary +allocations and settle them in the same transaction, so their future CIDs +cannot appear in an exact HTTP preflight request. Generic HTTP discovery fails +with `RegistryError("unsupported", ...)` before submission for that workflow. +Swaps, matched trades, order matches, allocation creation, and cancellation use +the canonical operation-specific discovery path. The DEX's own flows are exercised against a standard-shaped registry, not only its reference one. `testMatchedTradeViaTokenStandardRegistry` in @@ -134,7 +144,10 @@ a bespoke one. `testRealRegistryDvpAddSettles` and `testRealRegistryDvpSwapSettl in [`RealRegistryDvpTests.daml`](../../trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml) settle add-liquidity and swap DvPs against a genuinely context-requiring registry, and `testRealRegistryDvpRejectsMissingContext` proves the settle -aborts when that registry's disclosed context is dropped. +aborts when that registry's disclosed context is dropped. These are Daml +composition proofs: the tests already possess the context contracts. They do +not remove the off-ledger future-CID limitation for the backend's atomic +liquidity HTTP preflight. ## Mint / Burn / Transfer prerequisites @@ -209,14 +222,18 @@ registries are expected to enforce at least what `Registry.V2` does. ## Choice-context retrieval the DEX needs When the operator or trader builds a transaction that touches a registry -contract, the registry may require extra disclosed contracts or context. In the -reference registry this context is empty. External registries may return -disclosed configuration, rights, or credential contracts. The DEX -operator backend's **registry-client** module is responsible for fetching the -registry-specific context and attaching it to the choice arguments. - -See [Choice Context](choice-context.md) for the exact -inputs each registry choice expects. +contract, the registry may require extra disclosed contracts or context. The +reference self-registry's context is empty. External registries may return +disclosed configuration, rights, or credential contracts. + +The DEX's `registry-client` takes the exact operation arguments, calls the +matching standard endpoint, validates the wire response, and returns the +factory CID, context, and disclosures as one value. Settlement arguments come +from non-value-moving Daml previews for swaps and matched trades, and from an +ephemeral create-and-exercise preview for order matches. Cancel/withdraw +context is looked up per allocation ID, not once per admin. See +[Choice context](choice-context.md) for the complete choreography and the +atomic-liquidity exception. ## Registry-specific lifecycle changes @@ -265,10 +282,13 @@ legs — is rejected rather than settled. ## What the DEX does not assume -- It does not require the reference registry for base or quote assets. An - alternative must implement the V2 holding, allocation, and settlement APIs - used by the workflow and provide compatible factory/context discovery. The - included LP path still uses the concrete `LPTokenPolicy` component. +- It does not require the reference registry for base or quote assets in the + allocation, swap, order, or matched-trade flows. An alternative must + implement the V2 holding, allocation, and settlement APIs and the canonical + operation-specific discovery endpoints. Atomic add/remove liquidity is the + documented exception: the current backend requires the configured + empty-context self-registry adapter until that workflow is redesigned. The + included LP path also uses the concrete `LPTokenPolicy` component. - It does not assume holding precision is uniform. Each registry may expose its own display scale or amount constraints; the DEX treats amounts as `Decimal` and lets the registry enforce its own limits. diff --git a/docs/guides/run-on-testnet.md b/docs/guides/run-on-testnet.md index cb7e11cd..5399e084 100644 --- a/docs/guides/run-on-testnet.md +++ b/docs/guides/run-on-testnet.md @@ -1,72 +1,150 @@ # Run against a Canton testnet The DEX runs as two long-lived processes against a Canton participant: the -**operator backend** (operator-authority commands, ledger reads, the indexer) -and the **web app** (reads plus wallet-authority commands). This guide points -both at a participant that already has the DEX and Token Standard V2 packages -uploaded and vetted, and its parties allocated. The one-time build, upload, -party allocation, registry bootstrap, and pair/pool seeding are automated by -[`scripts/deploy-testnet.sh`](../../scripts/deploy-testnet.sh) — run that first, -or perform its steps by hand, then use this guide to bring up and verify the two -processes. +**operator backend** (configured operator/LP authority, ledger reads, the +indexer) and the **web app** (reads plus wallet-authority commands). This guide points +both at a participant whose operator, LP registrar, and asset-admin parties are +already allocated. The repository automates package build/upload, registry +bootstrap, and optional pair/unfunded-pool creation. It deliberately does **not** +allocate parties or claim to fund a pool: party allocation is participant- +specific, and first funding requires an LP-authorized wallet flow. One invariant throughout: tokens, concrete party ids, and validator-specific package hashes live in the environment, never in the repo. ## Prerequisites -- A Canton participant JSON Ledger API URL and a JWT that can `actAs` the - operator party and any bootstrap parties used by the commands you submit. -- Uploaded and vetted DARs for `canton-dex-trading` (built from `trading/`) and - the Token Standard V2 packages under `vendor/splice/token-standard`. +- Node.js 24, Java 17, DPM with the SDK pinned by `trading/daml.yaml`, and the + backend/frontend dependencies installed with `npm ci`. +- A Canton participant JSON Ledger API URL. For a compact validator setup, its + server-side JWT can `actAs` the operator and LP registrar and read the + configured registrars; pool creation and LP settlement require those control + roles. Registry bootstrap additionally needs `actAs` for each registry admin. + In production, prefer separate least-privilege bootstrap and runtime users. +- The target network must accept the exact Token Standard V2 package hashes in + `vendor/splice/dars/`. A production network may require its governance/vetting + process before upload. - Operator, LP registrar, and asset-admin parties allocated on the participant. - The `lpRegistrar`'s `Registry.V2` and the asset admins' registry factory contracts created — the registry bootstrap in [`scripts/bootstrap-registry.ts`](../../scripts/bootstrap-registry.ts) does this; without the LP registry no pool can allocate a liquidity move. -## Start the operator backend +## 1. Prepare the ledger -The backend runs `src/testnet-server.ts`. It requires five variables and reads -the rest with defaults. Pass the token through the environment; the process -reads it and does not write it to disk. +Copy the backend environment template, fill the participant values, and load it +into the current shell. `npm run testnet` does not implicitly read `.env`. ```bash -cd services/operator-backend +cp services/operator-backend/.env.example services/operator-backend/.env +# Edit services/operator-backend/.env. Do not commit it. + +set -a +source services/operator-backend/.env +set +a +``` + +Use two different high-entropy HTTP API tokens: + +```bash +export DEX_OPERATOR_API_TOKEN="" +export OPERATOR_ADMIN_TOKEN="" +``` + +These are credentials for the DEX HTTP service, not the participant JWT. A +full-mode testnet server refuses to start without both. For an intentional +read-only deployment, set `DEX_READ_ONLY=1`; every state-changing HTTP route +then returns 401 (the read-only `POST /v1/swaps/quote` computation remains open). + +Build, upload, and bootstrap the on-ledger registries: + +```bash +bash scripts/deploy-testnet.sh +``` -export CANTON_LEDGER_TOKEN="" +Expected final line: + +```text +==> Deployment phases completed without a suppressed error +``` + +The script does not allocate parties, start the backend, create a market by +default, mint holdings, or fund a pool. Each successful phase mutates the target +ledger and is not rolled back if a later phase fails. + +Record the `assetRegistryCid` and `lpRegistryCid` fields printed by the final +`bootstrap complete` log. Each reference `Registry.V2` implements both factory +interfaces for its own admin, so the two values within a factory pair are the +same registry cid: + +```bash +export CANTON_ALLOC_FACTORY_CID="" +export CANTON_SETTLE_FACTORY_CID="" + +# Required only when CANTON_LP_REGISTRAR differs from CANTON_ADMIN: +export CANTON_LP_ALLOC_FACTORY_CID="" +export CANTON_LP_SETTLE_FACTORY_CID="" +``` + +The included server maps the configured asset admin and LP registrar +separately. A venue that lists additional third-party admins should replace +this two-admin configuration with discovery from each admin's registry API, as +described in [Registry integration](registry-integration.md). + +## 2. Start the operator backend + +The backend runs `src/testnet-server.ts`. Keep the loaded environment in this +terminal. The process reads credentials from the environment and does not write +them to disk. + +```bash +cd services/operator-backend -CANTON_LEDGER_URL="https://" \ -CANTON_OPERATOR="" \ -CANTON_LP_REGISTRAR="" \ -CANTON_ADMIN="" \ -CANTON_NETWORK="canton:testnet" \ -CANTON_SYNCHRONIZER="" \ -CANTON_DEX_PACKAGE_ID="#canton-dex-trading" \ -PORT=8080 \ npm run testnet ``` | Variable | Required | Purpose | |---|---|---| | `CANTON_LEDGER_URL` | yes | JSON Ledger API base URL of the participant. | -| `CANTON_LEDGER_TOKEN` | yes | Bearer JWT that can `actAs` the operator party. | +| `CANTON_LEDGER_TOKEN` | yes | Server-side JWT with the read/actAs rights required by the enabled operator and LP flows. | | `CANTON_OPERATOR` | yes | Operator (venue) party id. | | `CANTON_LP_REGISTRAR` | yes | LP registrar party id. | | `CANTON_ADMIN` | yes | Asset-admin party id. | +| `DEX_OPERATOR_API_TOKEN` | yes in full mode | Bearer token for every non-admin HTTP write. | +| `OPERATOR_ADMIN_TOKEN` | yes in full mode | Separate bearer token for `/v1/admin/*` writes. | +| `DEX_READ_ONLY` | optional | Set `1` to start without API tokens and reject every state-changing route. | | `CANTON_SYNCHRONIZER` | recommended | Synchronizer id, e.g. `global-domain::1220...`. `submit-and-wait` requires it on a shared synchronizer. | -| `CANTON_DEX_PACKAGE_ID` | recommended | Template-id prefix. A concrete package hash, or `#canton-dex-trading` to resolve by package name. | +| `CANTON_DEX_PACKAGE_ID` | yes | Template-id prefix. Use the vetted concrete package hash, or `#canton-dex-trading` only where package-name resolution is acceptable. | | `CANTON_NETWORK` | optional | Display label surfaced by `/v1/status` (default `canton:devnet`). | -| `CANTON_ALLOC_FACTORY_CID`, `CANTON_SETTLE_FACTORY_CID` | optional | Registry factory CIDs from the bootstrap; set them before the allocation/settlement flows (add/remove liquidity, swaps, order funding) can run. See [Deployment](deployment.md#environment-variables). | +| `CANTON_ALLOC_FACTORY_CID`, `CANTON_SETTLE_FACTORY_CID` | yes in full mode | Asset-admin Registry cid, repeated because it implements both interfaces. | +| `CANTON_LP_ALLOC_FACTORY_CID`, `CANTON_LP_SETTLE_FACTORY_CID` | yes in full mode when LP registrar differs | LP registrar's Registry cid, again repeated for both interfaces. | +| `ALLOWED_ORIGINS` | yes for cross-origin browser access | Exact comma-separated web origins. Unset is default-deny. | +| `DEX_CALLER_JWT_SECRET`, `DEX_CALLER_JWT_AUDIENCE` | optional | Bind private reads and trader-subject writes to `X-Caller-Token.sub` in a multi-user deployment. | +| `DEX_HOSTED_RFQ_RELAY` | optional, default `0` | Custodial RFQ create/cancel/accept under hosted trader authority; enabling it requires caller binding and participant rights for those traders. | The exact variable contract is the header of [`testnet-server.ts`](../../services/operator-backend/src/testnet-server.ts); the full list with defaults is [`services/operator-backend/.env.example`](../../services/operator-backend/.env.example). -## Start the web app +Verify the backend before opening a browser: + +```bash +curl -fsS http://localhost:8080/v1/status +``` + +Do not continue unless the response contains `"synced":true`. HTTP 200 with +`synced:false` means the most recent participant ledger-end probe failed; check +the URL, participant token, and startup/indexer logs. -The dApp reads its network and backend base URL at build time. +## 3. Start the web app + +The dApp reads its public network/backend settings at build time. A production +build deliberately excludes Mock, Direct Canton, and the operator command +relay, so **choose and configure at least one real wallet provider**. This +example enables PartyLayer; replace the wallet ids with adapters supported by +your target network. The alternatives are the dApp SDK gateway +(`VITE_ENABLE_SDK=1`) or WalletConnect (`VITE_WC_PROJECT_ID=...`). ```bash cd app/web @@ -74,16 +152,43 @@ cd app/web VITE_API_BASE="http://localhost:8080" \ VITE_CANTON_NETWORK_ID="canton:testnet" \ VITE_CANTON_SYNCHRONIZER="" \ +VITE_ENABLE_PARTYLAYER=1 \ +VITE_PARTYLAYER_NETWORK="canton:testnet" \ +VITE_PARTYLAYER_WALLET_IDS="console,nightly,send" \ +VITE_DOCS_URL="https://srikanth-bitdynamics.github.io/Canton-Dex-Reference-Implementation/" \ npm run build npm run preview ``` -Open . The header should show the configured network and -the backend status should report `synced: true`. The full frontend variable list -is [`app/web/.env.example`](../../app/web/.env.example). +Open . The backend must allow this exact origin: -## Smoke checks +```bash +export ALLOWED_ORIGINS="http://localhost:4173" +``` + +Set `ALLOWED_ORIGINS` before starting (or restart) the backend. The header +should show the configured network, `/v1/status` should report `synced: true`, +and **Connect Wallet** should list the provider you deliberately enabled. If it +lists no production-capable provider, stop—the browser cannot author the +trader allocations required by the flow. The full frontend variable list is +[`app/web/.env.example`](../../app/web/.env.example). + +### Authorize protected writes in the validator browser + +Open **Admin → API session credentials** and enter short-lived copies of +`DEX_OPERATOR_API_TOKEN` and `OPERATOR_ADMIN_TOKEN`. They are stored only in +that tab's `sessionStorage`, never in the built JavaScript. Trader settle calls +use the operator token; `/v1/admin/*` calls use the admin token. If per-caller +binding is enabled, also enter the caller JWT issued for the connected party. + +This manual token handoff is for a validator/operator acceptance run. A public +multi-user dApp should obtain scoped, expiring credentials from its authenticated +BFF/session service. Do not distribute the venue's long-lived shared tokens to +ordinary traders and do not create `VITE_*` token variables—Vite embeds them in +public assets. + +## 4. Smoke checks ```bash curl -s http://localhost:8080/v1/status | python3 -m json.tool @@ -99,22 +204,62 @@ Expected: - `/v1/pairs` and `/v1/pools` return the on-ledger contracts visible to the operator party. -## Bootstrap a pair and pool +## 5. Create a pair and an unfunded pool -Use the admin endpoints in [operator-guide.md](operator-guide.md): +With the backend still running, use a second terminal that has the same +environment loaded: -- `POST /v1/admin/pairs` -- `POST /v1/admin/pools` +```bash +set -a +source services/operator-backend/.env +set +a + +DEPLOY_SKIP_BUILD=1 \ +DEPLOY_SKIP_UPLOAD=1 \ +DEPLOY_SKIP_BOOTSTRAP=1 \ +DEPLOY_SEED_MARKETS=1 \ +bash scripts/deploy-testnet.sh +``` + +This phase first requires `/v1/status` to succeed. It queries existing pairs and +pools, creates only missing BTC/USDC metadata, and stops on any HTTP failure. It +creates an **unfunded** pool; it does not fabricate reserves or LP holdings. -New pools start in `PS_Unfunded`. The first LP funds the pool through the same -add-liquidity request/allocate/settle flow used for later deposits. +Expected checkpoint: -## Wallet boundary +```bash +curl -fsS http://localhost:8080/v1/pairs +curl -fsS http://localhost:8080/v1/pools +``` -Operator-authority calls go through the backend. Trader-authority calls — such -as authoring allocations for add/remove liquidity, swaps, and order funding — -must go through a wallet or another user-authorized submitter. The backend must -not sign as traders. +The pair should be present, and the pool should report an unfunded/zero-reserve +state. The first LP must next run the same wallet-authorized +request → allocations → settle flow used for later deposits. Exact admin curl +alternatives are in [Operator Guide](operator-guide.md). + +## 6. Wallet and HTTP authorization boundaries + +Operator/LP-authority calls go through the backend. Trader-authority calls — +such as authoring allocations for add/remove liquidity, swaps, and order +funding — must go through a wallet or another user-authorized submitter. The +arbitrary command relay cannot be enabled in the deployed server. + +The RFQ HTTP create/cancel/accept endpoints are a separate custodial exception: +they submit as the RFQ trader and are disabled by default in +`testnet-server.ts`. A deployment that deliberately enables +`DEX_HOSTED_RFQ_RELAY=1` must give its participant user rights for each hosted +trader and configure `DEX_CALLER_JWT_SECRET` so `X-Caller-Token.sub` binds every +request to that trader. Its production UI controls also require +`VITE_ENABLE_HOSTED_RFQ=1`; leaving either side off keeps writes disabled. Do +not describe that mode as self-custodial. + +The browser's follow-up request to the backend is still a protected HTTP write: +it carries the operator API token entered for this tab. That token authorizes +the backend client; it does not replace the wallet's on-ledger authorization. +When per-caller binding is enabled, `X-Caller-Token.sub` must also equal the +trader party named by the request. The dApp sends the same token on its scoped +orders, holdings, balances, trades, and RFQ reads; an admin token may bypass the +party comparison for operational inspection. --- @@ -148,20 +293,22 @@ adapter id. Optional registry overrides are documented in **Validate the flow.** -1. Open the app, click **Connect Wallet**, and select **PartyLayer**. Approve +1. In **Admin → API session credentials**, configure the short-lived operator + token and, when enabled, the connected party's caller JWT. +2. Open the app, click **Connect Wallet**, and select **PartyLayer**. Approve the connection in the wallet and confirm the connected party is the party that owns the test holdings. -2. Confirm holdings load in **Portfolio**. The PartyLayer provider reads +3. Confirm holdings load in **Portfolio**. The PartyLayer provider reads holdings through its `ledgerApi` bridge for the connected party. -3. Run a small trader-authority action, such as: +4. Run a small trader-authority action, such as: - **Trade** → small pool swap - **Pools** → add liquidity or remove liquidity - **Orders** → place a prefunded order -4. Confirm the wallet approval returns an `updateId`. PartyLayer receipts may +5. Confirm the wallet approval returns an `updateId`. PartyLayer receipts may not include created contract ids directly; the operator backend recovers the created `Allocation`, `LiquidityAllocationAcceptance`, or order-funding evidence by reading the committed transaction tree for that `updateId`. -5. Confirm the operator settle step completes and the app refreshes holdings, +6. Confirm the operator settle step completes and the app refreshes holdings, pool reserves, orders, or activity from the backend/indexer. **What to record.** For each wallet adapter tested: diff --git a/docs/guides/using-the-dapp.md b/docs/guides/using-the-dapp.md index e433b9eb..4d4dfe94 100644 --- a/docs/guides/using-the-dapp.md +++ b/docs/guides/using-the-dapp.md @@ -2,8 +2,8 @@ How traders, LPs, and RFQ counterparties use the Canton DEX. Every action below is task-oriented: connect once, then swap, provide liquidity, place an -order, or trade an RFQ block. The one rule that shapes the whole surface — the -dApp never signs as you — is explained in +order, or trade an RFQ block. The external-wallet authority boundary — the dApp +does not hold your key or submit with your ledger authority — is explained in [How a trade is authorised](#how-a-trade-is-authorised). Audience: someone who already has a Canton party id (or is willing to use the @@ -13,40 +13,73 @@ mock wallet locally) and wants to trade. ## Connecting a wallet -The Connect Wallet button in the top bar opens the wallet picker. It -auto-detects the wallets available in this deployment — a dapp-sdk gateway, -injected/announced browser wallets, PartyLayer's catalog — and lists the -remaining providers below them, then routes your choice to its owning provider. -There is no built-in default in production or testnet builds. +The **Connect Wallet** button opens one combined picker. It asks each enabled +integration what it can reach — a dapp-sdk gateway, injected or announced +browser wallets, and PartyLayer's catalog — then adds any enabled +single-provider rows. Picking a row routes the connection back to the adapter +that discovered it. The dApp never connects a wallet automatically. -| Provider | When to use | Required env | +### External-wallet integrations + +These adapters keep user authority in an external wallet. “Production-facing” +means that the architecture has the correct authority boundary; it does not +replace live validation of the particular wallet, participant, packages, and +network you deploy. + +| Picker integration | Current scope | Enable with | |---|---|---| -| **Token Standard V2** | Local dev / testnet only (routes writes through the operator signing relay; dev builds only) | `VITE_API_BASE`, `VITE_CANTON_DEFAULT_PARTY` | -| **WalletConnect** | External CIP-0103 wallets (mobile / hardware) | `VITE_WC_PROJECT_ID` | -| **Direct Canton** | Advanced testnet sessions with a bearer token | `VITE_CANTON_LEDGER_URL`, `VITE_CANTON_AUTH_TOKEN` | -| **Mock Wallet** | Local dev only — DEV builds only | none | - -Once connected, your party id appears in the top bar. The provider persists -across reloads (the session is stored in `localStorage`), and clicking the -connected pill disconnects. - -On the public testnet at `testnet-dex.bitdynamics.cc`, testers are onboarded as -hosted parties on the operator's (BitDynamics) validator, and the traded assets -are issued locally by the deployment's own Token Standard V2 registry. This is an -interim arrangement until the general-purpose validator and wallet tooling (DA -Utilities) supports Token Standard V2, at which point users bring their own party -and V2 assets. See [Non-goals](../concepts/non-goals.md#the-hosted-testnet-is-a-demo-surface-not-a-wallet). +| **Canton wallet (dapp SDK / CIP-0103)** | Composes the Daml commands and delegates authorization and submission to a CIP-0103 wallet. The current capability table marks its update-id discovery path DvP-ready. | `VITE_ENABLE_SDK=1`; optionally set `VITE_WALLET_GATEWAY_URL` and `VITE_WALLET_GATEWAY_NAME` | +| **PartyLayer** | Opens PartyLayer's configured wallet catalog. Its update-id discovery path is implemented, but deliberately marked **unproven** until the selected wallet and deployment pass the live validator plan. | `VITE_ENABLE_PARTYLAYER=1` plus the PartyLayer variables in `.env.example` | +| **WalletConnect** | Connects an external wallet through Reown. The current adapter is marked **no DvP** and explicitly rejects LP add/remove, so enable it only for wallet/intent combinations you have validated. | `VITE_WC_PROJECT_ID` and `VITE_CANTON_NETWORK_ID` | + +When more than one is enabled, the picker adds a single **recommended** badge +using the capability order: dapp SDK (DvP-ready), PartyLayer (unproven pending +live validation), then WalletConnect (currently no-DvP). That badge is only a +UI hint; the user still chooses and approves the connection. If none is +configured in a production build, no development relay is silently substituted. + +### Development-only adapters + +| Picker integration | What it actually proves | Required env | +|---|---|---| +| **Operator Relay (dev only)** | Uses the `token-standard` provider id, but is not a Token Standard wallet. The browser composes commands and `/v1/wallet/submit` submits them with the backend's configured ledger authority. This tests orchestration, not self-custody. | Frontend: `VITE_API_BASE`, `VITE_CANTON_DEFAULT_PARTY`. Backend: `DEX_DEV_WALLET_RELAY=1` and an exact `DEX_DEV_RELAY_PARTIES` allowlist. | +| **Mock Wallet (dev)** | Returns deterministic placeholder contract ids so the UI can be explored. It submits no ledger transaction. | none | + +`CantonDirectProvider` is intentionally **not registered**. Its former path sent +a DEX intent to `/v1/wallet/execute`, but a Canton participant exposes a command +API rather than that DEX-specific endpoint. Shipping a participant bearer token +in browser storage would also be unsafe. Use the dapp SDK, PartyLayer, or +WalletConnect for external authorization; use the operator relay only for an +explicit local development exercise. + +Once connected, the active party appears in the top bar and clicking the +connected pill disconnects. Reconnection and persistence belong to the chosen +external wallet/SDK; do not assume every provider stores or restores the same +session. The development relay stores only its configured demo party and ledger +user id. It never stores a participant JWT. + +For a testnet or public deployment, use a submit-capable external wallet. Do not +compile participant, operator, or admin bearer credentials into the browser +bundle. + +This repository does not provision a public DEX hostname, party faucet, or +browser custody service. An operator deploying it must supply the Canton +participant, parties, assets, API origin, and wallet/onboarding design. The +development-only signing relay is explained under +[Non-goals](../concepts/non-goals.md#the-development-relay-is-not-a-wallet). --- ## How a trade is authorised -Read this once and the pool/order screens follow. **The dApp holds no keys.** A -DvP action is a three-step handshake: the dApp asks the operator for a -Daml-built spec, your wallet signs that spec (locking the named funds), and the -operator settles against it. The wallet carries *your* authority; the operator -carries *its own*. The hosted RFQ screen is a separate relay flow described -below. +Read this once and the pool/order screens follow. With an external-wallet +adapter, **the dApp holds no keys**. A DvP action is a three-step handshake: the +dApp asks the operator for a Daml-built spec, your wallet authorizes that spec +(locking the named funds), and the operator settles against it. The wallet +carries *your* authority; the operator carries *its own*. The development +operator relay does not satisfy this self-custody boundary: its backend submits +using configured ledger rights. The included operator-mediated RFQ screen uses +a separate authority flow described below. ```mermaid sequenceDiagram @@ -175,20 +208,28 @@ Bilateral block trades. You publish a request, whitelisted dealers quote, and you accept one. Acceptance creates a `MatchedTrade` and policy receipt; token funding and settlement are separate steps. +This screen's writes use the explicitly custodial hosted-RFQ mode, not the +connected wallet. Production builds disable its New / Accept / Cancel controls +unless `VITE_ENABLE_HOSTED_RFQ=1`; the backend independently requires +`DEX_HOSTED_RFQ_RELAY=1` and `DEX_CALLER_JWT_SECRET`. Enable both only for a +deployment that deliberately provisions trader `actAs` rights and issues a +short-lived caller JWT bound to the connected party. Reads remain usable while +writes are disabled. + 1. Open **RFQ** → click **+ New RFQ**. 2. Pick pair, side, size, and validity window. Select dealers from the whitelist on the right. -3. Send. The hosted trader screen creates the RFQ. A dealer integration must - observe that contract and create `RfqQuote` contracts; this reference does - not include a dealer quote-entry screen. Visible quotes stream into the - expanded row. +3. Send. The included operator-mediated screen creates the RFQ. A dealer + integration must observe that contract and create `RfqQuote` contracts; + this reference does not include a dealer quote-entry screen. Visible quotes + stream into the expanded row. 4. Keep the default **Operator policy** ranking, or re-sort with the Best price / Earliest / Trusted only buttons. Under policy `v2.0` the ranking chain is **trusted tier first → later expiry first → earlier posting time first → dealer id** as the tiebreaker — price is *not* part of the policy chain; you choose from the policy-ranked candidates. The policy modal shows the exact ranking that was applied. -5. Click **Accept** on the dealer you want. On the hosted demo, the backend +5. Click **Accept** on the dealer you want. In this reference flow, the backend submits `Rfq_Accept` with its configured trader and operator authorities; a `PolicyReceipt` records the ranking applied. This is not a self-custodial wallet approval flow. @@ -200,7 +241,7 @@ Accepted RFQs move to the **Accepted** tab; those that expire with no acceptance (or no quotes) move to **Expired**. The page does not claim that acceptance itself moved balances. The later `MatchedTrade` allocation and settle choices are demonstrated by the Daml tests and operator API, but are not driven by this -hosted RFQ screen. +RFQ screen. --- @@ -233,9 +274,10 @@ dApp passes only the intent verb. | Remove liquidity | `remove-liquidity` | Base-receipt + quote-receipt + LP burn-sender `Allocation`s, settled by [`PoolLiquidityRules_SettleRemoveLiquidity`](../../trading/CantonDex/Dex/PoolLiquidityRules.daml) | | Place order | `place-order` + `fund-order` | [`OrderFundingRequest`](../../trading/CantonDex/Dex/OrderFundingRequest.daml) → funded [`Order`](../../trading/CantonDex/Dex/Order.daml) | -RFQ create, cancel, and accept are not wallet intents in this app. The hosted -RFQ page calls the operator API, whose ledger user must be authorized for the -hosted parties involved. +RFQ create, cancel, and accept are not wallet intents in this app. The +operator-mediated RFQ page calls the operator API, whose ledger user must be +authorized for the configured parties involved. This is an implementation +example, not a public relay service supplied by the repository. The split that makes the "operator can't rewrite your price" guarantee is one pair of choices: the request choice builds a spec and creates nothing, and the diff --git a/docs/guides/validator-test-plan.md b/docs/guides/validator-test-plan.md index 48e70db4..b87abea8 100644 --- a/docs/guides/validator-test-plan.md +++ b/docs/guides/validator-test-plan.md @@ -1,256 +1,333 @@ # Canton Testnet Validator — Live Test Plan -The checklist that signs off a Canton DEX deployment against a live testnet -validator. Work it top to bottom: an offline pre-flight first, then eleven -numbered phases — from DAR upload through Docker Compose — each a set of -checkboxes you tick against a real participant. Where a phase has a headless -script that proves the same thing without a browser, it is linked inline; run it -to corroborate the manual check, not to replace the sign-off. - -## Goals - -1. Confirm each wallet provider enabled for the deployment connects against a - real participant; development-only providers are checked separately. -2. Verify each wallet intent translates correctly into on-ledger Token Standard - V2 transactions. -3. Confirm operator-driven settlement flows use AllocationFactory + - SettlementFactory, and separately verify that hosted RFQ relay parties are - explicitly authorized. -4. Validate indexer + history endpoints reflect on-ledger state. -5. Stress-test idempotency and graceful shutdown. +Use this manual checklist to sign off one deployed DEX environment. It combines +boundaries that the automated suites intentionally test separately: a real +participant, authenticated backend, browser dApp, and real wallet. Record +evidence for each scenario; running a ledger script is useful corroboration, +not a substitute for the browser path. + +## Know what each path proves + +| Path | Includes | Does not prove | +|---|---|---| +| Offline pre-flight | Daml Script, backend tests, dApp tests, backend HTTP smoke | participant compatibility, real wallet, live state | +| Live RFQ test | RFQ service, JSON API, Daml engine | HTTP auth, browser/wallet, token settlement | +| Live AMM round trip | JSON API, Registry.V2, add → quote-bound swap → partial remove DvP with reserve, slice, LP-supply, invariant, and conservation checks | backend HTTP, browser, real wallet | +| Existing-pool probe | JSON API, existing pool, add and swap | backend HTTP, real wallet, remove | +| Matched-trade probe | JSON API, allocations, settlement | RFQ/order matching, AMM, HTTP, real wallet | +| This plan | deployed backend + dApp + wallet + participant | production load, security audit, disaster recovery | + +The exact environment and expected output for every automated path is in the +[Testing reference](../reference/testing.md). + +## Safety and evidence + +All live writes mutate ledger state. Use dedicated test parties and a dedicated +pool; do not seed a production pool. A failed script can leave earlier +transactions committed because there is no cross-transaction rollback. Before +starting, create an evidence directory outside the repository and record: + +- deployment name, Git commit, DAR package id, synchronizer id, and timestamp; +- operator, admin, LP registrar, trader, LP, swapper, and dealer party ids; +- backend and dApp URLs, but never bearer tokens or wallet secrets; +- each command, exit code, run id, relevant contract/update ids, and screenshots; +- cleanup performed after the run. + +Mark each scenario **Pass**, **Fail**, **Blocked**, or **N/A**. A blocked wallet +or auth path is not a pass merely because a raw JSON API script succeeds. ## Prerequisites -- Canton testnet validator with JSON Ledger API reachable (e.g., - `https://canton-testnet.example.com:7575`). -- A bearer JWT issued for `ledger-api-user` with rights to act-as the - operator, lpRegistrar, admin, and demo trader parties. -- The synchronizer id (e.g., `global-domain::1220...`), exported as - `CANTON_SYNCHRONIZER`. -- Docker / Docker Compose installed on the test runner host. -- `dpm` installed — it resolves the pinned SDK 3.5.2 automatically (see - [Local Setup](../getting-started.md#prerequisites)). -- All env vars in `services/operator-backend/.env.example` populated. +- An already-running Canton validator/participant with JSON Ledger API access. +- The current trading DAR and its Token Standard V2 dependencies uploaded. +- Real party ids for every role used by the scenario. +- A ledger JWT with only the rights needed by the backend or probe. +- A synchronizer id and the DEX/Token Standard package ids. +- Node.js 24, npm, DPM with the SDK pinned by `trading/daml.yaml`, curl, and + Docker Compose if Phase 8 is in scope. +- A submit-capable CIP-0103/PartyLayer/WalletConnect wallet supported by the + deployment. The development mock wallet does not prove live submission. + +The backend does **not** load `services/operator-backend/.env` automatically. +Export variables into the process environment (or use your deployment's secret +injection) before `npm start`. At minimum, full live mode needs: + +```text +CANTON_LEDGER_URL CANTON_LEDGER_TOKEN +CANTON_OPERATOR CANTON_LP_REGISTRAR +CANTON_ADMIN CANTON_DEX_PACKAGE_ID +CANTON_ALLOC_FACTORY_CID +CANTON_SETTLE_FACTORY_CID DEX_OPERATOR_API_TOKEN +OPERATOR_ADMIN_TOKEN +``` -## Pre-flight (offline) +When `CANTON_LP_REGISTRAR != CANTON_ADMIN`, full mode also requires +`CANTON_LP_ALLOC_FACTORY_CID` and `CANTON_LP_SETTLE_FACTORY_CID` for the LP +registry. `CANTON_SYNCHRONIZER` is strongly recommended and may be required by +the target participant's routing policy. -Before pointing anything at the validator, prove the build and the API surface -on your own machine — no Canton required. Both scripts exit non-zero on the -first failure, so they gate cleanly. +`CANTON_USER_ID`, `CANTON_NETWORK`, `DB_PATH`, `INDEXER_INTERVAL_MS`, `HOST`, +and `PORT` are optional. `DEX_CALLER_JWT_SECRET` and +`DEX_CALLER_JWT_AUDIENCE` enable per-caller party binding for private reads and +trader-subject writes; if enabled, the dApp also needs a short-lived caller JWT +whose `sub` is the connected party. +`DEX_HOSTED_RFQ_RELAY` remains `0` unless a deliberately custodial RFQ scenario +is in scope; enabling it makes caller binding mandatory. -```bash -bash scripts/run-local-daml-tests.sh # dpm build + the Daml suites -bash scripts/e2e-smoke.sh # boots the dev backend, curls every endpoint -``` +Do not put `DEX_OPERATOR_API_TOKEN`, `OPERATOR_ADMIN_TOKEN`, or the participant +JWT in a `VITE_*` variable. The Admin page can hold short-lived API tokens in +the current tab's `sessionStorage`; a public deployment should replace that +manual test handoff with an authenticated BFF/session issuer. -- [`run-local-daml-tests.sh`](../../scripts/run-local-daml-tests.sh) — builds - `canton-dex-trading` and runs the `trading-tests` suite. Proves the DAR you - are about to upload compiles and its conservation and invariant tests hold. -- [`e2e-smoke.sh`](../../scripts/e2e-smoke.sh) — starts the backend on an - in-memory ledger and curls the read endpoints, a swap quote, the order book, - the price feed, and the admin auth gate, printing `==> All smoke checks - passed`. Proves the HTTP surface answers and that `POST /v1/admin/pairs` is - refused without a bearer token — the same shapes Phases 1–8 exercise against - the validator. +## Phase 0 — Offline pre-flight -## Phase 0 — Build & upload DARs +Run from the repository root after installing dependencies: ```bash -export CANTON_LEDGER_URL=... -export CANTON_LEDGER_TOKEN=... -export CANTON_OPERATOR=... -export CANTON_LP_REGISTRAR=... -export CANTON_ADMIN=... - -./scripts/deploy-testnet.sh +bash scripts/run-local-daml-tests.sh +(cd services/registry-client && npm ci && npm run typecheck) +(cd services/operator-backend && npm ci && npm run typecheck && npm run typecheck:live-scripts && npm test) +(cd app/web && npm ci && npm test && npm run build) +bash scripts/backend-http-smoke.sh ``` Expected: -- `dpm build` succeeds; `trading/.daml/dist/canton-dex-trading-0.1.4.dar` exists. -- DARs upload to participant (HTTP 200 from `/v2/packages`). -- Parties allocated (or pre-existing). -- `scripts/bootstrap-registry.ts` reports each instrument and LP - config as "created" (or "already configured" on a re-run). -- The same run reports `Registry.V2 created` (or "already present") for the - lpRegistrar. Liquidity cannot be allocated until this step has run. -- If a `registryV2` block is configured, a second registry under - `CANTON_ADMIN` plus one line per instrument. Nothing can be minted - until that has run. - -## Phase 1 — Backend boot + +- [ ] Every command exits 0. +- [ ] The Daml runner reports every selected script `ok`. +- [ ] The HTTP smoke ends with `All backend HTTP smoke checks passed`. +- [ ] The smoke is recorded only as an in-memory selected-route check; it does + not prove successful writes or live Canton. + +## Phase 1 — Deployment readiness + +Follow [Run on a Testnet](run-on-testnet.md) for build, upload, party, and +registry bootstrap. Then capture independent evidence: + +- [ ] `canton-dex-trading` resolves to the expected package id. +- [ ] The Token Standard V2 allocation request/instruction packages required by + the live probes resolve to the expected ids. +- [ ] Operator, admin, LP registrar, test traders, LP, swapper, and dealers are + allocated and connected to the intended synchronizer. +- [ ] The asset-admin and (when distinct) LP-registrar `Registry.V2` contracts + plus required base/quote/LP instruments exist. +- [ ] `CANTON_ALLOC_FACTORY_CID` and `CANTON_SETTLE_FACTORY_CID` identify the + intended asset-admin registry, not `PENDING_*` placeholders. +- [ ] With distinct registrars, both `CANTON_LP_*_FACTORY_CID` values identify + the LP registry rather than reusing the asset registry. +- [ ] The test pool is uniquely identified by pair and `POOL_ID` if more than + one pool uses that pair. + +## Phase 2 — Backend and authentication + +With the environment exported, start the live server: ```bash cd services/operator-backend -npm install +npm ci npm start ``` -Expected logs (JSON, one per line): -``` -{"ts":"...","level":"info","msg":"server started","component":"testnet-server","url":"...","ledger":"..."} +In a second terminal: + +```bash +curl -fsS http://127.0.0.1:8080/v1/status +curl -fsS http://127.0.0.1:8080/v1/context +curl -fsS http://127.0.0.1:8080/v1/pools +curl -sS -o /dev/null -w '%{http_code}\n' \ + -X POST -H 'Content-Type: application/json' -d '{}' \ + http://127.0.0.1:8080/v1/admin/pairs ``` -Health checks: -- [ ] `curl http://localhost:8080/v1/status` returns `{network, slot, synced:true}` -- [ ] `curl http://localhost:8080/v1/context` returns operator/admin/lpRegistrar + factory CIDs -- [ ] `curl http://localhost:8080/v1/pools` returns `[]` (no pools yet) or seeded pools +- [ ] Startup logs identify the expected ledger URL, parties, network, DB, and + `mode:"full"`. +- [ ] Status reports `synced:true`; context contains the expected parties and + factory CIDs. +- [ ] The unauthenticated admin write returns 401. +- [ ] A request with a wrong admin token returns 401. +- [ ] A request with a wrong operator token to a non-admin write returns 401. +- [ ] If caller binding is enabled, a missing/invalid `X-Caller-Token` returns + 401, while a valid token for a different party returns 403. +- [ ] The same caller-binding check covers scoped orders, holdings, balances, + trades, RFQ history, and RFQ/quote reads; an admin token can inspect them. +- [ ] Read-only mode, if tested, was explicitly started with `DEX_READ_ONLY=1` + and is not signed off for write scenarios. + +Use the payload examples in [HTTP API](../reference/http-api.md) for an +authenticated write; do not use `{}` as a success-case payload. -## Phase 2 — Frontend boot +## Phase 3 — dApp and wallet connection + +Create `app/web/.env.local` with only public deployment configuration. For a +local Vite validation run: ```bash cd app/web -cp .env.example .env.local -# Set: -# VITE_API_BASE=http://localhost:8080 -# VITE_CANTON_LEDGER_URL=$CANTON_LEDGER_URL -# VITE_CANTON_AUTH_TOKEN=$CANTON_LEDGER_TOKEN -# VITE_CANTON_NETWORK_ID=canton:testnet -# VITE_WC_PROJECT_ID=... (optional, for WalletConnect) -npm install +npm ci npm run dev ``` -Open . Smoke checks: -- [ ] All 6 pages render without crash (TradePage, Pools, Orders, RFQ, - Portfolio, Admin) -- [ ] Error boundaries do NOT trigger (no red banners) -- [ ] Connect Wallet menu shows: Token Standard, WalletConnect (if - configured), Mock (DEV only — should be absent in prod build) - -## Phase 3 — Wallet provider validation - -### 3.1 Token Standard provider -- [ ] Click "Connect Wallet" → Token Standard -- [ ] Connection succeeds; party id displayed -- [ ] Reload page; session persists, no re-prompt -- [ ] Click Disconnect; localStorage `canton-dex:token-standard:session` cleared - -### 3.2 WalletConnect (if `VITE_WC_PROJECT_ID` set) -- [ ] Connect → QR modal opens, scannable -- [ ] After mobile wallet pairing, primary party returned -- [ ] Cancel during pairing surfaces error message, NOT a stuck state - -### 3.3 Direct Canton (advanced fallback) -- [ ] With `VITE_CANTON_LEDGER_URL` + `VITE_CANTON_AUTH_TOKEN` set, - Direct Canton appears in the menu -- [ ] Connect succeeds via `/v2/users/current` - -## Phase 4 — Operator admin operations - -Requires `OPERATOR_ADMIN_TOKEN`. - -- [ ] Create new pair (DOGE/USDC) via Admin page → 200, pair listed - via `GET /v1/pairs` -- [ ] Toggle pair active/inactive → state reflected on next GET -- [ ] Update fee model → new fees take effect -- [ ] Create pool for DOGE/USDC → 200, pool listed via `GET /v1/pools` -- [ ] Admin write WITHOUT bearer token → 401 with structured error - envelope - -## Phase 5 — Trader flows - -Three scripts drive these flows against a live participant without a browser -wallet — run them to corroborate the manual checks below, each proving one seam: - -- [`localnet-dvp-e2e.ts`](../../scripts/localnet-dvp-e2e.ts) - (`npm run localnet:dvp-e2e --prefix services/operator-backend`) — stands in - for the trader's CIP-0103 wallet, authoring the three allocations for each - DvP and settling. Proves the operator two-call add → swap → remove round-trip - (§5.3–5.5) and asserts the on-ledger reserves and LP supply. -- [`seed-testnet-pool.ts`](../../scripts/seed-testnet-pool.ts) - (`npm run testnet:seed-pool --prefix services/operator-backend`) — mints, adds - liquidity, and swaps against an *existing* live pool. Proves a swap moved the - reserves by exactly the constant-product amount and that `x·y` did not - decrease (§5.3). -- [`testnet-v2registry-trade.ts`](../../scripts/testnet-v2registry-trade.ts) — - posts a `MatchedTrade`, runs the V2 allocation accept on both sides, and - settles via `SettleBatch`. Proves matched-trade settlement through the - registry acting as allocation + settlement factory (§5.2). - -### 5.1 Place order -- [ ] Submit a buy order for BTC/USDC at limit price < current ask -- [ ] Wallet intent translates to OrderFundingRequest creation -- [ ] Operator backend observes and binds via OrderFundingRequest_Bind -- [ ] Order appears in `GET /v1/orders?trader=...` - -### 5.2 Order matching -- [ ] Place a crossing sell order (price ≤ existing buy) -- [ ] `POST /v1/orders/match {base,quote}` returns 1 match -- [ ] After settle, both orders archived (or remaining qty updated for partial) -- [ ] The settled fill appears in `GET /v1/trades` (a `SettledTrade` row, with - `dealer` null and both parties across `trader` / `counterparty`) - -### 5.3 Pool swap -- [ ] Quote: `POST /v1/swaps/quote` returns positive output for 0.01 BTC -- [ ] Submit swap intent through wallet → on-ledger PoolRules_Swap exercised -- [ ] Pool reserves update; swap appears in `GET /v1/swaps` - -### 5.4 Add liquidity (two-call DvP) -- [ ] `POST /v1/pools/add-liquidity/request` → operator creates a - LiquidityAllocationRequest -- [ ] Wallet authors the base-deposit, quote-deposit, and LP-receipt - allocations via AllocationFactory_Allocate -- [ ] `POST /v1/pools/add-liquidity/settle` → operator + lpRegistrar - settle (PoolLiquidityRules_SettleAddLiquidity); funds enter the pool and - LP tokens are minted to the LP atomically -- [ ] LP tokens minted (visible in Portfolio page LP section) -- [ ] Pool reserves grow proportionally - -### 5.5 Remove liquidity (two-call DvP) -- [ ] `POST /v1/pools/remove-liquidity/request` → operator creates a - LiquidityAllocationRequest -- [ ] Wallet authors the holder's base-receipt + quote-receipt + LP - burn-sender allocations -- [ ] `POST /v1/pools/remove-liquidity/settle` → operator + lpRegistrar - settle (PoolLiquidityRules_SettleRemoveLiquidity); base + quote are - delivered to the holder and the LP tokens burn to the burn - account atomically - -### 5.6 RFQ -- [ ] Trader creates RFQ via `POST /v1/rfq` -- [ ] Dealer posts a quote (separate wallet session) -- [ ] Trader+operator co-sign accept via `POST /v1/rfq/accept` -- [ ] PolicyReceipt returned and matches verifyReceipt() -- [ ] After expiry, `sweepExpired` cancels stale RFQs (verify via - logs after manually setting an RFQ's expiry in the past) - -## Phase 6 — Resilience - -- [ ] Send SIGTERM to backend; logs show graceful shutdown - (indexer stop → http close → db close) -- [ ] Restart backend; indexer resumes from last persisted offset -- [ ] Crash backend mid-submission; idempotency table prevents - duplicate commit on restart -- [ ] Submit malformed JSON to `/v1/swaps/quote` → 400 with `code: bad_request` -- [ ] Submit oversized body (>1 MiB) → 413 with `code: payload_too_large` - -## Phase 7 — Observability - -- [ ] Every request log line has `requestId`, `method`, `path`, - `status`, `durationMs` -- [ ] Every error log line goes to stderr (verify by redirecting) -- [ ] `X-Request-Id` header echoed back when supplied; generated otherwise - -## Phase 8 — Frontend validation - -- [ ] Error boundary triggered by throwing in a child component - surfaces the retry card without taking down the page shell -- [ ] Disconnect mid-transaction shows clear error message -- [ ] Page refresh after disconnect → no auto-reconnect, clean - "Connect Wallet" state - -## Phase 9 — Docker compose deployment - -- [ ] `docker-compose up` brings both services up -- [ ] Frontend at port 80 proxies `/v1/*` to backend -- [ ] `docker-compose restart backend` does not lose indexer state - (volume persistence) -- [ ] CORS narrowed when `ALLOWED_ORIGINS` set - -## Sign-off - -Mark this plan ✅ once every checkbox above is verified against a -real Canton testnet validator. +Open and validate all six routes: Trade, Pools, Orders, +RFQ, Portfolio, and Admin. + +- [ ] No route triggers its error boundary. +- [ ] The intended production-capable wallet is shown and connects. +- [ ] The connected party is the dedicated test trader. +- [ ] Reload and disconnect behave as documented by that provider. +- [ ] A cancelled/rejected wallet approval returns the UI to a usable state. +- [ ] The mock and dev-only relay/direct providers are not treated as evidence + of production wallet compatibility. + +For this controlled validation only, enter short-lived operator/admin API +tokens in **Admin → API session credentials**. If per-caller binding is enabled, +enter the test trader's scoped caller JWT too. + +- [ ] Browser network requests attach the admin token only to `/v1/admin/*` + writes and the operator token only to other writes. +- [ ] Credentials disappear when the tab session is cleared. +- [ ] No token appears in screenshots, console output, committed files, or the + built JavaScript bundle. + +## Phase 4 — Automated live corroboration + +Use a throwaway LocalNet for self-contained probes. Use the shared validator +only with dedicated parties/pools and explicit approval to leave test state. +Export each script's full environment from the [Testing +reference](../reference/testing.md#live-canton-probes), then run from +`services/operator-backend`: + +```bash +CANTON_LIVE_RFQ=1 npm run test:live:rfq +npm run live:roundtrip +npm run testnet:seed-pool +npm run live:matched-trade +``` + +- [ ] RFQ test checks exact RFQ/quote/trade CIDs and the stored policy receipt. +- [ ] AMM round trip prints its unique run id and passes exact add, swap, and + partial-remove reserve/holding/slice/LP assertions plus the documented + invariant and conservation checks. +- [ ] Existing-pool probe passes add/swap reserve, holding, slice, and invariant + assertions against the selected dedicated pool. +- [ ] Matched-trade probe passes the sender/receiver holding assertions. +- [ ] Results are mapped only to the boundaries in the table at the top; in + particular, the direct JSON API round trip is not cited as evidence for + the backend HTTP, browser, or real-wallet transport. + +## Phase 5 — Browser trader and admin flows + +For each scenario, capture the wallet approval, backend request id, resulting +ledger update/contract ids, and the refreshed UI state. Use small test amounts. + +### Admin + +- [ ] Create or select a dedicated test pair and pool with the admin credential. +- [ ] Update its supported fee/trading configuration and observe it on the next + GET. +- [ ] Repeat one write without the admin credential and observe 401. + +### Order lifecycle + +- [ ] Place a non-crossing order; the wallet signs the trader-authorized + funding transaction and the order appears in `/v1/orders`. +- [ ] Cancel it and verify the active contract disappears. +- [ ] Place crossing buy/sell orders using two test traders, run the match route, + and verify the resulting fill/history and balances. + +### Swap + +- [ ] Obtain a positive quote for a small input. +- [ ] Approve and submit the wallet intent, then verify input/output balances and + the exact reserve transition. +- [ ] Verify the swap/history projection after at least one indexer interval. + +### Add liquidity + +- [ ] The request route creates one `LiquidityAllocationRequest`. +- [ ] The wallet authors base-deposit, quote-deposit, and LP-receipt + allocations. +- [ ] The settle route consumes the request/allocations atomically; reserves and + LP supply increase by the expected values and the LP holding is visible. + +### Remove liquidity + +- [ ] The request route creates a remove `LiquidityAllocationRequest`. +- [ ] The wallet authors base-receipt, quote-receipt, and LP-burn allocations. +- [ ] Settle reduces reserves and LP supply by the expected values and delivers + base/quote holdings to the LP. + +The automated AMM round trip corroborates remove-liquidity directly through +the JSON Ledger API. This manual scenario is still required to establish the +different boundary under review here: browser state, backend authorization, +wallet approval/transport, and the deployed party-rights configuration. + +### RFQ + +- [ ] Trader creates an RFQ and a whitelisted dealer posts a quote from a + separate authorized session. +- [ ] Trader/operator accept returns a verifying `PolicyReceipt`; the exact + receipt is stored on the resulting `MatchedTrade`. +- [ ] Fund and settle the matched trade, then verify both assets moved. The live + RFQ automated test stops before this step and cannot substitute for it. +- [ ] Create an already-expired fixture through an approved test setup, invoke + the deployment's RFQ sweep job, and verify the operator archives it. The + reference exposes `sweepExpired` as a service method but does not include + a standalone scheduler/CLI, so mark this **Blocked** if the deployment has + no job entrypoint. + +## Phase 6 — Failure handling and observability + +- [ ] Malformed JSON returns 400 with `code:"bad_request"` and a request id. +- [ ] A body over 1 MiB returns 413 with `code:"payload_too_large"`. +- [ ] A supplied `X-Request-Id` is echoed; otherwise the server creates one. +- [ ] Request logs contain request id, method, path, status, and duration. +- [ ] Ledger/authorization failures preserve a useful structured error without + leaking JWTs or API tokens. +- [ ] Send SIGTERM to the backend process, observe graceful HTTP/indexer/DB + shutdown, restart with the same `DB_PATH`, and verify status/history. + +Do not claim crash/idempotency recovery from the restart check alone. A +mid-submission fault requires a controlled fault-injection harness and evidence +that only one ledger update committed; mark it **Blocked** if that harness is +not available. + +## Phase 7 — Frontend failure states + +- [ ] Disconnect/reject during a transaction shows a clear retryable error. +- [ ] Refresh after disconnect returns to a clean Connect Wallet state. +- [ ] Stop the backend temporarily; each page shows a bounded error state and + recovers after the backend restarts. +- [ ] An authorization failure is distinguishable from wallet rejection and + from ledger validation failure. + +## Phase 8 — Docker Compose, if deployed that way + +Run from the repository root with all Compose variables exported: + +```bash +docker compose up --build +``` + +- [ ] Backend starts in the intended full/read-only mode; neither API token is + blank in full mode. +- [ ] Frontend on port 80 proxies `/v1/*` to the backend. +- [ ] `docker compose restart backend` retains indexer state in the named + volume. +- [ ] `ALLOWED_ORIGINS` is restricted to the deployed dApp origin. +- [ ] Secrets are injected at runtime and absent from the frontend image/bundle. + +## Cleanup and sign-off + +- [ ] Cancel every still-cancellable RFQ, quote, order, allocation request, and + matched trade created by the test. +- [ ] Record contracts that cannot be cleaned up safely (for example the + accepted RFQ test's unmatched trade) and the owner responsible. +- [ ] Stop/remove the throwaway LocalNet. For shared testnet state, do not delete + or mutate contracts outside the recorded run ids and dedicated pool. +- [ ] Remove tokens from `sessionStorage`, shell history where applicable, and + temporary environment files; revoke short-lived credentials. +- [ ] Attach the Pass/Fail/Blocked/N/A matrix and evidence links to the release + record. Any required **Fail** or **Blocked** scenario prevents sign-off. --- diff --git a/docs/reference/allocation-surface.md b/docs/reference/allocation-surface.md index 4cedc8bc..3ae33a19 100644 --- a/docs/reference/allocation-surface.md +++ b/docs/reference/allocation-surface.md @@ -158,7 +158,7 @@ transaction and references the returned allocation. There is no separate funding-mutation step between settlement and order roll-forward. `testOrderRemainderFundingArithmetic` in -[`trading-tests/CantonDex/Tests/EndToEndTests.daml`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) +[`OrderWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/OrderWorkflowTests.daml) checks the matcher-side residual calculation. The real-holding conservation checks are in [`RegistryConservationTests.daml`](../../trading-tests/CantonDex/Tests/RegistryConservationTests.daml), diff --git a/docs/reference/daml-proof-map.md b/docs/reference/daml-proof-map.md new file mode 100644 index 00000000..1bcd94b0 --- /dev/null +++ b/docs/reference/daml-proof-map.md @@ -0,0 +1,132 @@ +# Daml design-to-test proof map + +Use this page when a design statement says “proven by.” Each row links the +on-ledger choice to the smallest Daml Script that demonstrates the stated +property and gives a focused command. Run commands from `trading-tests/`. + +```bash +cd trading-tests +dpm test -p +``` + +## Read the fixture before trusting the claim + +The repository has two kinds of Daml fixture: + +- The four `*WorkflowTests.daml` suites use `MockRegistry`. They prove DEX choice choreography, + authority, contract consumption/recreation, and the allocation specification + passed to settlement. They do **not** prove real holding balances. +- Rows that claim real value movement point to suites using + `CantonDex.Registry.V2` holdings, including `PoolLiquidityRulesTests.daml`, + `PoolRoundingTests.daml`, `PoolStateInvariantTests.daml`, + `RealRegistryDvpTests.daml`, `RegistryConservationTests.daml`, + `RfqSettlementTests.daml`, and the real-value lifecycle tests. These can prove + locked backing, exact balance movement, release, and conservation in Daml Script. + +Neither fixture starts a Canton participant or drives the HTTP API, browser, or +external wallet. Those are separate integration proofs. + +## Pair listing metadata + +Source: [`DexPair`](../../trading/CantonDex/Dex/DexPair.daml) and its +operator-controlled update choices. + +| Claim | Executable proof | Focused command | +|---|---|---| +| Fee model, active flag, trading mode, and readers recreate one successor listing and preserve unrelated fields. | [`testDexPairLifecycleUpdates`](../../trading-tests/CantonDex/Tests/DexPairTests.daml) | `dpm test -p testDexPairLifecycleUpdates` | +| The registry admin observes the pair but cannot exercise the operator-controlled update. | [`testDexPairUpdatesRequireOperator`](../../trading-tests/CantonDex/Tests/DexPairTests.daml) | `dpm test -p testDexPairUpdatesRequireOperator` | +| Maker/taker fee counters accumulate the configured arithmetic; no test claims those counters move or collect assets. | [`testDexPairRecordsMatchedTradeFees`](../../trading-tests/CantonDex/Tests/DexPairTests.daml) | `dpm test -p testDexPairRecordsMatchedTradeFees` | + +`active` and `tradingMode` are not settlement gates in this reference. That is +a dependency fact visible in the source: [`PoolRules`](../../trading/CantonDex/Dex/PoolRules.daml) +and [`OrderMatchExecution`](../../trading/CantonDex/Dex/OrderMatchExecution.daml) +do not fetch or accept a `DexPair` contract. The tests above intentionally prove +listing behavior only; do not cite them as pause/enforcement tests. + +## AMM pool + +Core sources: + +- pricing and ratio math — [`ratioMatchedDeposit`](../../trading/CantonDex/Dex/PoolModel.daml) + and [`constantProductOut`](../../trading/CantonDex/Dex/PoolModel.daml); +- exact quote construction and swap — [`PoolRules_RequestSwap`](../../trading/CantonDex/Dex/PoolRules.daml), + [`PoolRules_Swap`](../../trading/CantonDex/Dex/PoolRules.daml); +- add/remove DvP — [`PoolLiquidityRules_SettleAddLiquidity`](../../trading/CantonDex/Dex/PoolLiquidityRules.daml), + [`PoolLiquidityRules_SettleRemoveLiquidity`](../../trading/CantonDex/Dex/PoolLiquidityRules.daml); +- real allocation and batch implementation — [`AllocationFactory`](../../trading/CantonDex/Registry/V2.daml), + [`SettlementFactory`](../../trading/CantonDex/Registry/V2.daml). + +| Claim | Executable proof | Focused command | +|---|---|---| +| Swap output rounds down and does not reduce `x*y` through decimal overpayment. | [`testSwapOutputRoundsDownToKeepConstantProduct`](../../trading-tests/CantonDex/Tests/PoolRoundingTests.daml) | `dpm test -p testSwapOutputRoundsDownToKeepConstantProduct` | +| The Daml-built request specification reaches `PoolRules_Swap` and its mock settlement choice against the same bound state and slices. | [`testPoolSwapViaRequestSwap`](../../trading-tests/CantonDex/Tests/PoolWorkflowTests.daml) | `dpm test -p testPoolSwapViaRequestSwap` | +| A context-requiring V2 registry consumes actual trader backing and creates the output holding; a changed signed output is rejected. | [`testRealRegistryDvpSwapSettles`](../../trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml) | `dpm test -p testRealRegistryDvpSwapSettles` | +| A stale add-liquidity quote cannot settle against a successor pool state. | [`testStaleQuoteRejected`](../../trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml) | `dpm test -p testStaleQuoteRejected` | +| Add liquidity moves real base/quote backing and mints real LP holdings in one DvP flow. | [`testDvpAddLiquidity`](../../trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml) | `dpm test -p testDvpAddLiquidity` | +| Off-ratio excess is returned rather than donated or used to mint shares. | [`testDvpAddOffRatioRefundsExcess`](../../trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml) | `dpm test -p testDvpAddOffRatioRefundsExcess` | +| Complete LP redemption drains multiple slices, returns real assets, burns every LP holding, and changes state to `Unfunded`. | [`testDvpMultiSliceRemove`](../../trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml) | `dpm test -p testDvpMultiSliceRemove` | +| Pool initialization, pause, and resume are actual state transitions; pause rejects a swap and resume preserves reserve and LP-supply accounting. | [`testPoolFullLifecycle`](../../trading-tests/CantonDex/Tests/PoolWorkflowTests.daml) | `dpm test -p testPoolFullLifecycle` | +| Aggregate reserves equal the active slice sums after add, swap, and complete remove. | [`testReconcileAfterAddSwapRemove`](../../trading-tests/CantonDex/Tests/PoolStateInvariantTests.daml) | `dpm test -p testReconcileAfterAddSwapRemove` | +| Liquidity settlement requires both operator and LP registrar authority. | [`testSettleRequiresCoControl`](../../trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml) | `dpm test -p testSettleRequiresCoControl` | + +## Resting orders + +Source path: [`OrderFundingRequest_Bind`](../../trading/CantonDex/Dex/OrderFundingRequest.daml) +→ [`Order_Fund`](../../trading/CantonDex/Dex/Order.daml) → +[`OrderMatchExecution_Execute`](../../trading/CantonDex/Dex/OrderMatchExecution.daml) +or [`Order_Cancel`](../../trading/CantonDex/Dex/Order.daml). + +| Claim | Executable proof | Focused command | +|---|---|---| +| Trader intent becomes an operator-bound pending order, then a trader-authored allocation is attached to it. | [`testOrderFundingFlow`](../../trading-tests/CantonDex/Tests/OrderWorkflowTests.daml) | `dpm test -p testOrderFundingFlow` | +| A match outside either signed limit fails. | [`testOrderMatchEnforcesLimitPrice`](../../trading-tests/CantonDex/Tests/OrderWorkflowTests.daml) | `dpm test -p testOrderMatchEnforcesLimitPrice` | +| Settlement and both partial-order roll-forwards occur atomically. | [`testOrderMatchRollsOrdersForwardAtomically`](../../trading-tests/CantonDex/Tests/OrderWorkflowTests.daml) | `dpm test -p testOrderMatchRollsOrdersForwardAtomically` | +| A real partial fill can spend only the funding budget carried into its next allocation iteration. | [`testPartialFillUsesRolledFundingBudget`](../../trading-tests/CantonDex/Tests/RegistryConservationTests.daml) | `dpm test -p testPartialFillUsesRolledFundingBudget` | +| Cancelling a funded order consumes the real allocation and returns its locked holding unlocked. | [`testOrderCancelReleasesRealFunding`](../../trading-tests/CantonDex/Tests/LifecycleChoiceTests.daml) | `dpm test -p testOrderCancelReleasesRealFunding` | +| Trader controls pre-bind cancel; operator controls reject. | [`testOrderFundingRequestCancelAndRejectAuthority`](../../trading-tests/CantonDex/Tests/LifecycleChoiceTests.daml) | `dpm test -p testOrderFundingRequestCancelAndRejectAuthority` | +| Operator can abort an unexecuted match proposal without touching referenced orders or allocations. | [`testOrderMatchExecutionAbort`](../../trading-tests/CantonDex/Tests/LifecycleChoiceTests.daml) | `dpm test -p testOrderMatchExecutionAbort` | + +## RFQ and OTC + +Source path: [`Rfq_Accept`](../../trading/CantonDex/Dex/Rfq.daml) creates a +`MatchedTrade`; [`MatchedTrade_RequestAllocations`](../../trading/CantonDex/Dex/MatchedTrade.daml) +and [`MatchedTrade_Settle`](../../trading/CantonDex/Dex/MatchedTrade.daml) +move its value, while [`MatchedTrade_Cancel`](../../trading/CantonDex/Dex/MatchedTrade.daml) +is the abandoned-trade exit. + +| Claim | Executable proof | Focused command | +|---|---|---| +| RFQ accept ranks quotes and records a policy receipt on the resulting trade; it does not move balances yet. | [`testRfqAcceptProducesMatchedTradeWithReceipt`](../../trading-tests/CantonDex/Tests/TradeWorkflowTests.daml) | `dpm test -p testRfqAcceptProducesMatchedTradeWithReceipt` | +| Accepted RFQ terms settle against real holdings with exact balance deltas and no stranded locks. | [`testRfqBuySettlesAgainstRealHoldings`](../../trading-tests/CantonDex/Tests/RfqSettlementTests.daml) | `dpm test -p testRfqBuySettlesAgainstRealHoldings` | +| The inherited RFQ deadline blocks later settlement; the failed transaction leaves the allocations and locked funds unchanged. | [`testExpiryBetweenAcceptAndSettleBlocksTheSettle`](../../trading-tests/CantonDex/Tests/RfqSettlementTests.daml) | `dpm test -p testExpiryBetweenAcceptAndSettleBlocksTheSettle` | +| A cross-admin OTC trade uses per-admin batches but remains one atomic Daml transaction. | [`testMatchedTradeSettlesPerAdminLegSubsets`](../../trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml) | `dpm test -p testMatchedTradeSettlesPerAdminLegSubsets` | +| Cancelling a proposed trade archives its requests/allocations and returns real sender backing without executing the leg. | [`testMatchedTradeCancelReleasesRealFunding`](../../trading-tests/CantonDex/Tests/LifecycleChoiceTests.daml) | `dpm test -p testMatchedTradeCancelReleasesRealFunding` | +| Trader controls RFQ cancellation; dealer controls quote withdrawal. | [`testRfqCancelAndQuoteWithdrawAuthority`](../../trading-tests/CantonDex/Tests/LifecycleChoiceTests.daml) | `dpm test -p testRfqCancelAndQuoteWithdrawAuthority` | + +## Token Standard safety properties used by every surface + +| Claim | Executable proof | Focused command | +|---|---|---| +| Executor-supplied extra legs cannot exceed locked allocation backing. | [`testExtraLegBeyondBackingRejected`](../../trading-tests/CantonDex/Tests/RegistryConservationTests.daml) | `dpm test -p testExtraLegBeyondBackingRejected` | +| Roll-forward carries actual locked backing, not an accounting-only budget. | [`testRollForwardCarriesLockedBacking`](../../trading-tests/CantonDex/Tests/RegistryConservationTests.daml) | `dpm test -p testRollForwardCarriesLockedBacking` | +| An uncommitted allocation is withdrawable only by its authorizer. | [`testUncommittedAllocationWithdrawsOnlyAsAuthorizer`](../../trading-tests/CantonDex/Tests/RegistryConservationTests.daml) | `dpm test -p testUncommittedAllocationWithdrawsOnlyAsAuthorizer` | +| A committed allocation is authorizer-withdrawable after its deadline, but not before. | [`testCommittedAllocationWithdrawsOnlyAfterDeadline`](../../trading-tests/CantonDex/Tests/RegistryConservationTests.daml) | `dpm test -p testCommittedAllocationWithdrawsOnlyAfterDeadline` | +| A deadline-free committed pool allocation is not unilaterally withdrawable. | [`testCommittedAllocationWithoutDeadlineCannotBeWithdrawn`](../../trading-tests/CantonDex/Tests/RegistryConservationTests.daml) | `dpm test -p testCommittedAllocationWithoutDeadlineCannotBeWithdrawn` | + +## Run by module, then run everything + +```bash +cd trading-tests +dpm test --files CantonDex/Tests/DexPairTests.daml +dpm test --files CantonDex/Tests/LifecycleChoiceTests.daml +dpm test --files CantonDex/Tests/PoolLiquidityRulesTests.daml +dpm test --files CantonDex/Tests/RealRegistryDvpTests.daml +dpm test +``` + +The final command is the release check. A focused test explains one invariant; +the complete suite catches interactions between workflows. + +**Where to read next:** [Builder guide](../guides/builder-guide.md) · +[Workflow design](../concepts/workflows.md) · +[Testing reference](testing.md) diff --git a/docs/reference/ecosystem-feedback.md b/docs/reference/ecosystem-feedback.md index d6b6d08d..b85ff47f 100644 --- a/docs/reference/ecosystem-feedback.md +++ b/docs/reference/ecosystem-feedback.md @@ -4,57 +4,52 @@ This page records how the reference implementation was evaluated by external parties, what they found, and what changed as a result. It is maintained as the single summary of that loop. +> **Status of the old hosted integration.** The evaluation below used a +> separately operated deployment during a historical feedback round. This +> repository does **not** provision a public hostname, public party faucet, or +> `/v1/testnet/*` API, and it does not promise that the old deployment remains +> available. Treat the linked reports as provenance for the feedback—not as +> current setup instructions. The API implemented in this tree is listed in +> [HTTP API](http-api.md); run it against a participant you control by following +> the [local live-ledger guide](../guides/localnet.md). + ## External integration (reuse proof point) -The reference DEX is integrated as an adapter in +During that feedback round, the reference DEX was integrated as an adapter in [**canton-trading-toolkit**](https://github.com/olevasyliev/canton-trading-toolkit), an independent, open-source, venue-agnostic trading client for the Canton -Network. The toolkit is live-validated on mainnet against an unrelated spot AMM -(Cantex) and connects to a perpetuals testnet (Ekiden); this DEX is a third -adapter (`DexRefAdapter`). The same client code that -trades on an unrelated mainnet venue drives quotes, swaps, orders, matching, RFQ -and liquidity on this one, entirely through the hosted testnet routes: the only -path open to a party with no wallet of its own. - -The integration is reproducible from outside with no operator credentials: - -``` -git clone https://github.com/olevasyliev/canton-trading-toolkit -cd canton-trading-toolkit && pip install -e . -PYTHONPATH=src python3 scripts/dexref_testnet_report.py # reads only -PYTHONPATH=src python3 scripts/dexref_testnet_report.py --execute # trades -``` - -The client allocates its own parties from the public faucet and exercises every -flow against `https://testnet-dex.bitdynamics.cc`. An external developer built a -working integration against the hosted testnet, from the public repository, and -published it. +Network. Its `DexRefAdapter` supplied useful independent feedback on quotes, +swaps, orders, matching, RFQ, and liquidity. The adapter and the reports are +external artifacts. Their deployment wrapper—including any party provisioning, +rate limits, or convenience endpoints—is not implemented by this repository. ## Evaluation and feedback -The integrator ran six rounds against the hosted testnet between 2026-07-27 and -2026-07-29, plus an earlier round against the repository's local demo mode. Each -round is a scripted run of dozens of assertions measured through the public -routes. The reports are public: +The integrator reported six rounds against the separately operated deployment +between 2026-07-27 and 2026-07-29, plus an earlier round against the +repository's local demo mode. The reports are public: - Hosted testnet report: [srikanth-bitdynamics/Canton-Dex-Reference-Implementation#126](https://github.com/srikanth-bitdynamics/Canton-Dex-Reference-Implementation/issues/126) - Local demo mode report: [canton-dev-fund#312 comment](https://github.com/canton-foundation/canton-dev-fund/issues/312#issuecomment-5044174855) -Because the integrator has no privileged access, the findings are exactly what any -external builder would hit. +The reports document what that external client observed at the time. The +regression tests named below are the durable evidence for behavior in the +current repository. ## Findings and resulting changes -Every finding from the six rounds was addressed. They fall into a few themes. -Each theme below closes with the test that pins the fix. +The findings that changed this repository fall into a few themes. Changes made +only in the old external deployment wrapper are not presented as current API +features. Each theme below closes with the checked-in test that pins the fix. ### Amounts are served at ledger precision Amounts must reach the client as exact decimal strings at ledger scale, never -re-floated through IEEE-754. The fills feed routes deltas directly through -`parseFloat().toFixed`; `/v1/swaps` serves the exact stored strings; and +re-floated through IEEE-754. The indexer derives reserve deltas with the +fixed-point decimal module, stores them as strings, and `/v1/swaps` serves those +exact strings. In addition, `/v1/instruments` reports each instrument's `decimals` so a client can learn scale from the API. Existing projection rows can be reindexed after an upgrade. @@ -91,7 +86,7 @@ recorded), and Two fixes concern funding and custody. Funding an order locks only what the order needs and returns the change, so a party can place more than one order at a time. An off-ratio liquidity add refunds the unmatched remainder, and the -hosted receipt reports settled amounts rather than echoing requested amounts. +settlement result reports settled amounts rather than echoing requested amounts. Proven by [`normalize-funding.test.ts`](../../app/web/src/__tests__/normalize-funding.test.ts) @@ -101,15 +96,15 @@ split handed to the wallet) and `testDvpAddOffRatioRefundsExcess` in (the unmatched leg is refunded in the same settlement, never reaching the reserves). -### The hosted routes are the only path in +### External clients need a complete, documented API -For a walletless integrator the hosted routes are the whole surface, so a gap in -them blocks external evaluation entirely. RFQ gained a hosted cancel, so a round -trip has an exit other than expiry. Order matching gained a hosted testnet -trigger (`POST /v1/testnet/match`) so matching and its atomic settlement can be -verified from outside. `/v1/swaps` accepts `?kind=` so liquidity events, not -just swaps, are readable. The `/v1/testnet/*` surface and the faucet's per-IP -party quota are documented with their consequences. +The feedback exposed missing operations in the external deployment wrapper. +The corresponding capabilities that remain in this repository use the normal +operator API: `POST /v1/rfq/:cid/cancel`, operator-authenticated +`POST /v1/orders/match`, and `GET /v1/swaps?kind=`. The first two are writes and +therefore require the appropriate operator and caller authority described in +[HTTP API](http-api.md#authorization). There is no +`/v1/testnet/*` namespace or public faucet in this tree. Proven by [`swaps-kind-filter.test.ts`](../../services/operator-backend/test/swaps-kind-filter.test.ts) @@ -120,10 +115,12 @@ collateral). ### Answered by design -`Holding_Split` is refused by the hosted relay because the relay exposes only a -fixed set of settlement choices, and splitting is a wallet concern it does not -surface. The boundary is described in -[Non-goals: the hosted testnet is a demo surface](../concepts/non-goals.md#the-hosted-testnet-is-a-demo-surface-not-a-wallet). +Holding preparation is a wallet concern in self-custodial flows. The only +generic command relay in this repository is the explicitly development-only +`POST /v1/wallet/submit`; it is disabled by default, requires an operator token, +and restricts `actAs` parties when enabled. It is not a public onboarding or +custody service. The boundary is described in +[Non-goals: the development relay is not a wallet](../concepts/non-goals.md#the-development-relay-is-not-a-wallet). ### Self-trade prevention @@ -148,10 +145,11 @@ crosses a different maker's ask and skips its own). ## How this loop is expected to continue -The reference tracks the same standard the ecosystem builds against, and its -hosted testnet is open for exactly this kind of evaluation. New reports open as -issues on the implementation repository; confirmed findings are fixed with a -regression test and this summary is updated. +The reference tracks the same standard the ecosystem builds against. Integrators +can evaluate a checkout with the repository's local live-ledger proof or deploy +their own instance, then open a reproducible issue on the implementation +repository. Confirmed findings should be fixed with a regression test and this +summary updated. --- diff --git a/docs/reference/http-api.md b/docs/reference/http-api.md index 4caca31a..3a32da21 100644 --- a/docs/reference/http-api.md +++ b/docs/reference/http-api.md @@ -9,23 +9,29 @@ before reading the endpoint tables: a party's holdings, trade and swap history. Reads never move value and, with two scoping exceptions below, need no authorization. 2. **Orchestration writes.** Administrative and settlement commands the - operator is authorized to submit, plus explicitly documented hosted-party - RFQ relay routes. These are gated by a bearer token. + operator is authorized to submit, plus the explicitly documented + operator-mediated RFQ routes. These are gated by a bearer token. Order funding, holding allocation, swaps, and LP actions preserve a self-custodial boundary: a trader wallet authors the allocation and this API -only requests or settles it. The RFQ endpoints are the exception. They submit -as hosted trader parties, and RFQ acceptance also submits as the operator, so -the backend ledger user must hold those act-as rights. Do not expose those -routes as a self-custodial production API without replacing that authority -model. +only requests or settles it. The RFQ write endpoints are a custodial exception. +They submit as configured trader parties, and acceptance also submits as the +operator, so the backend ledger user must hold those act-as rights. +`testnet-server.ts` disables that relay by default; opting in requires +per-caller JWT binding. Do not describe or expose that authority model as +self-custodial. + +The server in this repository has no `/v1/testnet/*` namespace, party faucet, +or public-host provisioning. Those are deployment concerns, not hidden API +routes. The only generic signing relay is the development-only endpoint +documented below. ```mermaid flowchart LR UI["dApp / integrator"] subgraph op["Operator backend — this API"] R["Reads
ACS + indexer → JSON"] - W["Orchestration writes
operator commands + hosted RFQ relay"] + W["Orchestration writes
operator commands + mediated RFQ"] end A["Trader wallet
(CIP-0103)"] L[("Canton ledger")] @@ -79,26 +85,35 @@ Three fail-closed gates, applied in this order: |---|---|---| | **Admin token** | `/v1/admin/*` writes | `Authorization: Bearer $OPERATOR_ADMIN_TOKEN` | | **Operator token** | every other state-changing route (pool swap/LP, order, RFQ, matched-trade, wallet relay) | `Authorization: Bearer $DEX_OPERATOR_API_TOKEN` | -| **Per-caller binding** *(optional)* | trader-subject writes | `X-Caller-Token` JWT whose `sub` is the caller's own party | +| **Per-caller binding** *(optional)* | party-scoped reads and trader-subject writes | `X-Caller-Token` JWT whose `sub` is the caller's own party | -Reads are open, except the *unfiltered* forms of `/v1/trades`, `/v1/rfq`, and -`/v1/rfq/history`, whose rows name both parties and so require the admin token. +Market reads are open. Account and party-history reads require an explicit +`owner` or `trader`; when per-caller binding is enabled, that party must match a +valid `X-Caller-Token` (**401** missing/invalid, **403** mismatch). An admin token +may read any party. The *unfiltered* forms of `/v1/trades`, `/v1/rfq`, and +`/v1/rfq/history` require the admin token because their rows name both parties. On the in-memory dev server, `DEX_DEV_OPEN=1` opens the operator-write gate without a token; see -[Local Setup → Exercising write paths](../getting-started.md#exercising-write-paths-in-demo-mode). +[Local Setup → Exercising write paths](../getting-started.md#what-is-safe-to-explore-in-this-mode). When the operator token is unset and the dev bypass is off, an operator write returns **401**. When per-caller binding is configured -(`callerJwtSecret`), a write whose subject party is not the caller's own — or -that carries no valid `X-Caller-Token` — returns **403**. Binding is off by +(`callerJwtSecret`), a party-scoped read or trader-subject write with no valid +`X-Caller-Token` returns **401**; a valid token for a different party returns +**403**. Binding is off by default (a single trusted backend); turn it on when the backend fronts mutually-distrusting callers. +The optional custodial RFQ mode is stricter: `testnet-server.ts` refuses to +enable `DEX_HOSTED_RFQ_RELAY=1` unless `DEX_CALLER_JWT_SECRET` is present. For +that mode, per-caller binding is mandatory rather than optional. + --- ## Read endpoints -Auth is **open** for every read below unless the row says otherwise. +Auth is **open** for market reads unless the row says otherwise. Rows marked +*caller-bound* require the party token only when per-caller binding is enabled. ### Reads — context and market @@ -125,9 +140,10 @@ it surfaces it here rather than making the dApp guess: } ``` -`GET /v1/status` reports `slot` as the participant's latest offset (polled every -2s, with a local counter fallback so the UI's liveness pill keeps moving if the -poll fails): +`GET /v1/status` reports `slot` as the participant's latest ledger-end offset, +polled every two seconds. `synced` reflects the **most recent** probe. A failed +configured-participant probe keeps the last real offset and returns +`synced:false`; only the no-Canton in-memory dev server uses a local counter: ```json { "network": "canton:devnet", "slot": 1234567, "synced": true, "serverTime": "2026-05-17T..." } @@ -151,7 +167,7 @@ fields until `registry-client` implements the standard's off-ledger | Method · Path | Purpose | |---|---| -| `GET /v1/orders?trader=` | Open orders for one trader (**400** without `?trader=`) | +| `GET /v1/orders?trader=` | Open orders for one trader; caller-bound (**400** without `?trader=`) | | `GET /v1/orders/book?pair=BASE/QUOTE` | Resting bids and asks for one market | | `GET /v1/orders/matches?pair=BASE/QUOTE` | Crossable pairs — a read-only preview | @@ -166,8 +182,8 @@ the operator route that *acts* on a match | Method · Path | Purpose | |---|---| -| `GET /v1/holdings?owner=` | Per-contract (UTXO-style) holding rows (**400** without `?owner=`) | -| `GET /v1/balances?owner=` | The holding rows summed per instrument, `available` vs `locked` | +| `GET /v1/holdings?owner=` | Per-contract (UTXO-style) holding rows; caller-bound (**400** without `?owner=`) | +| `GET /v1/balances?owner=` | Caller-bound holding totals per instrument, `available` vs `locked` | `/v1/balances` saves every client re-deriving a balance from the UTXO-style rows. `locked` is the portion committed to open orders, swaps, or allocations; @@ -187,9 +203,9 @@ without a `db` handle. | Method · Path | Purpose | Auth | |---|---|---| -| `GET /v1/trades?trader=&pair=&limit=` | accepted RFQ `MatchedTrade`s + the `SettledTrade` each order-book fill writes | open / **admin** unfiltered | +| `GET /v1/trades?trader=&pair=&limit=` | accepted RFQ `MatchedTrade`s + the `SettledTrade` each order-book fill writes | caller-bound / **admin** unfiltered | | `GET /v1/swaps?pair=&kind=&limit=` | Pool history; `kind` ∈ `swap`,`add_liquidity`,`remove_liquidity`,`state_change` (default `swap`) | open | -| `GET /v1/rfq/history?trader=&limit=` | RFQ lifecycle rows, including accepted quotes (trader, pair, winning dealer, rank) | open / **admin** unfiltered | +| `GET /v1/rfq/history?trader=&limit=` | RFQ lifecycle rows, including accepted quotes (trader, pair, winning dealer, rank) | caller-bound / **admin** unfiltered | | `GET /v1/price-history?pair=&hours=` | Price points from the swaps feed (`hours` 1–720, default 24) | open | | `GET /v1/stats/24h?pair=` | 24h price change, volume, swap count | open | | `GET /v1/dealers` | Dealer registry — public list | open | @@ -207,7 +223,7 @@ one genuinely float-valued field on the API: it is a ratio, not an amount. | Method · Path | Purpose | Auth | |---|---|---| -| `GET /v1/rfq?owner=` | RFQs and quotes scoped to one party | open / **admin** unfiltered | +| `GET /v1/rfq?owner=` | RFQs and quotes scoped to one party | caller-bound / **admin** unfiltered | A trader sees the RFQs they raised or were whitelisted for; a dealer sees the quotes they posted or received. The operator observes *every* RFQ and quote — who @@ -374,6 +390,13 @@ request is rejected with **400** before it reaches the ledger. | `POST /v1/rfq/:cid/cancel` | Cancel an open RFQ (**204**) | | `POST /v1/rfq/accept` | Operator + trader co-sign the accept → `{ tradeCid, receipt }` | +These three writes are disabled (`404`) by default in `testnet-server.ts`. +`DEX_HOSTED_RFQ_RELAY=1` enables the custodial mode only when +`DEX_CALLER_JWT_SECRET` is also configured; the participant user must have +`actAs` rights for every configured trader. This flag does not provision +parties or make the server a public service. Reads remain available when the +mode is disabled. + ```json // POST /v1/rfq { "trader": "...", "rfqId": "...", "pair": "BTC/USDC", "side": "RFQ_Buy", @@ -412,12 +435,14 @@ The pass-through bodies for the pair/pool routes are the service inputs in |---|---|---| | `POST /v1/wallet/submit` | Forward shaped ledger commands under the operator JWT | operator + flag | -Off by default: it returns **404** unless `DEX_DEV_WALLET_RELAY=1`. When on, the -forwarded `actAs` parties must be on the `DEX_DEV_RELAY_PARTIES` allowlist (else -**403**), the `commands` array and `commandId` are shape-checked, and the relay -follows the committed transaction tree to return the created allocation cids the -DvP settle path needs. It is a convenience for the walletless demo, not a -production authority path. +Only the in-memory `dev-server.ts` can enable this route with +`DEX_DEV_WALLET_RELAY=1`; `testnet-server.ts` hard-disables it even if that +variable leaks into a deployment environment. In dev, forwarded `actAs` +parties must be on `DEX_DEV_RELAY_PARTIES` (else **403**), the `commands` array +and `commandId` are shape-checked, and the relay follows the committed +transaction tree to return created allocation cids. It is a walletless local +diagnostic, not a public faucet, hosted-party service, or production authority +path. --- diff --git a/docs/reference/testing.md b/docs/reference/testing.md index 140de334..c580619e 100644 --- a/docs/reference/testing.md +++ b/docs/reference/testing.md @@ -1,25 +1,32 @@ # Testing This reference proves itself in layers. The Daml core is exercised by -in-script suites that run on an in-memory ledger with no Canton process at -all; the operator backend and the dApp have their own unit and integration -suites; and a small set of end-to-end paths drive the whole stack against a -live Canton participant. The design decision throughout is to test each -guarantee at the lowest layer that can hold it — value conservation and -authorization in Daml, projection and idempotency in the backend, command -composition in the dApp — and to reserve the slow, ledger-backed tests for the -seams that only a real engine exercises. - -| Layer | What it proves | Runner | Command | -|---|---|---|---| -| Daml in-script suites | choice logic, conservation, authorization, rounding | Daml Script (in-memory) | `dpm test` in `trading-tests/` | -| Backend | HTTP surface, matching, indexer projection, idempotency, auth | `node:test` (InMemoryLedger) | `npm test` in `services/operator-backend` | -| dApp | wallet-intent → command composition, funding planners, providers | Vitest + jsdom | `npm test` in `app/web` | -| HTTP smoke | every endpoint answers, auth gate holds | Bash + curl (InMemoryLedger) | `bash scripts/e2e-smoke.sh` | -| Live ledger | the JSON Ledger API driver + real settlement | `node:test` / `tsx` (Canton) | `CANTON_E2E=1 npm test`; `npm run localnet:dvp-e2e` | - -Everything above the last row runs offline and is what CI gates on. The last -row needs a Canton participant and is opt-in. +in-script suites that run on an in-memory ledger with no Canton process; the +operator backend and dApp have their own offline suites; a backend-process +smoke checks selected HTTP routes; and opt-in probes exercise narrower seams +against a live Canton participant. + +The word **end-to-end** is reserved here for a path whose stated boundaries are +actually present. None of the automated paths currently includes all of a +browser, real wallet transport, authenticated operator HTTP server, and live +Canton participant. The [Validator Test Plan](../guides/validator-test-plan.md) +is the manual deployment sign-off for those combined boundaries. + +| Path | Boundaries present | What it proves | What it does **not** prove | Command | +|---|---|---|---|---| +| Daml in-script suites | Daml Script engine | choice logic, conservation, authorization, rounding | Canton process, JSON API, backend, browser, wallet | `bash scripts/run-local-daml-tests.sh` | +| Backend suite | backend services/routes + `InMemoryLedger` | HTTP shapes, matching, projection, idempotency, auth | real Daml authorization or participant wire compatibility | `(cd services/operator-backend && npm test)` | +| dApp suite | React/jsdom + mocked fetch/providers | wallet-intent composition, funding planners, UI state | real browser wallet, backend, Canton | `(cd app/web && npm test)` | +| Backend HTTP smoke | backend process + curl + `InMemoryLedger` | selected reads/quote routes and one admin 401 | dApp, successful writes, wallet, Canton, every API endpoint | `bash scripts/backend-http-smoke.sh` | +| Live RFQ service integration | backend service + shared `JsonApiLedger` + Canton | RFQ create/quote/accept/list/cancel and receipt agreement | HTTP server, token settlement, registry factories, browser, wallet | `CANTON_LIVE_RFQ=1 npm run test:live:rfq` from `services/operator-backend` | +| Self-contained live AMM round trip | raw JSON API + Canton | Registry.V2 setup; add → quote-bound swap → partial remove; exact balance/reserve/slice/LP/invariant/conservation checks | backend HTTP, dApp, browser auth, real wallet transport | `npm run live:roundtrip` from `services/operator-backend` | +| Existing-pool add/swap probe | raw JSON API + Canton | mint → add → swap on an existing pool; exact reserve/balance/invariant checks | backend HTTP, dApp, wallet transport, remove | `npm run testnet:seed-pool` from `services/operator-backend` | +| Matched-trade settlement probe | raw JSON API + Canton | V2 allocations and `MatchedTrade_Settle` move one instrument | AMM, RFQ acceptance, backend HTTP, dApp, wallet | `npm run live:matched-trade` from `services/operator-backend` | + +The first four rows run without an external participant; the first three are CI +gates, while the backend HTTP smoke is a manual pre-flight. CI also type-checks +the live driver sources, but does not connect to a participant. Every live row +is opt-in and changes ledger state. ## Daml in-script suites (`trading-tests/`) @@ -50,7 +57,7 @@ ladder from cheap-but-blind to slow-but-honest: | Fixture | Holds real holdings? | Good for | Used by | |---|---|---|---| -| `MockRegistry` | no (empty `inputHoldingCids`) | choice plumbing, multi-party authority | `EndToEndTests` | +| `MockRegistry` | no (empty `inputHoldingCids`) | choice plumbing, multi-party authority | `PoolWorkflowTests`, `OrderWorkflowTests`, `TradeWorkflowTests`, `ChoiceContextWorkflowTests` | | `DexRegistry` over `MockRegistry` | no | the `RegistryApi` interface handshake | `TokenStandardHarnessTests` | | `CantonDex.Registry.V2` | yes (locks, credits, mint/burn accounts) | settlement, conservation, DvP | `PoolLiquidityRulesTests`, `RegistryConservationTests`, `RfqSettlementTests`, `PoolStateInvariantTests` | | upstream `TestTokenV2_RegistryV2` | yes, with a real disclosed `TokenRules` context | cross-registry settlement, per-admin choice context | `RealRegistryDvpTests` | @@ -68,8 +75,13 @@ change, and balance conservation. Value movement is therefore tested against | [`InstrumentTests.daml`](../../trading-tests/CantonDex/Tests/InstrumentTests.daml) | 6 | the standalone lifecycle sample retained in the package lineage: config updates, credential-gated mint, burn, transfer offers, and preapproval | `CantonDex.Instrument` sample (not used by DEX workflows) | | [`EdgeCaseTests.daml`](../../trading-tests/CantonDex/Tests/EdgeCaseTests.daml) | 5 | rejection paths for the standalone lifecycle sample: invalid mint/burn amounts, instrument mismatch, and missing issuer credentials | `CantonDex.Instrument` sample (not used by DEX workflows) | | [`PolicyReceiptTests.daml`](../../trading-tests/CantonDex/Tests/PolicyReceiptTests.daml) | 10 | `PolicyReceipt` + `MatchedTrade` shape invariants: `policyReceiptValues` encoding, `foldPolicyReceiptIntoMetadata`, `isWellFormed`, and the authority guard that rejects a receipt whose `signedBy` is not the venue | pure | -| [`PoolRoundingTests.daml`](../../trading-tests/CantonDex/Tests/PoolRoundingTests.daml) | 5 | pool arithmetic always rounds in the pool's favour, so a swap, deposit, or withdrawal can never quietly pay out more than it should | pure (`PoolModel`) | -| [`EndToEndTests.daml`](../../trading-tests/CantonDex/Tests/EndToEndTests.daml) | 19 | workflow choreography and authority across pool funding, order funding/matching, RFQ accept, OTC settlement, swap, and choice-context threading; it does not prove value movement because the fixture has no holdings | `MockRegistry` | +| [`PoolRoundingTests.daml`](../../trading-tests/CantonDex/Tests/PoolRoundingTests.daml) | 5 | four focused arithmetic proofs plus one holding-backed swap prove that pool-favouring rounding also preserves the settlement invariant | pure `PoolModel` (4); `Registry.V2` holding-backed swap (1) | +| [`PoolWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/PoolWorkflowTests.daml) | 3 | pool initialization, pause/resume, quote/state binding, swap replacement, and request-to-settlement choreography; it does not prove value movement | `MockRegistry` | +| [`OrderWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/OrderWorkflowTests.daml) | 7 | order funding, allocation binding, limit enforcement, rejection paths, and atomic remainder roll-forward; it does not prove locked backing | `MockRegistry` | +| [`TradeWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/TradeWorkflowTests.daml) | 4 | allocation-request consumption, RFQ ranking receipts and expiry, and bilateral settlement assembly; it does not prove balance movement | `MockRegistry` | +| [`ChoiceContextWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/ChoiceContextWorkflowTests.daml) | 5 | allocation and split-admin settlement choice contexts reach the correct registry factories and missing context is rejected; it does not prove value movement | context-requiring `MockRegistry` factories | +| [`DexPairTests.daml`](../../trading-tests/CantonDex/Tests/DexPairTests.daml) | 3 | operator-only listing updates, consuming replacement/visibility, and fee-counter accounting; explicitly does not claim that listing metadata gates pool or order execution | pure listing state | +| [`LifecycleChoiceTests.daml`](../../trading-tests/CantonDex/Tests/LifecycleChoiceTests.daml) | 5 | the exits that happy-path suites can obscure: request cancel/reject, funded-order cancel, matched-trade cancel, RFQ cancel/quote withdraw, and match abort, including controller failures and release of real locked holdings | `Registry.V2` where value release matters | | [`TokenStandardHarnessTests.daml`](../../trading-tests/CantonDex/Tests/TokenStandardHarnessTests.daml) | 1 | the matched-trade flow driven through the `RegistryApi` interface, mirroring `splice-token-standard-test-v2`'s `TradingAppV2` exercise | `DexRegistry` | | [`PoolLiquidityRulesTests.daml`](../../trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml) | 16 | DvP liquidity against real holdings: an atomic add funds base + quote and mints LP tokens in one flow; remove delivers base + quote to the holder and burns LP via the burn account; stale-quote rejection; the settle is co-controlled by operator + `lpRegistrar` | `Registry.V2` | | [`PoolStateInvariantTests.daml`](../../trading-tests/CantonDex/Tests/PoolStateInvariantTests.daml) | 5 | `PoolState.reserves` always equals the sum of the live `PoolSlice` holdings: `PoolRules_ReconcileState` succeeds across an add → swap → remove lifecycle and fails on an omitted slice, an operator-fabricated state, or a foreign slice | `Registry.V2` | @@ -91,9 +103,11 @@ testFloorDivStaysBelowExactQuotient = do ## Backend tests (`services/operator-backend`) -The backend suite runs on `node:test` against an `InMemoryLedger` that mimics -Daml choice semantics, so the HTTP surface, indexer, and pricing logic are all -tested without a Canton process. Type-check and run: +The backend suite runs on `node:test` against a TypeScript `InMemoryLedger` +fixture that implements only the selected choices needed by these service and +route tests. It does not execute Daml. This keeps HTTP, indexer, and pricing +tests fast while the Daml and live-Canton layers prove the ledger behavior. +Type-check and run: ```bash cd services/operator-backend @@ -106,11 +120,11 @@ The files group by concern: | Area | Representative files | What they cover | |---|---|---| | Matching & pricing | `matching.test.ts`, `pool.test.ts`, `order.test.ts`, `decimal-money.test.ts` | order-book aggregation and `matchOrdersForPair`, the AMM quote math, decimal-string money handling | -| RFQ & matched trade | `rfq.test.ts`, `matched-trade.test.ts`, `match-leg-shape.test.ts` | the RFQ accept flow end-to-end (`RfqService.accept` → `MatchedTrade` + `PolicyReceipt`, with `verifyReceipt` digest replay), and the settlement batch wire shape | +| RFQ & matched trade | `rfq.test.ts`, `matched-trade.test.ts`, `match-leg-shape.test.ts` | the RFQ accept path through the service boundary (`RfqService.accept` → `MatchedTrade` + `PolicyReceipt`, with `verifyReceipt` digest replay), and the settlement batch wire shape | | Indexer & idempotency | `idempotency.test.ts`, `indexer-projection-exactness.test.ts`, `indexer-migrations.test.ts`, `order-fill-recording.test.ts` | the replay/idempotency guard, exact decimal projection out of the store, schema migrations, order-fill recording | | Auth & read scoping | `auth.test.ts`, `caller-auth.test.ts`, `read-exposure.test.ts`, `rfq-read-scoping.test.ts` | the write-route auth gate, CORS default-deny, and that party-scoped reads never over-expose | | Ledger driver | `json-api-ledger.test.ts` | `JsonApiLedger.submit` serialization against a mocked `fetch` — create/exercise envelopes and the `updateId` → transaction-tree follow, with no live ledger | -| Docs as tests | `docs-governance-caveats.test.ts`, `docs-token-standard-scope.test.ts`, `docs-v2-only.test.ts` | assertions that keep the docs honest about scope and governance caveats | +| Docs as tests | `docs-governance-caveats.test.ts`, `docs-hosted-scope.test.ts`, `docs-token-standard-scope.test.ts`, `docs-v2-only.test.ts` | assertions that keep the docs honest about governance, Token Standard, and hosted-deployment boundaries | ## dApp tests (`app/web`) @@ -133,168 +147,228 @@ The load-bearing seams: | Wallet providers | `detection.test.ts`, `sdk-provider.test.ts`, `partylayer-provider.test.ts`, `walletconnect-provider.test.ts`, `wallet-store.test.ts` | wallet discovery and the one-row mapping, each provider's result shape and disconnect signal, and store lifecycle (no listener leaks) | | UI | `pages.test.tsx`, `swap-decimal-strings.test.tsx` | page rendering against the mocked backend, and that swap inputs preserve decimal-string precision | -## HTTP smoke test +## Backend HTTP smoke -`scripts/e2e-smoke.sh` boots the dev backend (still `InMemoryLedger`) and curls -every key endpoint in sequence, asserting the response shape and the admin auth -gate, then shuts down. It needs only `node` and `curl` — no Canton: +[`scripts/backend-http-smoke.sh`](../../scripts/backend-http-smoke.sh) starts the development +backend with `InMemoryLedger`, checks selected reads and quotes, confirms that +an unauthenticated admin write returns 401, and stops the process. It does not +start the dApp or Canton, submit a successful write, or exercise a wallet. + +Install the backend dependencies once, then run the script from the repository +root: ```bash -bash scripts/e2e-smoke.sh # "==> All smoke checks passed" +(cd services/operator-backend && npm ci) +bash scripts/backend-http-smoke.sh +# final line: ==> All backend HTTP smoke checks passed ``` -It walks the read endpoints (`/v1/status`, `/v1/context`, `/v1/pools`, -`/v1/pairs`, `/v1/orders`, `/v1/holdings`), a swap quote, the order book, the -price feed, and finally confirms `POST /v1/admin/pairs` is refused without auth. +The script needs Bash, Node.js, npm, curl, and grep. Set `PORT` to use a port +other than 18080. It refuses to reuse a port already serving `/v1/status`. On +failure it prints the retained backend-log path; on success it removes its +temporary directory. -## Against a live Canton participant +## Live Canton probes -The dev backend is in-memory. Two opt-in paths exercise the real JSON Ledger -API driver (`services/operator-backend/src/ledger/json-api.ts`) against an -actual Canton engine. +The probes below require an **already-running participant** with the required +DARs uploaded. LocalNet start/stop is deliberately separate from these test +commands. If using `canton-devkit`, its lifecycle command is +`canton-devkit localnet`; its environment does not supply the DEX role or +package-id variables listed below. The repository's optional adapter uses the +app-provider primary party for operator/admin and allocates an LP/trader and +swapper through the JSON Ledger API. The self-contained driver's synchronizer +id is optional on a single-synchronizer participant; the other raw scripts +still require one. -### The RFQ accept integration test (`CANTON_E2E=1`) +> **State warning:** every live probe submits commands and can leave contracts +> behind after success or failure. Use a throwaway LocalNet where possible. On +> a shared testnet, use dedicated parties/pools and record the printed run id. +> There is no automatic rollback. -`services/operator-backend/test/canton-e2e.test.ts` covers the same ground as -the in-memory `rfq.test.ts`, but routes every command through the real Daml -engine on a Canton participant. It verifies: +### RFQ service integration -- `JsonApiLedger.submit` serializes `submit-and-wait` envelopes with `actAs`, - `commandId`, and `disclosedContracts`. -- `Rfq` and `RfqQuote` creates land on-ledger. -- `RfqService.accept` co-submits `Rfq_Accept` under `[trader, operator]`; the - choice computes its own ranking + receipt and creates a `MatchedTrade` whose - `policyReceipt` matches what the backend computed off-ledger. -- `verifyReceipt` (digest replay) holds against the on-ledger receipt. +[`canton-live-rfq.test.ts`](../../services/operator-backend/test/live/canton-live-rfq.test.ts) +uses the real `JsonApiLedger` and backend `RfqService`, without starting the +HTTP server. It creates an RFQ and quotes, accepts one, verifies the returned +receipt, queries the resulting `MatchedTrade`, checks exact CIDs in the list +case, and verifies cancel archives an RFQ. It does not fund or settle the +`MatchedTrade`; the accepted trade remains on-ledger. -The test is gated on `CANTON_E2E=1` so it stays out of the default run; a local -sandbox run takes ~30s including Canton boot. +Required environment: -**Prerequisites:** DPM with the SDK version pinned by `trading/daml.yaml`, and -the `canton-dex-trading` DAR built (`cd trading && dpm build`). +| Variable | Meaning | +|---|---| +| `CANTON_JSON_API_URL` | participant JSON Ledger API base URL | +| `CANTON_JSON_API_TOKEN` | JWT with `actAs` for operator, trader, and both dealers | +| `CANTON_OPERATOR_PARTY` | RFQ operator and trade venue | +| `CANTON_TRADER_PARTY` | RFQ trader | +| `CANTON_DEALER_JUMP`, `CANTON_DEALER_ORCA` | two quote dealers | +| `CANTON_BTC_ADMIN` | asset-admin party written into the resulting trade | -**1. Boot a sandbox with the DEX DARs.** The trading DAR pulls its Token -Standard dependencies in on upload, but listing them explicitly avoids a -missing-dependency failure: +After building and uploading the current trading DAR and allocating the +parties, run from the backend directory: ```bash -daml sandbox \ - --port 6865 \ - --json-api-port 7575 \ - --dar trading/.daml/dist/canton-dex-trading-0.1.4.dar \ - --dar vendor/splice/dars/splice-api-token-allocation-v2-1.0.0.dar \ - --dar vendor/splice/dars/splice-api-token-allocation-instruction-v2-1.0.0.dar \ - --dar vendor/splice/dars/splice-api-token-allocation-request-v2-1.0.0.dar \ - --dar vendor/splice/dars/splice-api-token-holding-v2-1.0.0.dar \ - --dar vendor/splice/dars/splice-api-token-transfer-instruction-v2-1.0.0.dar \ - --dar vendor/splice/dars/splice-api-token-transfer-events-v2-1.0.0.dar \ - --dar vendor/splice/dars/splice-api-token-metadata-v1-1.0.0.dar +cd services/operator-backend +CANTON_LIVE_RFQ=1 \ + CANTON_JSON_API_URL=https://participant.example \ + CANTON_JSON_API_TOKEN=... \ + CANTON_OPERATOR_PARTY=... \ + CANTON_TRADER_PARTY=... \ + CANTON_DEALER_JUMP=... \ + CANTON_DEALER_ORCA=... \ + CANTON_BTC_ADMIN=... \ + npm run test:live:rfq ``` -**2. Allocate parties and obtain a JWT.** +The live test lives under `test/live/`, outside the ordinary `test/*.test.ts` +glob, so `npm test` cannot discover or submit it. When `CANTON_LIVE_RFQ` is +absent, the explicit `npm run test:live:rfq` command emits one skipped test. -```bash -daml ledger allocate-parties operator alice orca jump btc-admin -daml-helper request-token --party operator > /tmp/operator.jwt -``` +The driver's main mappings are: -The token must grant `actAs` for every party the test submits as — operator, -trader, both dealers, and the asset admin — and is sent as -`Authorization: Bearer ...` on every request. `daml-helper request-token` is -for local dev only; production deployments issue per-session tokens from a -proper IAM. +| `LedgerSubmitter` method | JSON API call | +|---|---| +| `submit` | `POST /v2/commands/submit-and-wait` | +| `query` | `GET /v2/state/ledger-end`, then `POST /v2/state/active-contracts` | +| `subscribe` | `GET /v2/updates/flats` (SSE) | -**3. Run the test.** +### Self-contained AMM round-trip probe + +[`live-amm-roundtrip.ts`](../../scripts/live-amm-roundtrip.ts) creates a unique +`Registry.V2`, registers base/quote/LP instruments, mints the deposit assets, +creates the pool contracts, authors the LP's three allocations, and settles one +add-liquidity DvP. In full mode it then has the swapper authorize the exact +quote-bound input allocation, executes a quote-to-base swap, and redeems half +the LP position through three LP-authored remove allocations. + +The driver asserts: + +- the exact holding and reserve deltas for every phase; +- `PoolState.reserves` equals the active `PoolSlice` sums after add, swap, and + remove; +- LP holdings, `PoolState.totalLpSupply`, and `LPTokenPolicy.totalSupply` + agree after mint and burn; +- the constant product does not decrease after the fee-bearing swap; +- reserve value per remaining LP token does not decrease after redemption; and +- aggregate unlocked holdings plus pool reserves conserve both instruments. + +It does not call the operator HTTP API, render the dApp, exercise browser +authentication, or use a real wallet transport. The script directly authors +the allocations that a wallet would normally submit. + +Required variables are `CANTON_LEDGER_URL`, `CANTON_LEDGER_TOKEN`, +`CANTON_DEX_PACKAGE_ID`, `CANTON_ALLOC_INSTR_PACKAGE_ID`, `CANTON_OPERATOR`, +`CANTON_ADMIN`, and `CANTON_TRADER`. `CANTON_SWAPPER` is optional and defaults +to the trader. `CANTON_USER_ID` defaults to `ledger-api-user`; +`CANTON_SYNCHRONIZER` is also optional, and an omitted value lets a +single-synchronizer participant route commands automatically. The JWT must be +allowed to act as every distinct configured party. `CANTON_ADMIN` is also the +asset issuer and LP registrar in this self-contained fixture. The trader must +differ from the operator because an add cannot self-transfer; full mode also +requires the swapper to differ from the operator. ```bash -CANTON_E2E=1 \ - CANTON_JSON_API_URL=http://localhost:7575 \ - CANTON_JSON_API_TOKEN=$(cat /tmp/operator.jwt) \ - CANTON_OPERATOR_PARTY=operator \ - CANTON_TRADER_PARTY=alice \ - CANTON_DEALER_JUMP=jump \ - CANTON_DEALER_ORCA=orca \ - CANTON_BTC_ADMIN=btc-admin \ - npm test --prefix services/operator-backend +cd services/operator-backend +npm run live:roundtrip +# PASS: add -> swap -> partial remove settled real holdings; ... ``` -The three Canton cases run inside the full backend suite: +The final output includes the unique run, registry, and pool identifiers left +on the participant. `npm run localnet:amm-roundtrip` is the full-round-trip +compatibility alias. For a fast diagnostic that intentionally stops after the +first DvP, use `npm run live:add-liquidity`; it still requires trader and +operator to be different parties. + +### Existing-pool add and swap probe + +[`seed-testnet-pool.ts`](../../scripts/seed-testnet-pool.ts) discovers an +existing registry and pool, mints test assets, performs one add-liquidity and +one swap, and checks exact reserves and holdings, slice reconciliation, and +that the constant-product invariant did not decrease. It does not test remove +liquidity, HTTP, the dApp, or wallet transport. It adds assets to the selected +pool on every run, so use a dedicated test pool. + +Required variables are `CANTON_LEDGER_URL`, `CANTON_LEDGER_TOKEN`, +`CANTON_SYNCHRONIZER`, `CANTON_DEX_PACKAGE_ID`, +`CANTON_ALLOC_INSTR_PACKAGE_ID`, and `CANTON_OPERATOR`. Optional selectors and +amounts are documented at the top of the script: `CANTON_USER_ID`, +`CANTON_LP`, `CANTON_SWAPPER`, `CANTON_REGISTRY_CID`, +`CANTON_LP_REGISTRY_CID`, `POOL_BASE`, `POOL_QUOTE`, `POOL_ID`, `SEED_BASE`, +`SEED_QUOTE`, `SWAP_IN`, and `SWAP_IN_SIDE`. The JWT must be allowed to act as +the operator and all asset-admin, LP-registrar, LP, and swapper parties resolved +by the script. +```bash +cd services/operator-backend +npm run testnet:seed-pool ``` -✔ Canton E2E: RFQ accept produces MatchedTrade with PolicyReceipt -✔ Canton E2E: rfq.list returns visible RFQs and quotes -✔ Canton E2E: rfq.cancel archives an open Rfq -``` - -To run only this file, replace the `npm test` line with -`node --import tsx --test services/operator-backend/test/canton-e2e.test.ts`. -When `CANTON_E2E` is unset, the suite emits a single skip line and the -in-memory `rfq.test.ts` still runs. - -**How the driver maps to the JSON Ledger API:** - -| `LedgerSubmitter` method | JSON API call | -|---|---| -| `submit` (create) | `POST /v2/commands/submit-and-wait` with `CreateCommand` | -| `submit` (exercise) | `POST /v2/commands/submit-and-wait` with `ExerciseCommand` | -| `submit` (exerciseInterface) | `POST /v2/commands/submit-and-wait` with `ExerciseByInterfaceCommand` | -| `query` | `POST /v2/state/active-contracts` | -| `subscribe` | `GET /v2/updates/flats` (SSE) | - -Errors are mapped from the JSON API's `{ errors: [...] }` body to typed -`LedgerError` instances. Contention errors (HTTP 409 / gRPC `ABORTED` carrying -`contention` or `inconsistent`) are tagged retryable, so `retryOnContention` -recovers automatically. When a case fails, the JSON API's response body is the -most useful artifact — the driver puts it in `LedgerError.detail`; set -`NODE_DEBUG=http,fetch` to see full request/response wire traffic. Common -failure modes: -| Symptom | Cause | -|---|---| -| `401: invalid token` | JWT expired or scoped to the wrong party set | -| `404: template not found` | DAR not uploaded, or operator party can't see it | -| `409: contention` | Submission stale; the driver retries automatically | -| `400: requires authorizer X` | `actAs` doesn't include a party the choice needs | +### Matched-trade settlement probe -### The headless DvP round-trip (`localnet:dvp-e2e`) +[`testnet-v2registry-trade.ts`](../../scripts/testnet-v2registry-trade.ts) +creates its own registry and instrument, mints to a sender, creates a +one-instrument `MatchedTrade`, accepts both allocation sides, settles the batch, +and verifies sender/receiver holdings. It is a direct ledger settlement probe; +it does not exercise RFQ acceptance, order matching, AMM code, HTTP, or a +wallet. -`scripts/localnet-dvp-e2e.ts` drives the one seam the browser dApp can't -automate: the trader's wallet authoring allocations. It stands in for a -CIP-0103 wallet, authoring the trader's three allocations for each DvP add and -remove, then settling — exercising the operator's full two-call flow -(request → wallet authors allocations → settle) plus a swap, against a live -LocalNet participant. From the backend (which has `tsx` on its path), with the -LocalNet `CANTON_*` environment exported: +Required variables are `CANTON_LEDGER_URL`, `CANTON_LEDGER_TOKEN`, +`CANTON_SYNCHRONIZER`, `CANTON_DEX_PACKAGE_ID`, +`CANTON_ALLOC_REQUEST_PACKAGE_ID`, `CANTON_ALLOC_INSTR_PACKAGE_ID`, +`CANTON_VENUE`, `CANTON_ADMIN`, `CANTON_ALICE`, and `CANTON_BOB`. +`CANTON_USER_ID` is optional. The JWT must be allowed to act as all four +configured parties. ```bash -npm run localnet:dvp-e2e --prefix services/operator-backend +cd services/operator-backend +npm run live:matched-trade ``` -It is self-contained: it creates its own `Registry.V2`, registers -base/quote/LP instruments, mints to the trader, builds the pool contracts, then -runs add → swap → remove and asserts the on-ledger reserves and LP supply. +### Diagnosing and recovering from failures + +| Symptom | Likely cause | Recovery | +|---|---|---| +| `missing env: NAME` / `required env: NAME` | incomplete environment | export the named variable; no ledger command was sent before configuration finished | +| HTTP 401 | expired JWT or missing party rights | issue a fresh token with the exact `actAs` set | +| template/package not found | DAR absent or wrong package-id environment | upload the current DARs and correct the package ids | +| requires authorizer / authorization failure | token cannot act as a submitted party | compare the script's documented party set with the JWT rights | +| contract not found / duplicate fixture | stale CID, wrong observing party, or a rerun against shared state | use a new throwaway LocalNet or choose a dedicated pool; do not assume a failed run rolled back earlier transactions | + +The RFQ driver surfaces JSON API failures as `LedgerError.detail`; the raw +scripts print their failing step and HTTP response body. Preserve that output +and the run id before resetting a throwaway LocalNet. There is no generic +cleanup command because a partial run can stop at many different contract +states. ## What CI runs `.github/workflows/ci.yml` gates every pull request on the offline layers: the Daml build, in-script tests, and upgrade-compatibility check; backend typecheck and tests; frontend typecheck, tests, and production build; the documentation -site build; and a Docker build smoke. The live-ledger paths above are opt-in and -not part of CI. +site build; and a container build plus backend runtime smoke. The container +check starts the read-only backend with an intentionally unreachable +participant, asserts that `/v1/status` reports unsynchronized state, verifies +an unauthenticated admin write returns 401, and checks the non-root runtime and +SQLite binding. CI also runs `npm run typecheck:live-scripts` so the +deployment/bootstrap and raw live-driver sources cannot silently drift. It does +not connect to Canton or prove any live path. ## Out of scope -- A pool add-liquidity + swap end-to-end over the *JSON Ledger API*. The - `PoolLiquidityRulesTests` and `RealRegistryDvpTests` Daml suites cover this - ground at the ledger level, and `localnet:dvp-e2e` covers it against a live - participant; a JSON-API-driven version can be added as another integration - test. +- An automated remove-liquidity path through the authenticated operator HTTP + API and a real browser wallet. The self-contained raw JSON-API driver proves + the live ledger transition, not those application boundaries. - The order-funding flow (`OrderFundingRequest` → trader-authored allocation → `Order_Fund`) through a real browser wallet. The wallet handoff lives in `app/web/src/wallet/`; an integration test for it needs a wallet emulator. -- The full registry HTTP API. The `CANTON_E2E` test stubs `getFactories` - because the RFQ accept flow reads no factory CIDs; tests that exercise pool - swaps will need a real registry-backed factory. +- One automated path through browser, real wallet transport, authenticated + backend HTTP, and live Canton. +- A live external-registry HTTP round trip. The live RFQ integration test uses + a `FixedRegistryClient` whose methods are never called because RFQ acceptance + does not allocate or settle tokens. Offline tests prove the canonical + operation-specific request bodies and response validation; a live swap test + still needs a deployed registry endpoint, credentials, and factory contracts. --- diff --git a/docs/tutorials/amm-first-walkthrough.md b/docs/tutorials/amm-first-walkthrough.md new file mode 100644 index 00000000..55146a62 --- /dev/null +++ b/docs/tutorials/amm-first-walkthrough.md @@ -0,0 +1,362 @@ +# Trace one AMM swap from formula to Daml settlement + +This tutorial is for an AMM developer who knows `x*y=k` but is new to Canton +and Daml. You will trace one exact-input swap through the repository, run three +focused Daml tests, and learn what each test does—and does not—prove. + +This is a code-reading tutorial, not a live-network deployment. It uses the +Daml Script runner so you can focus on contract state, authority, and value +movement before adding a participant, wallet, or HTTP backend. + +## Before you begin + +Read the [Canton and Daml primer](../concepts/canton-daml-primer.md), then install +the Daml prerequisites from [Getting started](../getting-started.md#additional-tools-for-daml-builds-tests-and-the-live-proof). + +From the repository root, build the trading DAR once: + +```bash +dpm install 3.5.2 +bash scripts/build-trading-surface.sh +``` + +A successful build ends with: + +```text +canton-dex-trading built successfully. +``` + +You will work with these files: + +| Question | File | +|---|---| +| Where is the constant-product formula? | [`PoolModel.daml`](../../trading/CantonDex/Dex/PoolModel.daml) | +| Where are quote binding and swap settlement enforced? | [`PoolRules.daml`](../../trading/CantonDex/Dex/PoolRules.daml) | +| What is pool configuration versus mutable state? | [`Pool.daml`](../../trading/CantonDex/Dex/Pool.daml), [`PoolState.daml`](../../trading/CantonDex/Dex/PoolState.daml) | +| Where is reserve value represented? | [`PoolSlice.daml`](../../trading/CantonDex/Dex/PoolSlice.daml) and [`Registry/V2.daml`](../../trading/CantonDex/Registry/V2.daml) | +| Which tests should I read first? | [`PoolRoundingTests.daml`](../../trading-tests/CantonDex/Tests/PoolRoundingTests.daml), [`PoolWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/PoolWorkflowTests.daml), [`RealRegistryDvpTests.daml`](../../trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml) | + +## 1. Start from the familiar formula + +For reserve-in `x`, reserve-out `y`, exact input `dx`, and fee `f`, the usual +constant-product output is: + +```text +dxAfterFee = dx × (1 - f) +dy = y × dxAfterFee / (x + dxAfterFee) +``` + +The repository implements that in +[`constantProductOut`](../../trading/CantonDex/Dex/PoolModel.daml): + +```daml +constantProductOut reserveIn reserveOut feeBps inputAmount = + let amountInAfterFee = + floorDiv (floorMul inputAmount (intToDecimal (10000 - feeBps))) 10000.0 + in floorDiv (floorMul amountInAfterFee reserveOut) + (reserveIn + amountInAfterFee) +``` + +Two details matter: + +- fees use basis points, so 30 means 0.30%; +- multiplication and division round down on pool payouts so fixed-scale + decimal rounding cannot make the pool pay more than the exact result. + +The full input—not only `amountInAfterFee`—is later added to the input reserve. +That is how the fee remains in the pool and accrues to LPs. + +### Run the arithmetic proof + +From `trading-tests/`: + +```bash +cd trading-tests +dpm test -p testSwapOutputRoundsDownToKeepConstantProduct +``` + +Expected result: + +```text +testSwapOutputRoundsDownToKeepConstantProduct: ok +``` + +Read that test in +[`PoolRoundingTests.daml`](../../trading-tests/CantonDex/Tests/PoolRoundingTests.daml). +It creates a zero-fee 1000/1000 pool, swaps 7 units, and asserts that the +post-swap product is not lower than the pre-swap product. Zero fees remove the +usual fee cushion, exposing a one-unit-of-precision overpayment. + +This test proves arithmetic plus real Daml settlement in its fixture. It does +not exercise the backend or browser. + +## 2. Replace one “pool contract” with four responsibilities + +An EVM AMM often places configuration, reserves, and swap functions on one pair +contract. This reference separates them: + +```mermaid +flowchart TD + Pool[Pool
immutable instruments, parties, fee] + State[PoolState
aggregate reserves, LP supply, status] + Rules[PoolRules
request, validate, settle, pause] + Slices[PoolSlice set
committed reserve inventory] + Holding[Token Standard Holding / Allocation
actual value backing] + + Pool --> State + Pool --> Rules + State -->|prices against totals| Rules + Slices -->|must sum to reserves| State + Slices --> Holding + Rules -->|settles and rolls forward| Slices +``` + +Open the files and identify these fields: + +- `Pool.poolId`, the two instrument IDs, `lpInstrumentId`, and `feeBps` are + stable configuration. +- `PoolState.reserves`, `totalLpSupply`, and `status` are the small global state + every reserve-changing operation serializes through. +- each `PoolSlice` names one side, amount, and committed allocation contract ID; +- `PoolRules` is operator-signed and exposes nonconsuming choices. The rules + contract stays active while a swap archives and recreates state and slices. + +The accounting invariant is: + +```text +PoolState.baseAmount = sum(active base PoolSlice.amount) +PoolState.quoteAmount = sum(active quote PoolSlice.amount) +``` + +`PoolState` makes pricing efficient; slices connect those totals to reserved +Token Standard value. A reserve number without matching slices would be only +an operator assertion, not spendable inventory. + +## 3. See why quoting is not authorization + +The browser can compute or request a quote without moving funds. A settle needs +an allocation specification that binds the trader to exact transfer-leg sides +and one pool snapshot. + +The operator exercises `PoolRules_RequestSwap`. Its result contains: + +```daml +data PoolRules_RequestSwapResult = PoolRules_RequestSwapResult with + settlement : V2.SettlementInfo + allocationSpec : V2.AllocationSpecification + quoteBinding : Optional SwapQuoteBinding +``` + +The `quoteBinding` records the state and slice contract IDs plus the trader's +minimum output: + +```daml +data SwapQuoteBinding = SwapQuoteBinding with + expectedPoolId : PoolId + poolStateCid : ContractId PoolState + inputSliceCid : ContractId PoolSlice + outputSliceCids : [ContractId PoolSlice] + minOutputAmount : Decimal +``` + +Contract IDs are part of the concurrency control. If another swap archives the +bound `PoolState` or a bound slice first, the old quote cannot settle. The +operator must produce a fresh request; it cannot reuse the trader's authority +against different state. + +Inside `PoolRules_RequestSwap`, Daml builds the specification from the prepared +input and output legs: + +```daml +allocationSpec = + Utils.mkIteratedAllocationSpecification + pool.admin + swapperAccount + None + (prepared.preparedSwapInLeg :: prepared.preparedOutputDelivery.legs) + None + False +``` + +The operator prepares this specification, but the trader's wallet authors the +allocation against it. Preparing terms and authorizing funds are separate +actions. + +## 4. Follow authority, not HTTP calls + +The essential swap has three ledger steps: + +| Step | Daml action | Required authority | Result | +|---|---|---|---| +| Prepare | exercise `PoolRules_RequestSwap` | operator | exact settlement info, allocation spec, and quote binding | +| Allocate | exercise `AllocationFactory_Allocate` | trader, plus any registry-required context/actors | trader's input value locked for those terms | +| Settle | exercise `PoolRules_Swap` | operator | input and output settle atomically; state/slices roll forward | + +The dApp and backend orchestrate those steps, but neither changes who controls +them. A frontend button cannot substitute operator authority, and an operator +API token cannot substitute the trader's wallet authority on a self-custodial +allocation. + +### Run the choreography proof + +```bash +cd trading-tests +dpm test -p testPoolSwapViaRequestSwap +``` + +Expected result: + +```text +testPoolSwapViaRequestSwap: ok +``` + +Read the named test in +[`PoolWorkflowTests.daml`](../../trading-tests/CantonDex/Tests/PoolWorkflowTests.daml). +The most important three lines of the story are: + +```daml +reqRes <- submit operator $ exerciseCmd rulesCid PoolRules_RequestSwap with ... +bobInstr <- submit bob $ exerciseCmd factoryCid bobAllocateArg +swapRes <- submit operator $ exerciseCmd rulesCid PoolRules_Swap with ... +``` + +This is excellent authority and choreography documentation: operator, then +trader, then operator. Its `MockRegistry` fixture does not contain real +holdings, so this particular test does **not** prove balance conservation. The +file header says so explicitly. + +## 5. Read the atomic settlement boundary + +`PoolRules_Swap` recomputes the output from the bound pool snapshot and checks +that every supplied contract ID and the minimum output match the quote binding. +It then calls the registry's batch settlement factory: + +```daml +settleResult <- exercise factoryCid V2.SettlementFactory_SettleBatch with + settlement + transferLegs = swapInLeg :: outDel.legs + allocations = swapperFinalized :: inputFinalized :: outDel.sliceFinalizeds + actors = [operator] + extraArgs +``` + +Because this is nested in one Daml transaction, settlement and the following +state changes are atomic. After successful settlement the choice: + +1. rolls the input reserve allocation forward with the full input added; +2. consumes enough output slices to pay the trader and recreates any leftover + boundary slice; +3. asserts that slice deltas equal reserve deltas; +4. archives the old `PoolState` and creates the successor reserves. + +If batch settlement fails, the state and slice updates do not commit. If a +reserve/slice assertion fails, the value settlement does not commit either. + +## 6. Run the real-holding proof + +Now run the test whose fixture creates actual Token Standard holdings and uses +an upstream context-requiring V2 registry: + +```bash +cd trading-tests +dpm test -p testRealRegistryDvpSwapSettles +``` + +Expected result: + +```text +testRealRegistryDvpSwapSettles: ok +``` + +Read the named test in +[`RealRegistryDvpTests.daml`](../../trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml). +It proves more than the choreography test: + +- the request's sender side is the exact input instrument and amount; +- the receiver side is a positive amount of the output instrument; +- changing the signed receiver amount by `0.0000000001` makes settlement fail; +- the trader's input holdings back the allocation; +- reserves move in the expected directions; +- the trader receives an output `Holding`. + +It still runs in Daml Script. It does not prove package upload, JSON API +serialization, wallet compatibility, network topology, or browser behavior. + +## 7. Promote the proof to a real Canton process + +Return to the repository root and run the default live proof: + +```bash +bash scripts/run-dpm-sandbox-proof.sh +``` + +This starts the real Canton sandbox bundled with the pinned DPM SDK, uploads +the Token Standard and DEX package closure, and drives add → swap → remove +through the JSON Ledger API. The final checkpoint is: + +```text +==> PASS: portable live-Canton proof completed + The throwaway sandbox is now stopping; no persistent ledger state remains. +``` + +You have now crossed two boundaries that Daml Script did not test: a Canton +process started, and the JSON Ledger API accepted the package and value-flow +commands. The script uses one unrestricted authentication-disabled sandbox +user, but three Canton parties: operator/admin/LP registrar share the bootstrap +party, while LP/trader and swapper are distinct counterparties. It still does +not start the operator HTTP server, browser, or wallet. Those omissions are +deliberate; see +[Local Canton from a clean clone](../guides/localnet.md) for the proof matrix +and the optional persistent environments. + +## 8. Connect the code to the UI without overstating it + +After the Daml tests pass, run the browser preview from +[Getting started](../getting-started.md#mode-1-run-the-browser-preview). On the +Trade page: + +1. change the BTC or USDC input and observe the quote; +2. open browser developer tools and find the quote/request calls; +3. connect Mock Wallet and inspect the wallet intent logged to the console; +4. notice that its returned `#mock-…:0` value is not the allocation created in + the Daml test. + +The UI shows how a real integration is orchestrated. The Daml tests show what +the contracts enforce. Only a live participant plus compatible wallet joins +the browser orchestration and on-ledger settlement boundaries in one validation. + +## 9. Use the same reading pattern for other AMM flows + +You can now trace add and remove liquidity with the same questions: + +| Question | Add/remove liquidity answer | +|---|---| +| What computes the economic amounts? | pool ratio, LP supply, and conservative rounding in `PoolModel.daml` | +| What records intent? | `LiquidityAllocationRequest` | +| Who authorizes base/quote or LP value? | the liquidity provider through allocation factory choices | +| Who executes? | operator and LP registrar on the liquidity rules choice | +| What makes it atomic? | one settlement batch combines deposits/redemption with LP mint/burn | +| Which real-value test should I read? | `testDvpAddLiquidity`, `testDvpRemoveDeliversToHolder`, and their negative cases in `PoolLiquidityRulesTests.daml` | + +Then read [Liquidity and custody](../concepts/liquidity-and-custody.md) for the +full slice design and [LP tokens](../concepts/lp-tokens.md) for issuance and +redemption. + +## Completion checklist + +You have completed this tutorial when you can point to: + +- the function that computes `amountOut`; +- the contracts that separate pool configuration, aggregate state, and reserve + backing; +- the choice that builds the trader's exact allocation specification; +- the line where the trader—not the operator—authors the allocation; +- the nested batch-settlement choice; +- one mock-registry choreography test and one real-holding value test; +- the final checkpoint of the DPM sandbox proof; +- the reason passing the Daml and sandbox proofs is not yet a live browser and + external-wallet dApp. + +**Next canonical step:** [15-minute design tour](../concepts/design-tour.md). +Use [Liquidity and custody](../concepts/liquidity-and-custody.md) and +[Local Canton from a clean clone](../guides/localnet.md) as topic references. diff --git a/docs/tutorials/make-your-first-amm-change.md b/docs/tutorials/make-your-first-amm-change.md new file mode 100644 index 00000000..87d44446 --- /dev/null +++ b/docs/tutorials/make-your-first-amm-change.md @@ -0,0 +1,201 @@ +# Tutorial: make your first AMM code change + +This is Step 8 of the +[canonical newcomer learning path](../README.md#canonical-newcomer-learning-path). +Complete the workflow-design step first. Here you will make one small, +behavior-preserving Daml refactor: give the swap-fee calculation a name, prove +the new helper with a focused test, and then check every layer that could be +affected. + +You will edit two files in your own checkout: + +- `trading/CantonDex/Dex/PoolModel.daml`, which owns the AMM arithmetic; and +- `trading-tests/CantonDex/Tests/PoolRoundingTests.daml`, which proves the + arithmetic's conservative rounding. + +The finished change does **not** alter the formula, template fields, choices, +HTTP API, or UI. That makes it a useful first contribution: the fail/pass loop +is real, while the expected behavior remains stable. + +## Before you start + +From the repository root, confirm that the unmodified Daml surface is green: + +```bash +bash scripts/run-local-daml-tests.sh +``` + +All Daml Script tests should report `ok`, and the command should exit with +status 0. If the command cannot find Java, DPM, or SDK 3.5.2, return to +[Getting started — prerequisites](../getting-started.md#prerequisites). + +Keep the repository root as the starting directory for every command below. + +## 1. Write the focused proof first + +Open +[`trading-tests/CantonDex/Tests/PoolRoundingTests.daml`](../../trading-tests/CantonDex/Tests/PoolRoundingTests.daml) +and find this existing declaration: + +```daml +testSwapOutputRoundsDownToKeepConstantProduct : Script () +testSwapOutputRoundsDownToKeepConstantProduct = do +``` + +Immediately after the `= do` line, add these two assertions: + +```daml + PM.amountAfterSwapFee 30 1000.0 === 997.0 + PM.amountAfterSwapFee 25 1000.0 === 997.5 +``` + +They state the rule in basis points: a 30 bps fee leaves `997.0` of a +`1000.0` input, and a 25 bps fee leaves `997.5`. The `PM` alias is already +imported at the top of the test file. + +Run only that script: + +```bash +(cd trading-tests && dpm test -p testSwapOutputRoundsDownToKeepConstantProduct) +``` + +### Expected failure + +The command should exit nonzero because `amountAfterSwapFee` does not exist +yet. Depending on the SDK's diagnostic wording, the error will say that +`PM.amountAfterSwapFee` is unknown, not in scope, or not exported. This failure +is the red half of the red/green loop. If the test passes at this point, check +that you saved the file and ran the command from this checkout. + +## 2. Extract the fee calculation + +Open +[`trading/CantonDex/Dex/PoolModel.daml`](../../trading/CantonDex/Dex/PoolModel.daml). +Find `floorDiv`, then add this helper immediately below it: + +```daml +-- | Input remaining after the pool fee, rounded down so the pool never +-- pays out from value it did not receive. +amountAfterSwapFee : Int -> Decimal -> Decimal +amountAfterSwapFee feeBps inputAmount = + floorDiv + (floorMul inputAmount (intToDecimal (10000 - feeBps))) + 10000.0 +``` + +Next, find `constantProductOut` and replace only its definition with: + +```daml +constantProductOut : Decimal -> Decimal -> Int -> Decimal -> Decimal +constantProductOut reserveIn reserveOut feeBps inputAmount = + let amountInAfterFee = amountAfterSwapFee feeBps inputAmount + in floorDiv (floorMul amountInAfterFee reserveOut) + (reserveIn + amountInAfterFee) +``` + +The old inline expression and the new helper call are mathematically +identical. `floorMul` and `floorDiv` still round in the pool's favor at the +same points. + +## 3. Build, then make the focused proof green + +Build the trading DAR before compiling its test package: + +```bash +bash scripts/build-trading-surface.sh +(cd trading-tests && dpm test -p testSwapOutputRoundsDownToKeepConstantProduct) +``` + +The focused command should now exit 0 and report the named script as `ok`. +If it still reports the missing helper, confirm that the helper is at module +scope rather than nested inside `floorDiv`. + +## 4. Check which layers the change affects + +Use this table before expanding the change: + +| Layer | Impact of this tutorial's edit | Why | +|---|---|---| +| Daml implementation | **Changed** | `constantProductOut` now calls a named helper. | +| Ledger schema and choices | **Unchanged** | No template, record, choice argument, or result type changed. | +| Settlement behavior | **Unchanged by design** | The same fee and rounding expression runs before the same output calculation. | +| Operator backend | **No edit required** | Its public API and expected quote shape did not change. | +| React dApp / wallet handoff | **No edit required** | No request, response, or wallet-intent field changed. | + +This is impact analysis, not permission to ignore other layers for a real math +change. If you later change the formula or rounding, inspect and update these +consumers together: + +- `services/operator-backend/src/pool/index.ts` for backend quote math; +- `services/operator-backend/src/dev-server.ts` for preview behavior; +- `scripts/live-amm-roundtrip.ts` for the independent live-proof expectation; +- the related backend tests and dApp tests for displayed quotes and limits. + +The UI can display a fee and proposed quote, but it does not authorize final +settlement. The Daml choice must always recompute and validate executable +amounts from the bound ledger state. + +## 5. Run the full local checks + +Now prove that the refactor did not disturb another workflow: + +```bash +bash scripts/run-local-daml-tests.sh +(cd services/operator-backend && npm run typecheck && npm test) +(cd app/web && npm test && npm run build) +``` + +Expected results: + +- every Daml Script test reports `ok` and the script exits 0; +- backend type-checking exits cleanly and TAP ends with `# fail 0`; and +- Vitest reports all dApp tests passed, then Vite writes `app/web/dist/`. + +Run `npm ci` once in `services/operator-backend` and `app/web` if their +dependencies are not installed. + +## 6. Prove the DAR on a real throwaway Canton process + +Run the repository's portable live-ledger proof: + +```bash +bash scripts/run-dpm-sandbox-proof.sh +``` + +Near the end, expect: + +```text +==> Running the live-Canton DvP proof +==> PASS: portable live-Canton proof completed + The throwaway sandbox is now stopping; no persistent ledger state remains. +``` + +This proves package upload and add → quote-bound swap → partial-remove value +movement through the JSON Ledger API on a real Canton process. It still does +not prove browser, external-wallet, or operator-backend HTTP integration. Those +boundaries require the separately configured environments described in +[Getting started](../getting-started.md) and the +[testing reference](../reference/testing.md). + +## 7. Review the change + +Check whitespace and inspect only the intended diff: + +```bash +git diff --check +git diff -- \ + trading/CantonDex/Dex/PoolModel.daml \ + trading-tests/CantonDex/Tests/PoolRoundingTests.daml +``` + +You are finished when: + +- the focused proof failed before the helper existed and passed afterward; +- the complete Daml, backend, and dApp checks pass; +- the live sandbox proof prints its `PASS` line; +- the diff contains one helper, one call-site refactor, and two assertions; and +- you can explain why no backend or UI source edit was needed. + +Continue to Step 9, the [Builder guide](../guides/builder-guide.md), to plan a +behavior-changing extension and identify every affected boundary before you +edit it. diff --git a/scripts/e2e-smoke.sh b/scripts/backend-http-smoke.sh old mode 100755 new mode 100644 similarity index 66% rename from scripts/e2e-smoke.sh rename to scripts/backend-http-smoke.sh index fb7db14e..71a35f3c --- a/scripts/e2e-smoke.sh +++ b/scripts/backend-http-smoke.sh @@ -1,47 +1,69 @@ #!/usr/bin/env bash -# End-to-end smoke test. Starts the dev backend, hits every key endpoint, -# verifies responses, then shuts down. Exits non-zero on any failure. +# Backend HTTP smoke test. Starts the in-memory dev backend, checks a selected +# set of read/quote endpoints plus the admin auth gate, then shuts down. +# Exits non-zero on any failure. # -# Usage: ./scripts/e2e-smoke.sh +# Usage: bash scripts/backend-http-smoke.sh # -# Requires: node, curl. Does not require a Canton participant (uses -# InMemoryLedger). +# Requires: bash, node, npm, curl, grep, and `npm ci` already run in +# services/operator-backend. Does not require a Canton participant or dApp. +# This is not a wallet, settlement, or full-stack browser test. set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" PORT="${PORT:-18080}" BASE="http://localhost:${PORT}" +SMOKE_TMP_DIR="$(mktemp -d "${TMPDIR:-/tmp}/canton-dex-http-smoke.XXXXXX")" +BACKEND_LOG="$SMOKE_TMP_DIR/backend.log" cleanup() { + local status=$? if [[ -n "${BACKEND_PID:-}" ]]; then kill "$BACKEND_PID" 2>/dev/null || true wait "$BACKEND_PID" 2>/dev/null || true fi + if [[ "$status" -eq 0 ]]; then + rm -rf "$SMOKE_TMP_DIR" + else + echo "backend log retained at: $BACKEND_LOG" >&2 + fi + return "$status" } trap cleanup EXIT +if curl -fsS "${BASE}/v1/status" >/dev/null 2>&1; then + echo "refusing to run: ${BASE} is already serving /v1/status; choose another PORT" >&2 + exit 1 +fi + echo "==> Starting dev backend on :$PORT" ( cd "$ROOT_DIR/services/operator-backend" - PORT="$PORT" npm run dev >/tmp/e2e-smoke-backend.log 2>&1 & - echo $! > /tmp/e2e-smoke-backend.pid -) -BACKEND_PID="$(cat /tmp/e2e-smoke-backend.pid)" + PORT="$PORT" exec npm run dev +) >"$BACKEND_LOG" 2>&1 & +BACKEND_PID="$!" # Wait for the server to come up. +READY=0 for i in {1..30}; do if curl -fsS "${BASE}/v1/status" >/dev/null 2>&1; then + READY=1 break fi if ! kill -0 "$BACKEND_PID" 2>/dev/null; then echo "backend died during startup; log:" - cat /tmp/e2e-smoke-backend.log + cat "$BACKEND_LOG" exit 1 fi sleep 0.5 done +if [[ "$READY" != "1" ]]; then + echo "backend did not become ready within 15 seconds; log:" + cat "$BACKEND_LOG" + exit 1 +fi check_get_contains() { local name="$1" @@ -83,8 +105,9 @@ check_status() { fi } -echo "==> Read endpoints" -check_get_contains status "${BASE}/v1/status" '"synced":true' +echo "==> Selected read endpoints" +check_get_contains status-preview "${BASE}/v1/status" '"network":"preview:in-memory"' +check_get_contains status-sync "${BASE}/v1/status" '"synced":true' check_get_contains context "${BASE}/v1/context" '"operator"' check_get_contains pools "${BASE}/v1/pools" 'BTC' check_get_contains pairs "${BASE}/v1/pairs" 'BTC' @@ -116,4 +139,4 @@ echo "==> Admin auth gate" check_status admin-401 401 -X POST -H 'Content-Type: application/json' -d '{}' \ "${BASE}/v1/admin/pairs" -echo "==> All smoke checks passed" +echo "==> All backend HTTP smoke checks passed" diff --git a/scripts/bootstrap-registry.ts b/scripts/bootstrap-registry.ts index 5b9fec09..3d71cd16 100644 --- a/scripts/bootstrap-registry.ts +++ b/scripts/bootstrap-registry.ts @@ -13,15 +13,17 @@ // node --import tsx scripts/bootstrap-registry.ts // // Required env vars (see services/operator-backend/.env.example): -// CANTON_LEDGER_URL, CANTON_LEDGER_TOKEN, CANTON_USER_ID, +// CANTON_LEDGER_URL, CANTON_LEDGER_TOKEN, CANTON_DEX_PACKAGE_ID, // CANTON_ADMIN, CANTON_LP_REGISTRAR, CANTON_OPERATOR. // // Optional: // BOOTSTRAP_CONFIG path to a JSON config (default: scripts/bootstrap-registry.json) // BOOTSTRAP_DRY_RUN "1" to print the plan without submitting -// CANTON_DEX_PACKAGE_ID package hash prefix for template ids +// CANTON_USER_ID JSON Ledger API user id (default: ledger-api-user) import { readFileSync, existsSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { JsonApiLedger } from "../services/operator-backend/src/ledger/json-api.js"; import { rootLogger } from "../services/operator-backend/src/lib/logger.js"; @@ -75,7 +77,15 @@ function required(name: string): string { } function loadConfig(): BootstrapConfig { - const path = process.env.BOOTSTRAP_CONFIG ?? "scripts/bootstrap-registry.json"; + // The deploy script intentionally runs this module from + // services/operator-backend so `tsx` resolves from that package. Anchor the + // default beside this source file instead of silently changing behavior with + // the caller's working directory. Explicit relative overrides remain + // relative to the caller, as shell users expect. + const scriptDir = dirname(fileURLToPath(import.meta.url)); + const path = process.env.BOOTSTRAP_CONFIG + ? resolve(process.cwd(), process.env.BOOTSTRAP_CONFIG) + : resolve(scriptDir, "bootstrap-registry.json"); if (!existsSync(path)) { log.info("config file not found, using defaults", { path }); return DEFAULT_CONFIG; @@ -175,6 +185,7 @@ async function main(): Promise { const lpRegistrar = required("CANTON_LP_REGISTRAR"); // Lazy: only needed once there is a registry to create. const operator = () => required("CANTON_OPERATOR"); + const dexPackageId = required("CANTON_DEX_PACKAGE_ID"); const userId = process.env.CANTON_USER_ID ?? "ledger-api-user"; const dryRun = process.env.BOOTSTRAP_DRY_RUN === "1"; @@ -183,7 +194,7 @@ async function main(): Promise { baseUrl, token, applicationId: userId, - templateIdPrefix: process.env.CANTON_DEX_PACKAGE_ID, + templateIdPrefix: dexPackageId, synchronizerId: process.env.CANTON_SYNCHRONIZER, }); @@ -229,7 +240,13 @@ async function main(): Promise { } } - log.info("bootstrap complete", { dryRun }); + log.info("bootstrap complete", { + dryRun, + assetAdmin: admin, + assetRegistryCid: assetRegistryCid ?? null, + lpRegistrar, + lpRegistryCid: lpRegistryCid ?? null, + }); } main().catch((e) => { diff --git a/scripts/deploy-testnet.sh b/scripts/deploy-testnet.sh index 011df69a..7bdeb9dc 100755 --- a/scripts/deploy-testnet.sh +++ b/scripts/deploy-testnet.sh @@ -1,130 +1,207 @@ #!/usr/bin/env bash -# Canton testnet deployment. +# Deterministic Canton testnet deployment. # -# Steps: -# 1. Build all DARs from source. -# 2. Upload DARs to the target Canton participant via JSON Ledger API. -# 3. Allocate parties (operator, lpRegistrar, admin, demo trader). -# 4. Run the registry bootstrap script (scripts/bootstrap-registry.ts). -# 5. Seed initial pairs and pools via the operator backend admin API. -# 6. Health check. +# Default phases: +# 1. Build the DEX DARs. +# 2. Upload the current DEX DAR and its embedded dependency closure. +# 3. Bootstrap Registry.V2 contracts and instrument configuration. # -# Required env vars (see services/operator-backend/.env.example): -# CANTON_LEDGER_URL, CANTON_LEDGER_TOKEN -# CANTON_OPERATOR, CANTON_LP_REGISTRAR, CANTON_ADMIN -# OPERATOR_ADMIN_TOKEN (for admin API calls) +# Optional market metadata phase (DEPLOY_SEED_MARKETS=1): +# 4. Through an ALREADY-RUNNING operator backend, create a DexPair and an +# unfunded Pool when they do not already exist. # -# Optional: -# DEPLOY_SKIP_BUILD=1 skip `dpm build` (use existing DARs) -# DEPLOY_SKIP_UPLOAD=1 skip DAR upload (already uploaded) -# DEPLOY_SKIP_PARTIES=1 skip party allocation (already exist) -# DEPLOY_SKIP_SEED=1 skip initial pair/pool seeding +# Deliberate boundaries: +# - This script does not allocate parties. CANTON_* party values must be the +# exact allocated party ids, and the ledger JWT must have their rights. +# - Creating a Pool does not fund it. Use seed-testnet-pool.ts or a wallet LP +# flow after this script. +# - A successful or partially failed run mutates the target ledger. Run only +# against the intended participant and inspect the printed phase boundary. set -euo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +usage() { + printf '%s\n' \ + "Usage: bash scripts/deploy-testnet.sh" \ + "" \ + "Required:" \ + " CANTON_LEDGER_URL CANTON_LEDGER_TOKEN" \ + " CANTON_OPERATOR CANTON_LP_REGISTRAR CANTON_ADMIN" \ + " CANTON_DEX_PACKAGE_ID" \ + "" \ + "Optional phase flags:" \ + " DEPLOY_SKIP_BUILD=1 use existing DARs" \ + " DEPLOY_SKIP_UPLOAD=1 packages are already uploaded/vetted" \ + " DEPLOY_SKIP_BOOTSTRAP=1 registry already exists" \ + " DEPLOY_SEED_MARKETS=1 create pair + unfunded pool via API" \ + "" \ + "Market phase variables:" \ + " API_BASE (default http://localhost:8080)" \ + " OPERATOR_ADMIN_TOKEN" \ + " DEPLOY_BASE (default BTC), DEPLOY_QUOTE (default USDC)" \ + " DEPLOY_LP_INSTRUMENT (default BTC-USDC-LP)" \ + " DEPLOY_POOL_FEE_BPS (default 30)" +} + +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + usage + exit 0 +fi +if [[ "$#" -ne 0 ]]; then + usage >&2 + exit 2 +fi + require() { if [[ -z "${!1:-}" ]]; then - echo "[deploy-testnet] missing required env var: $1" >&2 - exit 1 + printf '[deploy-testnet] missing required env var: %s\n' "$1" >&2 + exit 2 fi } -require CANTON_LEDGER_URL -require CANTON_LEDGER_TOKEN -require CANTON_OPERATOR -require CANTON_LP_REGISTRAR -require CANTON_ADMIN +for required_var in \ + CANTON_LEDGER_URL CANTON_LEDGER_TOKEN \ + CANTON_OPERATOR CANTON_LP_REGISTRAR CANTON_ADMIN \ + CANTON_DEX_PACKAGE_ID; do + require "$required_var" +done AUTH="Authorization: Bearer ${CANTON_LEDGER_TOKEN}" -# 1. Build DARs ---------------------------------------------------------- - if [[ "${DEPLOY_SKIP_BUILD:-0}" != "1" ]]; then - echo "==> Building DARs" + printf '%s\n' '==> [1/4] Building DEX DARs' bash "$ROOT_DIR/scripts/build-trading-surface.sh" - (cd "$ROOT_DIR/trading-tests" && dpm build) else - echo "==> Skipping DAR build (DEPLOY_SKIP_BUILD=1)" + printf '%s\n' '==> [1/4] Build skipped (DEPLOY_SKIP_BUILD=1)' fi -# 2. Upload DARs --------------------------------------------------------- - upload_dar() { local dar="$1" - echo " uploading: $dar" + printf ' upload %s\n' "${dar#"$ROOT_DIR"/}" curl -fsS -X POST \ -H "$AUTH" \ -H "Content-Type: application/octet-stream" \ --data-binary "@$dar" \ - "${CANTON_LEDGER_URL}/v2/packages" >/dev/null + "${CANTON_LEDGER_URL%/}/v2/packages" >/dev/null } if [[ "${DEPLOY_SKIP_UPLOAD:-0}" != "1" ]]; then - echo "==> Uploading DARs to $CANTON_LEDGER_URL" - for dar in \ - "$ROOT_DIR"/vendor/splice/daml/splice-util-token-standard-wallet/.daml/dist/splice-util-token-standard-wallet-current.dar \ - "$ROOT_DIR"/trading/.daml/dist/*.dar \ - "$ROOT_DIR"/trading-tests/.daml/dist/*.dar; do - [[ -f "$dar" ]] && upload_dar "$dar" - done + printf '%s\n' '==> [2/4] Uploading package closure' + # A DAR already contains its transitive DALF dependency closure. Select the + # exact name/version declared in daml.yaml: globbing dist/*.dar can pick up + # stale builds whose old dependency hashes share a package name/version and + # Canton correctly rejects that ambiguous package-vetting request. + read -r dex_name dex_version < <(node -e ' + const fs = require("node:fs"); + const yaml = fs.readFileSync(process.argv[1], "utf8"); + const field = (name) => yaml.match(new RegExp(`^${name}:\\s*(.+)$`, "m"))?.[1]?.trim(); + const packageName = field("name"); + const version = field("version"); + if (!packageName || !version) process.exit(1); + process.stdout.write(`${packageName} ${version}\n`); + ' "$ROOT_DIR/trading/daml.yaml") + dex_dar="$ROOT_DIR/trading/.daml/dist/${dex_name}-${dex_version}.dar" + if [[ ! -f "$dex_dar" ]]; then + printf '[deploy-testnet] expected current DAR not found: %s\n' "$dex_dar" >&2 + printf '%s\n' '[deploy-testnet] run without DEPLOY_SKIP_BUILD to create it' >&2 + exit 1 + fi + upload_dar "$dex_dar" + printf '%s\n' ' uploaded 1 DAR (including its Token Standard dependency closure)' else - echo "==> Skipping DAR upload (DEPLOY_SKIP_UPLOAD=1)" + printf '%s\n' '==> [2/4] Upload skipped (DEPLOY_SKIP_UPLOAD=1)' fi -# 3. Allocate parties ---------------------------------------------------- - -allocate_party() { - local hint="$1" - echo " allocating party hint=$hint" - curl -fsS -X POST \ - -H "$AUTH" \ - -H "Content-Type: application/json" \ - -d "{\"partyIdHint\":\"$hint\"}" \ - "${CANTON_LEDGER_URL}/v2/parties" >/dev/null || true -} - -if [[ "${DEPLOY_SKIP_PARTIES:-0}" != "1" ]]; then - echo "==> Allocating parties (idempotent; existing parties are no-op)" - allocate_party "$CANTON_OPERATOR" - allocate_party "$CANTON_LP_REGISTRAR" - allocate_party "$CANTON_ADMIN" - allocate_party "trader-demo" +if [[ "${DEPLOY_SKIP_BOOTSTRAP:-0}" != "1" ]]; then + printf '%s\n' '==> [3/4] Bootstrapping Registry.V2 contracts' + # tsx is a backend dependency; running from this directory makes a clean + # clone work without a nonexistent root node_modules. Install the locked + # dependency tree only when it is not already present. + if [[ ! -x "$ROOT_DIR/services/operator-backend/node_modules/.bin/tsx" ]]; then + (cd "$ROOT_DIR/services/operator-backend" && npm ci) + fi + (cd "$ROOT_DIR/services/operator-backend" && \ + node --import tsx ../../scripts/bootstrap-registry.ts) else - echo "==> Skipping party allocation (DEPLOY_SKIP_PARTIES=1)" + printf '%s\n' '==> [3/4] Bootstrap skipped (DEPLOY_SKIP_BOOTSTRAP=1)' fi -# 4. Registry bootstrap -------------------------------------------------- - -echo "==> Running registry bootstrap" -echo " (instrument configs, LP configs, credentials, and the lpRegistrar's" -echo " Registry.V2 -- without which liquidity cannot be allocated)" -(cd "$ROOT_DIR" && node --import tsx scripts/bootstrap-registry.ts) - -# 5. Seed initial pair/pool --------------------------------------------- - -if [[ "${DEPLOY_SKIP_SEED:-0}" != "1" && -n "${OPERATOR_ADMIN_TOKEN:-}" ]]; then - echo "==> Seeding BTC/USDC pair (via operator admin API)" +if [[ "${DEPLOY_SEED_MARKETS:-0}" == "1" ]]; then + require OPERATOR_ADMIN_TOKEN API_BASE="${API_BASE:-http://localhost:8080}" - curl -fsS -X POST \ - -H "Authorization: Bearer ${OPERATOR_ADMIN_TOKEN}" \ - -H "Content-Type: application/json" \ - -d '{"baseInstrumentId":"BTC","quoteInstrumentId":"USDC","feeModel":{"makerFeeBps":10,"takerFeeBps":30,"poolFeeBps":30},"tradingMode":"TM_Both"}' \ - "${API_BASE}/v1/admin/pairs" || echo " (pair may already exist; continuing)" -else - echo "==> Skipping initial pair/pool seed" -fi - -# 6. Health check -------------------------------------------------------- + BASE="${DEPLOY_BASE:-BTC}" + QUOTE="${DEPLOY_QUOTE:-USDC}" + LP_INSTRUMENT="${DEPLOY_LP_INSTRUMENT:-${BASE}-${QUOTE}-LP}" + POOL_FEE_BPS="${DEPLOY_POOL_FEE_BPS:-30}" + + printf '%s\n' '==> [4/4] Creating pair and unfunded pool through operator API' + # This is a precondition, not an informational health check: market seeding + # cannot work until the fail-closed backend is running. + curl -fsS "${API_BASE%/}/v1/status" >/dev/null + + pairs_json="$(curl -fsS "${API_BASE%/}/v1/pairs")" + pair_exists="$(PAIRS_JSON="$pairs_json" BASE="$BASE" QUOTE="$QUOTE" node -e ' + const rows = JSON.parse(process.env.PAIRS_JSON || "[]"); + process.stdout.write(rows.some((p) => p.baseInstrumentId === process.env.BASE && p.quoteInstrumentId === process.env.QUOTE) ? "1" : "0"); + ')" + if [[ "$pair_exists" == "0" ]]; then + pair_payload="$(CANTON_ADMIN="$CANTON_ADMIN" BASE="$BASE" QUOTE="$QUOTE" \ + POOL_FEE_BPS="$POOL_FEE_BPS" node -e ' + process.stdout.write(JSON.stringify({ + admin: process.env.CANTON_ADMIN, + baseInstrumentId: process.env.BASE, + quoteInstrumentId: process.env.QUOTE, + feeModel: { + makerFeeBps: 10, + takerFeeBps: 30, + poolFeeBps: Number(process.env.POOL_FEE_BPS), + }, + tradingMode: "TM_Both", + active: true, + })); + ')" + curl -fsS -X POST \ + -H "Authorization: Bearer ${OPERATOR_ADMIN_TOKEN}" \ + -H "Content-Type: application/json" \ + --data-binary "$pair_payload" \ + "${API_BASE%/}/v1/admin/pairs" >/dev/null + printf ' created pair %s/%s\n' "$BASE" "$QUOTE" + else + printf ' pair %s/%s already exists\n' "$BASE" "$QUOTE" + fi -echo "==> Health check" -API_BASE="${API_BASE:-http://localhost:8080}" -if curl -fsS "${API_BASE}/v1/status" >/dev/null 2>&1; then - echo " operator backend reachable at ${API_BASE}" + pools_json="$(curl -fsS "${API_BASE%/}/v1/pools")" + pool_exists="$(POOLS_JSON="$pools_json" BASE="$BASE" QUOTE="$QUOTE" node -e ' + const rows = JSON.parse(process.env.POOLS_JSON || "[]"); + process.stdout.write(rows.some((p) => p.baseInstrumentId === process.env.BASE && p.quoteInstrumentId === process.env.QUOTE) ? "1" : "0"); + ')" + if [[ "$pool_exists" == "0" ]]; then + pool_payload="$(CANTON_LP_REGISTRAR="$CANTON_LP_REGISTRAR" \ + CANTON_ADMIN="$CANTON_ADMIN" BASE="$BASE" QUOTE="$QUOTE" \ + LP_INSTRUMENT="$LP_INSTRUMENT" POOL_FEE_BPS="$POOL_FEE_BPS" node -e ' + process.stdout.write(JSON.stringify({ + lpRegistrar: process.env.CANTON_LP_REGISTRAR, + admin: process.env.CANTON_ADMIN, + baseInstrumentId: process.env.BASE, + quoteInstrumentId: process.env.QUOTE, + lpInstrumentId: process.env.LP_INSTRUMENT, + feeBps: Number(process.env.POOL_FEE_BPS), + })); + ')" + curl -fsS -X POST \ + -H "Authorization: Bearer ${OPERATOR_ADMIN_TOKEN}" \ + -H "Content-Type: application/json" \ + --data-binary "$pool_payload" \ + "${API_BASE%/}/v1/admin/pools" >/dev/null + printf ' created UNFUNDED pool %s/%s; fund it through an LP wallet flow\n' "$BASE" "$QUOTE" + else + printf ' pool %s/%s already exists\n' "$BASE" "$QUOTE" + fi else - echo " operator backend not reachable at ${API_BASE} (start it separately)" + printf '%s\n' '==> [4/4] Market metadata skipped (set DEPLOY_SEED_MARKETS=1 after backend startup)' fi -echo "==> Deployment complete" +printf '%s\n' '==> Deployment phases completed without a suppressed error' diff --git a/scripts/live-amm-roundtrip.ts b/scripts/live-amm-roundtrip.ts new file mode 100644 index 00000000..179e14a3 --- /dev/null +++ b/scripts/live-amm-roundtrip.ts @@ -0,0 +1,913 @@ +// Headless AMM liquidity round trip against a live Canton participant. +// +// Stands in for the trader's wallet (the one piece a browser CIP-0103 +// wallet normally does): it authors the trader's allocations and drives a +// self-contained add -> swap -> partial remove through the JSON Ledger API. +// +// It does NOT exercise the operator HTTP server, dApp, a real wallet transport, +// or browser authentication. Those boundaries need separate tests. +// +// Self-contained: creates its own V2 Registry (admin == pool admin == +// lpRegistrar, the self-registry case), registers base/quote/LP +// instruments, mints to the LP and swapper, creates the pool contracts, then: +// 1. adds liquidity and asserts reserves + LP supply/holding; +// 2. swaps quote -> base and asserts exact balances/reserves + x*y; +// 3. redeems half the LP position and asserts returned balances, remaining +// reserves/slices/supply, and reserve-per-LP invariants. +// +// STATE WARNING: a successful or partially failed run leaves contracts on the +// participant. Use a throwaway LocalNet. The unique `dvp-` run id +// printed at startup identifies the pool and command ids left by this run. +// +// Env: +// CANTON_LEDGER_URL, CANTON_LEDGER_TOKEN, +// CANTON_DEX_PACKAGE_ID (e.g. #canton-dex-trading), +// CANTON_ALLOC_INSTR_PACKAGE_ID +// (e.g. #splice-api-token-allocation-instruction-v2), +// CANTON_USER_ID (default ledger-api-user), +// CANTON_SYNCHRONIZER (optional; omit to let a single-synchronizer +// participant route the submission), +// CANTON_OPERATOR, CANTON_ADMIN, CANTON_TRADER, +// CANTON_SWAPPER (optional; defaults to CANTON_TRADER) +// (operator == venue; admin == instrument issuer == lpRegistrar; +// trader == the LP). A full round trip requires swapper != operator because +// a swap cannot contain self-transfer legs. The token must have actAs for +// every distinct configured party. +// +// Run from services/operator-backend (which has tsx on its path): +// npm run live:roundtrip # add -> swap -> partial remove +// npm run live:add-liquidity # add only; still needs trader != operator + +import * as dec from "../services/operator-backend/src/pool/decimal.js"; + +function req(name: string): string { + const v = process.env[name]; + if (!v) { console.error(`missing env: ${name}`); process.exit(2); } + return v; +} + +const trader = req("CANTON_TRADER"); +const cfg = { + baseUrl: req("CANTON_LEDGER_URL"), + token: req("CANTON_LEDGER_TOKEN"), + sync: process.env.CANTON_SYNCHRONIZER || undefined, + pkg: req("CANTON_DEX_PACKAGE_ID"), + userId: process.env.CANTON_USER_ID ?? "ledger-api-user", + operator: req("CANTON_OPERATOR"), + admin: req("CANTON_ADMIN"), + trader, + swapper: process.env.CANTON_SWAPPER ?? trader, + // AllocationFactory_Allocate is a token-standard INTERFACE choice; it must + // be exercised against the interface id (alloc-instruction-v2 package), + // not the concrete Registry template. + pkgAllocInstr: req("CANTON_ALLOC_INSTR_PACKAGE_ID"), +}; +const lpRegistrar = cfg.admin; // self-registry: admin issues base/quote AND LP + +const RUN = `dvp-${Date.now()}`; +const BASE = "BTC", QUOTE = "USDC", LP = `BTC-USDC-LP-${RUN}`; +const ADD_BASE = "4.0", ADD_QUOTE = "12000.0"; +const SWAP_IN = "1000.0"; // USDC -> BTC +const FEE_BPS = 30; +const CAP = "1000000000.0"; +const ADD_ONLY = process.argv.includes("--add-only"); +const unknownArgs = process.argv.slice(2).filter((arg) => arg !== "--add-only"); +if (unknownArgs.length > 0) { + console.error(`unknown argument(s): ${unknownArgs.join(", ")}`); + process.exit(2); +} +if (cfg.trader === cfg.operator) { + console.error( + "liquidity flow requires CANTON_TRADER != CANTON_OPERATOR because a deposit cannot self-transfer", + ); + process.exit(2); +} +if (!ADD_ONLY && cfg.swapper === cfg.operator) { + console.error( + "full round trip requires CANTON_SWAPPER != CANTON_OPERATOR; " + + "use a second sandbox party or pass --add-only", + ); + process.exit(2); +} +const tid = (m: string) => `${cfg.pkg}:${m}`; +const acct = (p: string) => ({ owner: p, provider: null, id: "" }); +const EXTRA = { context: { values: {} }, meta: { values: {} } }; + +interface Created { contractId: string; templateId: string; createArgument: Record } +interface Exercised { choice: string; exerciseResult: unknown } +type Ev = + | { CreatedEvent: Created } + | { ArchivedEvent: { contractId: string } } + | { ExercisedEvent: Exercised }; +interface Tx { transaction: { updateId: string; events: Ev[] } } +interface PoolStateArg { + poolId: string; + status: string; + reserves: { baseAmount: string; quoteAmount: string }; + totalLpSupply: string; +} +interface SliceArg { poolId: string; operator: string; side: string; amount: string } +interface HoldingArg { + admin: string; owner: string; instrumentId: string; amount: string; locked?: boolean; +} +interface RequestArg { allocations: unknown[]; settlement: unknown } +interface PolicyArg { totalSupply: string; lpInstrumentId: { admin: string; id: string } } +interface SwapRequestResult { + settlement: unknown; + allocationSpec: unknown; + quoteBinding: SwapQuoteBinding | null; +} +interface SwapQuoteBinding { + expectedPoolId: string; + poolStateCid: string; + inputSliceCid: string; + outputSliceCids: string[]; + minOutputAmount: string; +} + +const argOf = (created: Created): T => created.createArgument as unknown as T; + +function only(values: T[], what: string): T { + if (values.length !== 1) { + throw new Error(`expected exactly 1 ${what}, found ${values.length}`); + } + return values[0]!; +} + +// Canton 3.x JSON API encodes Daml Int64 as a JSON string. Coerce every +// integer-valued number before submission. +function encInt(v: unknown): unknown { + if (typeof v === "number") return Number.isInteger(v) ? String(v) : v; + if (Array.isArray(v)) return v.map(encInt); + if (v !== null && typeof v === "object") { + const o: Record = {}; + for (const [k, val] of Object.entries(v)) o[k] = encInt(val); + return o; + } + return v; +} + +async function submit( + actAs: string[], cid: string, commands: unknown[], readAs: string[] = [], +): Promise { + const uniqueActAs = [...new Set(actAs)]; + const uniqueReadAs = [...new Set(readAs)].filter((party) => !uniqueActAs.includes(party)); + const res = await fetch(`${cfg.baseUrl}/v2/commands/submit-and-wait-for-transaction`, { + method: "POST", + headers: { Authorization: `Bearer ${cfg.token}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + commands: { + commandId: cid, + userId: cfg.userId, + actAs: uniqueActAs, + ...(uniqueReadAs.length > 0 ? { readAs: uniqueReadAs } : {}), + ...(cfg.sync ? { synchronizerId: cfg.sync } : {}), + commands: encInt(commands), + }, + transactionShape: "TRANSACTION_SHAPE_ACS_DELTA", + }), + }); + const t = await res.text(); + if (!res.ok) throw new Error(`submit ${cid} -> HTTP ${res.status}: ${t}`); + return JSON.parse(t) as Tx; +} +function creates(tx: Tx, suffix: string): Created[] { + return tx.transaction.events + .filter((e): e is { CreatedEvent: Created } => "CreatedEvent" in e) + .map((e) => e.CreatedEvent) + .filter((c) => c.templateId.endsWith(suffix)); +} +function exercisedResult(tx: Tx, choice: string): unknown { + for (const event of tx.transaction.events) { + if ("ExercisedEvent" in event && event.ExercisedEvent.choice === choice) { + return event.ExercisedEvent.exerciseResult; + } + } + return undefined; +} +async function treeExercisedResult( + updateId: string, party: string, choice: string, +): Promise { + const url = new URL( + `/v2/updates/transaction-tree-by-id/${encodeURIComponent(updateId)}`, + cfg.baseUrl, + ); + url.searchParams.append("parties", party); + const response = await fetch(url.toString(), { + headers: { Authorization: `Bearer ${cfg.token}` }, + }); + if (!response.ok) return undefined; + const body = (await response.json()) as { + transaction?: { eventsById?: Record }; + }; + for (const event of Object.values(body.transaction?.eventsById ?? {})) { + const exercised = event.ExercisedTreeEvent?.value; + if (exercised?.choice === choice) return exercised.exerciseResult; + } + return undefined; +} +async function ledgerEnd(): Promise { + const r = await fetch(`${cfg.baseUrl}/v2/state/ledger-end`, { headers: { Authorization: `Bearer ${cfg.token}` } }); + if (!r.ok) throw new Error(`ledger end -> HTTP ${r.status}: ${await r.text()}`); + return ((await r.json()) as { offset: number }).offset; +} +async function acs(party: string, template: string): Promise { + const offset = await ledgerEnd(); + const r = await fetch(`${cfg.baseUrl}/v2/state/active-contracts`, { + method: "POST", + headers: { Authorization: `Bearer ${cfg.token}`, "Content-Type": "application/json" }, + body: JSON.stringify({ + verbose: false, activeAtOffset: offset, + filter: { filtersByParty: { [party]: { cumulative: [ + { identifierFilter: { TemplateFilter: { value: { templateId: tid(template), includeCreatedEventBlob: false } } } }, + ] } } }, + }), + }); + if (!r.ok) throw new Error(`ACS ${template} -> HTTP ${r.status}: ${await r.text()}`); + const body = (await r.json()) as Array<{ contractEntry?: { JsActiveContract?: { createdEvent?: Created } } }>; + return body.map((e) => e.contractEntry?.JsActiveContract?.createdEvent).filter((x): x is Created => !!x); +} +async function step(name: string, fn: () => Promise): Promise { + const t0 = Date.now(); + try { const out = await fn(); console.log(` ok ${name} (${Date.now() - t0}ms)`); return out; } + catch (e) { console.error(` FAIL ${name}: ${(e as Error).message}`); throw e; } +} +const eq = (a: unknown, b: unknown, m: string) => { + if (String(a) !== String(b)) throw new Error(`assert ${m}: expected ${b}, got ${a}`); +}; +const eqDec = (a: bigint, b: bigint, m: string) => { + if (a !== b) { + throw new Error( + `assert ${m}: expected ${dec.formatDecimal(b)}, got ${dec.formatDecimal(a)}`, + ); + } +}; +const atLeastRaw = (a: bigint, b: bigint, m: string) => { + if (a < b) throw new Error(`assert ${m}: expected left side >= right side`); +}; + +const sum = (values: bigint[]): bigint => values.reduce((total, value) => total + value, 0n); + +function constantProductOut( + reserveIn: bigint, reserveOut: bigint, feeBps: number, inputAmount: bigint, +): bigint { + const feeNumerator = dec.parseDecimal(String(10000 - feeBps)); + const feeDenominator = dec.parseDecimal("10000"); + const afterFee = dec.divFloor( + dec.mulFloor(inputAmount, feeNumerator), + feeDenominator, + ); + return dec.divFloor(dec.mulFloor(afterFee, reserveOut), reserveIn + afterFee); +} + +function coveringPlan(slices: Created[], target: bigint, side: string): { + cids: string[]; + outs: string[]; +} { + let remaining = target; + const cids: string[] = []; + const outs: string[] = []; + for (const slice of slices) { + if (remaining <= 0n) break; + const amount = dec.parseDecimal(argOf(slice).amount); + const drawn = amount < remaining ? amount : remaining; + cids.push(slice.contractId); + outs.push(dec.formatDecimal(drawn)); + remaining -= drawn; + } + if (remaining > 0n) { + throw new Error( + `${side} slices cannot cover ${dec.formatDecimal(target)}; short ${dec.formatDecimal(remaining)}`, + ); + } + return { cids, outs }; +} + +// Author one allocation as the trader (the wallet's job): exercise +// AllocationFactory_Allocate on the registry, locking inputHoldingCids. +async function authorAlloc( + regCid: string, + party: string, + settlement: unknown, + allocation: unknown, + inputHoldingCids: string[], + label: string, +): Promise { + const tx = await submit([party], `${RUN}-author-${label}`, [{ + ExerciseCommand: { + templateId: `${cfg.pkgAllocInstr}:Splice.Api.Token.AllocationInstructionV2:AllocationFactory`, + contractId: regCid, + choice: "AllocationFactory_Allocate", + choiceArgument: { + settlement, + allocation, + requestedAt: new Date().toISOString(), + inputHoldingCids, + extraArgs: EXTRA, + actors: [party], + }, + }, + }]); + return only( + creates(tx, "CantonDex.Registry.V2:Allocation"), + `${label} allocation`, + ).contractId; +} + +async function holdingsFor( + party: string, + instrumentId: string, +): Promise> { + const holdings = await acs(party, "CantonDex.Registry.V2:Holding"); + return holdings + .map((created) => ({ cid: created.contractId, arg: argOf(created) })) + .filter( + ({ arg }) => + arg.owner === party && + arg.admin === cfg.admin && + arg.instrumentId === instrumentId && + !arg.locked, + ) + .map(({ cid, arg }) => ({ cid, amount: arg.amount })); +} + +async function balance(party: string, instrumentId: string): Promise { + return sum((await holdingsFor(party, instrumentId)).map((holding) => dec.parseDecimal(holding.amount))); +} + +async function poolSlices(poolId: string): Promise<{ base: Created[]; quote: Created[] }> { + const slices = (await acs(cfg.operator, "CantonDex.Dex.PoolSlice:PoolSlice")) + .filter((created) => { + const arg = argOf(created); + return arg.poolId === poolId && arg.operator === cfg.operator; + }); + return { + base: slices.filter((created) => argOf(created).side === "BaseSide"), + quote: slices.filter((created) => argOf(created).side === "QuoteSide"), + }; +} + +function sliceTotal(slices: Created[]): bigint { + return sum(slices.map((created) => dec.parseDecimal(argOf(created).amount))); +} + +async function reconcile( + rulesCid: string, + poolId: string, + poolCid: string, + poolStateCid: string, +): Promise { + const slices = await poolSlices(poolId); + const sliceCids = [...slices.base, ...slices.quote].map((created) => created.contractId); + await submit([cfg.operator], `${RUN}-reconcile-${Date.now()}`, [{ + ExerciseCommand: { + templateId: tid("CantonDex.Dex.PoolRules:PoolRules"), + contractId: rulesCid, + choice: "PoolRules_ReconcileState", + choiceArgument: { expectedPoolId: poolId, poolCid, poolStateCid, sliceCids }, + }, + }]); + return sliceCids.length; +} + +async function main() { + console.log(`run ${RUN}`); + console.log( + `operator=${cfg.operator.slice(0, 20)}.. admin=${cfg.admin.slice(0, 20)}.. ` + + `trader=${cfg.trader.slice(0, 20)}.. swapper=${cfg.swapper.slice(0, 20)}..`, + ); + + // 1. Registry + instruments + trader holdings --------------------------- + const regCid = await step("create Registry.V2 (factory + settlement)", async () => { + const tx = await submit([cfg.admin], `${RUN}-reg`, [{ + CreateCommand: { + templateId: tid("CantonDex.Registry.V2:Registry"), + createArguments: { + admin: cfg.admin, + users: [...new Set([cfg.operator, cfg.trader, cfg.swapper])], + }, + }, + }]); + return creates(tx, "CantonDex.Registry.V2:Registry")[0]!.contractId; + }); + // RegisterInstrument returns an InstrumentConfig; Mint consumes the + // latest config (BumpSupply) and rotates it. Track per-instrument. + const configCid: Record = {}; + for (const id of [BASE, QUOTE, LP]) { + await step(`register ${id}`, async () => { + const tx = await submit([cfg.admin], `${RUN}-reg-${id}`, [{ + ExerciseCommand: { + templateId: tid("CantonDex.Registry.V2:Registry"), contractId: regCid, + choice: "Registry_RegisterInstrument", + choiceArgument: { + instrumentId: id, decimals: "10", supplyCap: CAP, + holderRequirements: [], issuerRequirements: [], isin: null, cusip: null, + }, + }, + }]); + configCid[id] = creates(tx, "CantonDex.Registry.V2:InstrumentConfig")[0]!.contractId; + }); + } + const mint = (id: string, amt: string, owner: string) => + step(`mint ${amt} ${id} -> ${owner === cfg.trader ? "trader" : owner.slice(0, 8)}`, async () => { + const tx = await submit([cfg.admin, owner], `${RUN}-mint-${id}-${owner.slice(0, 6)}-${Date.now()}`, [{ + ExerciseCommand: { + templateId: tid("CantonDex.Registry.V2:Registry"), contractId: regCid, + choice: "Registry_Mint", + choiceArgument: { configCid: configCid[id], owner, amount: amt, issuerClaims: [] }, + }, + }]); + configCid[id] = creates(tx, "CantonDex.Registry.V2:InstrumentConfig")[0]!.contractId; + return creates(tx, "CantonDex.Registry.V2:Holding")[0]!.contractId; + }); + // Snapshot unrelated unlocked inventory before this run mints anything. + // The driver is safe to repeat on a persistent LocalNet, so conservation + // must compare deltas instead of pretending the participant was pristine. + const valueParties = [...new Set([cfg.trader, cfg.swapper])]; + const initialUnlockedBase = sum( + await Promise.all(valueParties.map((party) => balance(party, BASE))), + ); + const initialUnlockedQuote = sum( + await Promise.all(valueParties.map((party) => balance(party, QUOTE))), + ); + await mint(BASE, ADD_BASE, cfg.trader); + await mint(QUOTE, ADD_QUOTE, cfg.trader); + if (!ADD_ONLY) await mint(QUOTE, SWAP_IN, cfg.swapper); + + // 2. Pool contracts (operator-authored), as the admin bootstrap does ---- + // Unique poolId per run so we never collide with other pools the + // operator observes (which would make a poolId-based lookup ambiguous). + const poolId = `${BASE}-${QUOTE}-${RUN}`; + const lpInstrumentId = { admin: lpRegistrar, id: LP }; + const poolCid = await step("create Pool", async () => { + const tx = await submit([cfg.operator], `${RUN}-pool`, [{ + CreateCommand: { + templateId: tid("CantonDex.Dex.Pool:Pool"), + createArguments: { + poolId, operator: cfg.operator, lpRegistrar, admin: cfg.admin, + baseInstrumentId: BASE, quoteInstrumentId: QUOTE, lpInstrumentId, + feeBps: "30", + }, + }, + }]); + return creates(tx, "CantonDex.Dex.Pool:Pool")[0]!.contractId; + }); + let stateCid = await step("create PoolState (Unfunded)", async () => { + const tx = await submit([cfg.operator], `${RUN}-state`, [{ + CreateCommand: { + templateId: tid("CantonDex.Dex.PoolState:PoolState"), + createArguments: { + poolId, operator: cfg.operator, lpRegistrar, status: "PS_Unfunded", + reserves: { baseAmount: "0.0", quoteAmount: "0.0" }, totalLpSupply: "0.0", publicReaders: [], + }, + }, + }]); + return creates(tx, "CantonDex.Dex.PoolState:PoolState")[0]!.contractId; + }); + const rulesCid = await step("create PoolRules", async () => { + const tx = await submit([cfg.operator], `${RUN}-rules`, [{ + CreateCommand: { templateId: tid("CantonDex.Dex.PoolRules:PoolRules"), createArguments: { operator: cfg.operator } }, + }]); + return only(creates(tx, "CantonDex.Dex.PoolRules:PoolRules"), "PoolRules").contractId; + }); + const dvpCid = await step("create PoolLiquidityRules", async () => { + const tx = await submit([cfg.operator, lpRegistrar], `${RUN}-dvp`, [{ + CreateCommand: { + templateId: tid("CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules"), + createArguments: { operator: cfg.operator, lpRegistrar }, + }, + }]); + return creates(tx, "CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules")[0]!.contractId; + }); + let policyCid = await step("create LPTokenPolicy", async () => { + const tx = await submit([lpRegistrar], `${RUN}-policy`, [{ + CreateCommand: { + templateId: tid("CantonDex.Lp.Policy:LPTokenPolicy"), + createArguments: { lpRegistrar, operator: cfg.operator, lpInstrumentId, totalSupply: "0.0", active: true }, + }, + }]); + return creates(tx, "CantonDex.Lp.Policy:LPTokenPolicy")[0]!.contractId; + }); + + // 3. DvP ADD: request -> author 3 allocations -> settle ----------------- + console.log("\n== ADD LIQUIDITY =="); + const reqAdd = await step("PoolLiquidityRules_RequestAddLiquidity", async () => { + const lpAmount = dec.formatDecimal( + dec.sqrt(dec.mul(dec.parseDecimal(ADD_BASE), dec.parseDecimal(ADD_QUOTE))), + ); + const tx = await submit([cfg.operator], `${RUN}-add-req`, [{ + ExerciseCommand: { + templateId: tid("CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules"), contractId: dvpCid, + choice: "PoolLiquidityRules_RequestAddLiquidity", + choiceArgument: { + poolCid, recipient: cfg.trader, baseAmount: ADD_BASE, quoteAmount: ADD_QUOTE, + lpAmount, requestedAt: new Date().toISOString(), settleAt: null, + }, + }, + }]); + const r = creates(tx, "CantonDex.Dex.LiquidityAllocationRequest:LiquidityAllocationRequest")[0]!; + return { cid: r.contractId, arg: r.createArgument as { allocations: unknown[]; settlement: unknown } }; + }); + const addBaseH = (await holdingsFor(cfg.trader, BASE)) + .find((holding) => dec.parseDecimal(holding.amount) === dec.parseDecimal(ADD_BASE)); + const addQuoteH = (await holdingsFor(cfg.trader, QUOTE)) + .find((holding) => dec.parseDecimal(holding.amount) === dec.parseDecimal(ADD_QUOTE)); + if (!addBaseH || !addQuoteH) throw new Error("trader add-liquidity holdings were not found"); + const settlement = reqAdd.arg.settlement; + const [baseSpec, quoteSpec, receiptSpec] = reqAdd.arg.allocations; + if (!baseSpec || !quoteSpec || !receiptSpec) throw new Error("add request did not return 3 allocation specs"); + const baseDep = await step("trader authors base deposit", () => + authorAlloc(regCid, cfg.trader, settlement, baseSpec, [addBaseH.cid], "add-base")); + const quoteDep = await step("trader authors quote deposit", () => + authorAlloc(regCid, cfg.trader, settlement, quoteSpec, [addQuoteH.cid], "add-quote")); + const receipt = await step("trader authors LP receipt", () => + authorAlloc(regCid, cfg.trader, settlement, receiptSpec, [], "add-receipt")); + const addRes = await step("PoolLiquidityRules_SettleAddLiquidity", async () => { + const tx = await submit([cfg.operator, lpRegistrar], `${RUN}-add-settle`, [{ + ExerciseCommand: { + templateId: tid("CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules"), contractId: dvpCid, + choice: "PoolLiquidityRules_SettleAddLiquidity", + choiceArgument: { + expectedPoolId: poolId, poolCid, poolStateCid: stateCid, lpPolicyCid: policyCid, + requestCid: reqAdd.cid, acceptanceCid: null, recipient: cfg.trader, + lpBaseDepositCid: baseDep, lpQuoteDepositCid: quoteDep, lpReceiptCid: receipt, + baseFactoryCid: regCid, quoteFactoryCid: regCid, lpFactoryCid: regCid, + baseQuoteSettleCid: regCid, lpSettleCid: regCid, + baseAmount: ADD_BASE, quoteAmount: ADD_QUOTE, minLpTokens: "0.0", knownTotalLpSupply: "0.0", + requestedAt: new Date().toISOString(), poolAdminExtraArgs: EXTRA, lpRegistrarExtraArgs: EXTRA, + }, + }, + }]); + // This settle tx creates exactly one PoolState (for THIS pool); match + // it by poolId to be unambiguous even if [0] ordering ever changes. + const ps = creates(tx, "CantonDex.Dex.PoolState:PoolState") + .find((c) => (c.createArgument as { poolId: string }).poolId === poolId)!; + const policy = only( + creates(tx, "CantonDex.Lp.Policy:LPTokenPolicy") + .filter((created) => argOf(created).lpInstrumentId.id === LP), + "post-add LPTokenPolicy", + ); + stateCid = ps.contractId; + policyCid = policy.contractId; + return argOf(ps); + }); + const expectLp = dec.sqrt(dec.mul(dec.parseDecimal(ADD_BASE), dec.parseDecimal(ADD_QUOTE))); + eq(addRes.status, "PS_Active", "pool active after add"); + eqDec(dec.parseDecimal(addRes.reserves.baseAmount), dec.parseDecimal(ADD_BASE), "base reserve"); + eqDec(dec.parseDecimal(addRes.reserves.quoteAmount), dec.parseDecimal(ADD_QUOTE), "quote reserve"); + eqDec(dec.parseDecimal(addRes.totalLpSupply), expectLp, "LP minted = sqrt(base*quote)"); + console.log(` reserves ${addRes.reserves.baseAmount}/${addRes.reserves.quoteAmount}, LP ${addRes.totalLpSupply} (= sqrt(${ADD_BASE}*${ADD_QUOTE}))`); + // Confirm the trader actually received the LP holding (DvP, not just supply bump). + const lpHeld = (await acs(cfg.trader, "CantonDex.Registry.V2:Holding")) + .map((c) => c.createArgument as { owner: string; instrumentId: string; amount: string; locked?: boolean }) + .filter((p) => p.owner === cfg.trader && p.instrumentId === LP && !p.locked); + eq(lpHeld.length >= 1, true, "trader holds an LP holding"); + eqDec( + sum(lpHeld.map((holding) => dec.parseDecimal(holding.amount))), + expectLp, + "trader LP balance = minted", + ); + console.log(` trader LP holding: ${lpHeld.map((h) => h.amount).join("+")}`); + + const addSliceCount = await step("reconcile reserves against pool slices", () => + reconcile(rulesCid, poolId, poolCid, stateCid)); + console.log(` ${addSliceCount} pool slices reconcile exactly with reserves`); + + if (ADD_ONLY) { + console.log("\n== live-ledger add-liquidity probe complete =="); + console.log("PASS: add-liquidity DvP (trader authored all 3 allocations; operator+lpRegistrar settled)"); + console.log(`created ledger state: run=${RUN}, registry=${regCid}, pool=${poolId}`); + console.log("persistence is controlled by the enclosing environment; the DPM proof wrapper removes its throwaway sandbox"); + return; + } + + // 4. SWAP: quote snapshot -> wallet allocation -> atomic settle --------- + console.log("\n== SWAP QUOTE -> BASE =="); + const beforeSwapSlices = await step("read active pool slices", () => poolSlices(poolId)); + const inputSlice = beforeSwapSlices.quote[0]; + if (!inputSlice) throw new Error("pool has no quote slice to receive swap input"); + + const swapIn = dec.parseDecimal(SWAP_IN); + const oldBase = dec.parseDecimal(addRes.reserves.baseAmount); + const oldQuote = dec.parseDecimal(addRes.reserves.quoteAmount); + const expectedOut = constantProductOut(oldQuote, oldBase, FEE_BPS, swapIn); + if (expectedOut <= 0n || expectedOut >= oldBase) { + throw new Error(`invalid quoted output ${dec.formatDecimal(expectedOut)} ${BASE}`); + } + const outputPlan = coveringPlan(beforeSwapSlices.base, expectedOut, "base"); + const quoteBinding: SwapQuoteBinding = { + expectedPoolId: poolId, + poolStateCid: stateCid, + inputSliceCid: inputSlice.contractId, + outputSliceCids: outputPlan.cids, + minOutputAmount: dec.formatDecimal(expectedOut), + }; + + const swapHolding = (await holdingsFor(cfg.swapper, QUOTE)) + .find((holding) => dec.parseDecimal(holding.amount) === swapIn); + if (!swapHolding) throw new Error(`swapper has no unlocked ${SWAP_IN} ${QUOTE} holding`); + const swapperQuoteBefore = await balance(cfg.swapper, QUOTE); + const swapperBaseBefore = await balance(cfg.swapper, BASE); + + const swapRequest = await step("PoolRules_RequestSwap", async () => { + const tx = await submit([cfg.operator], `${RUN}-swap-request`, [{ + ExerciseCommand: { + templateId: tid("CantonDex.Dex.PoolRules:PoolRules"), + contractId: rulesCid, + choice: "PoolRules_RequestSwap", + choiceArgument: { + poolCid, + swapper: cfg.swapper, + inputInstrumentId: QUOTE, + inputAmount: SWAP_IN, + quoteBinding, + }, + }, + }]); + const result = exercisedResult(tx, "PoolRules_RequestSwap") + ?? await treeExercisedResult(tx.transaction.updateId, cfg.operator, "PoolRules_RequestSwap"); + if (!result) { + throw new Error( + "participant did not expose the PoolRules_RequestSwap result in the transaction or transaction tree", + ); + } + return result as SwapRequestResult; + }); + if (!swapRequest.quoteBinding) throw new Error("swap request returned no quote binding"); + eq(swapRequest.quoteBinding.poolStateCid, stateCid, "request is bound to current PoolState"); + eq( + swapRequest.quoteBinding.minOutputAmount, + quoteBinding.minOutputAmount, + "request preserves quoted minimum", + ); + + const swapAllocationCid = await step("swapper authors the exact swap allocation", () => + authorAlloc( + regCid, + cfg.swapper, + swapRequest.settlement, + swapRequest.allocationSpec, + [swapHolding.cid], + "swap-input", + )); + + const swapRes = await step("PoolRules_Swap", async () => { + const tx = await submit([cfg.operator], `${RUN}-swap-settle`, [{ + ExerciseCommand: { + templateId: tid("CantonDex.Dex.PoolRules:PoolRules"), + contractId: rulesCid, + choice: "PoolRules_Swap", + choiceArgument: { + expectedPoolId: poolId, + poolCid, + poolStateCid: stateCid, + swapperAccount: acct(cfg.swapper), + inputInstrumentId: QUOTE, + inputAmount: SWAP_IN, + minOutputAmount: quoteBinding.minOutputAmount, + swapperAllocationCid: swapAllocationCid, + inputSliceCid: quoteBinding.inputSliceCid, + outputSliceCids: quoteBinding.outputSliceCids, + factoryCid: regCid, + extraArgs: EXTRA, + quoteBinding, + }, + }, + }], [cfg.swapper]); + const state = only( + creates(tx, "CantonDex.Dex.PoolState:PoolState") + .filter((created) => argOf(created).poolId === poolId), + "post-swap PoolState", + ); + stateCid = state.contractId; + return argOf(state); + }); + + const postSwapBase = dec.parseDecimal(swapRes.reserves.baseAmount); + const postSwapQuote = dec.parseDecimal(swapRes.reserves.quoteAmount); + eq(swapRes.status, "PS_Active", "pool active after swap"); + eqDec(postSwapBase, oldBase - expectedOut, "base reserve after swap"); + eqDec(postSwapQuote, oldQuote + swapIn, "quote reserve after swap"); + eqDec( + dec.parseDecimal(swapRes.totalLpSupply), + dec.parseDecimal(addRes.totalLpSupply), + "swap does not change LP supply", + ); + atLeastRaw(postSwapBase * postSwapQuote, oldBase * oldQuote, "x*y does not decrease"); + eqDec( + swapperQuoteBefore - await balance(cfg.swapper, QUOTE), + swapIn, + "swapper quote balance paid", + ); + eqDec( + await balance(cfg.swapper, BASE) - swapperBaseBefore, + expectedOut, + "swapper base balance received", + ); + const postSwapSlices = await poolSlices(poolId); + eqDec(sliceTotal(postSwapSlices.base), postSwapBase, "base slices equal base reserve after swap"); + eqDec(sliceTotal(postSwapSlices.quote), postSwapQuote, "quote slices equal quote reserve after swap"); + const swapSliceCount = await step("reconcile post-swap reserves and slices", () => + reconcile(rulesCid, poolId, poolCid, stateCid)); + console.log( + ` ${SWAP_IN} ${QUOTE} -> ${dec.formatDecimal(expectedOut)} ${BASE}; ` + + `reserves ${swapRes.reserves.baseAmount}/${swapRes.reserves.quoteAmount}; ` + + `x*y non-decreasing; ${swapSliceCount} slices reconciled`, + ); + + // 5. REMOVE: request -> wallet allocations -> redeem half the LP -------- + console.log("\n== REMOVE HALF THE LP POSITION =="); + const supplyBeforeRemove = dec.parseDecimal(swapRes.totalLpSupply); + const redeemAmount = dec.divFloor(supplyBeforeRemove, dec.parseDecimal("2.0")); + if (redeemAmount <= 0n) throw new Error("half-position redemption rounded to zero"); + const share = dec.divFloor(redeemAmount, supplyBeforeRemove); + const baseOut = dec.mulFloor(postSwapBase, share); + const quoteOut = dec.mulFloor(postSwapQuote, share); + const removeSlices = await poolSlices(poolId); + const basePlan = coveringPlan(removeSlices.base, baseOut, "base"); + const quotePlan = coveringPlan(removeSlices.quote, quoteOut, "quote"); + + const lpHoldings = await holdingsFor(cfg.trader, LP); + const lpInputCids: string[] = []; + let lpCovered = 0n; + for (const holding of lpHoldings) { + lpInputCids.push(holding.cid); + lpCovered += dec.parseDecimal(holding.amount); + if (lpCovered >= redeemAmount) break; + } + if (lpCovered < redeemAmount) { + throw new Error( + `LP holdings cover ${dec.formatDecimal(lpCovered)}, need ${dec.formatDecimal(redeemAmount)}`, + ); + } + const traderBaseBeforeRemove = await balance(cfg.trader, BASE); + const traderQuoteBeforeRemove = await balance(cfg.trader, QUOTE); + const traderLpBeforeRemove = await balance(cfg.trader, LP); + + const removeRequest = await step("PoolLiquidityRules_RequestRemoveLiquidity", async () => { + const tx = await submit([cfg.operator], `${RUN}-remove-request`, [{ + ExerciseCommand: { + templateId: tid("CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules"), + contractId: dvpCid, + choice: "PoolLiquidityRules_RequestRemoveLiquidity", + choiceArgument: { + poolCid, + holder: cfg.trader, + baseOuts: basePlan.outs, + quoteOuts: quotePlan.outs, + lpBurnAmount: dec.formatDecimal(redeemAmount), + requestedAt: new Date().toISOString(), + settleAt: null, + }, + }, + }]); + const request = only( + creates(tx, "CantonDex.Dex.LiquidityAllocationRequest:LiquidityAllocationRequest"), + "remove LiquidityAllocationRequest", + ); + return { cid: request.contractId, arg: argOf(request) }; + }); + const [baseReceiptSpec, quoteReceiptSpec, burnSpec] = removeRequest.arg.allocations; + if (!baseReceiptSpec || !quoteReceiptSpec || !burnSpec) { + throw new Error("remove request did not return 3 allocation specs"); + } + const holderBaseReceiptCid = await step("trader authors base receipt", () => + authorAlloc( + regCid, + cfg.trader, + removeRequest.arg.settlement, + baseReceiptSpec, + [], + "remove-base-receipt", + )); + const holderQuoteReceiptCid = await step("trader authors quote receipt", () => + authorAlloc( + regCid, + cfg.trader, + removeRequest.arg.settlement, + quoteReceiptSpec, + [], + "remove-quote-receipt", + )); + const holderBurnSenderCid = await step("trader authors LP burn sender", () => + authorAlloc( + regCid, + cfg.trader, + removeRequest.arg.settlement, + burnSpec, + lpInputCids, + "remove-lp-burn", + )); + + const removeRes = await step("PoolLiquidityRules_SettleRemoveLiquidity", async () => { + const tx = await submit([cfg.operator, lpRegistrar], `${RUN}-remove-settle`, [{ + ExerciseCommand: { + templateId: tid("CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules"), + contractId: dvpCid, + choice: "PoolLiquidityRules_SettleRemoveLiquidity", + choiceArgument: { + expectedPoolId: poolId, + poolCid, + poolStateCid: stateCid, + lpPolicyCid: policyCid, + requestCid: removeRequest.cid, + acceptanceCid: null, + holder: cfg.trader, + lpTokensToRedeem: dec.formatDecimal(redeemAmount), + knownTotalLpSupply: dec.formatDecimal(supplyBeforeRemove), + minBaseOut: dec.formatDecimal(baseOut), + minQuoteOut: dec.formatDecimal(quoteOut), + baseSliceCids: basePlan.cids, + quoteSliceCids: quotePlan.cids, + holderBaseReceiptCid, + holderQuoteReceiptCid, + holderBurnSenderCid, + baseFactoryCid: regCid, + quoteFactoryCid: regCid, + lpFactoryCid: regCid, + baseQuoteSettleCid: regCid, + lpSettleCid: regCid, + requestedAt: new Date().toISOString(), + poolAdminExtraArgs: EXTRA, + lpRegistrarExtraArgs: EXTRA, + }, + }, + }]); + const state = only( + creates(tx, "CantonDex.Dex.PoolState:PoolState") + .filter((created) => argOf(created).poolId === poolId), + "post-remove PoolState", + ); + const policy = only( + creates(tx, "CantonDex.Lp.Policy:LPTokenPolicy") + .filter((created) => argOf(created).lpInstrumentId.id === LP), + "post-remove LPTokenPolicy", + ); + stateCid = state.contractId; + policyCid = policy.contractId; + return { state: argOf(state), policy: argOf(policy) }; + }); + + const finalBase = dec.parseDecimal(removeRes.state.reserves.baseAmount); + const finalQuote = dec.parseDecimal(removeRes.state.reserves.quoteAmount); + const finalSupply = dec.parseDecimal(removeRes.state.totalLpSupply); + eq(removeRes.state.status, "PS_Active", "partially redeemed pool remains active"); + eqDec(finalBase, postSwapBase - baseOut, "base reserve after remove"); + eqDec(finalQuote, postSwapQuote - quoteOut, "quote reserve after remove"); + eqDec(finalSupply, supplyBeforeRemove - redeemAmount, "LP supply after burn"); + eqDec(dec.parseDecimal(removeRes.policy.totalSupply), finalSupply, "policy supply equals PoolState supply"); + eqDec( + await balance(cfg.trader, BASE) - traderBaseBeforeRemove, + baseOut, + "LP received base payout", + ); + eqDec( + await balance(cfg.trader, QUOTE) - traderQuoteBeforeRemove, + quoteOut, + "LP received quote payout", + ); + eqDec( + traderLpBeforeRemove - await balance(cfg.trader, LP), + redeemAmount, + "LP holding burned", + ); + atLeastRaw(finalBase * supplyBeforeRemove, postSwapBase * finalSupply, "base per LP does not decrease"); + atLeastRaw(finalQuote * supplyBeforeRemove, postSwapQuote * finalSupply, "quote per LP does not decrease"); + + const finalSlices = await poolSlices(poolId); + eqDec(sliceTotal(finalSlices.base), finalBase, "final base slices equal reserve"); + eqDec(sliceTotal(finalSlices.quote), finalQuote, "final quote slices equal reserve"); + const finalSliceCount = await step("reconcile final reserves and slices", () => + reconcile(rulesCid, poolId, poolCid, stateCid)); + + const finalUnlockedBase = sum(await Promise.all(valueParties.map((party) => balance(party, BASE)))); + const finalUnlockedQuote = sum(await Promise.all(valueParties.map((party) => balance(party, QUOTE)))); + eqDec( + finalBase + finalUnlockedBase, + initialUnlockedBase + dec.parseDecimal(ADD_BASE), + "base value conserved", + ); + eqDec( + finalQuote + finalUnlockedQuote, + initialUnlockedQuote + dec.parseDecimal(ADD_QUOTE) + dec.parseDecimal(SWAP_IN), + "quote value conserved", + ); + + console.log( + ` burned ${dec.formatDecimal(redeemAmount)} ${LP}; returned ` + + `${dec.formatDecimal(baseOut)} ${BASE} + ${dec.formatDecimal(quoteOut)} ${QUOTE}`, + ); + console.log( + ` final reserves ${removeRes.state.reserves.baseAmount}/${removeRes.state.reserves.quoteAmount}; ` + + `LP supply ${removeRes.state.totalLpSupply}; ${finalSliceCount} slices reconciled`, + ); + console.log("\n== live-ledger AMM round trip complete =="); + console.log( + "PASS: add -> swap -> partial remove settled real holdings; balances, reserves, " + + "slice totals, LP supply, x*y, reserve-per-LP, and value conservation all hold", + ); + console.log(`created ledger state: run=${RUN}, registry=${regCid}, pool=${poolId}`); + console.log("persistence is controlled by the enclosing environment; the DPM proof wrapper removes its throwaway sandbox"); +} + +main().catch((e) => { console.error("FATAL", (e as Error).message); process.exit(1); }); diff --git a/scripts/localnet-dvp-e2e.ts b/scripts/localnet-dvp-e2e.ts deleted file mode 100644 index 3df0a5d0..00000000 --- a/scripts/localnet-dvp-e2e.ts +++ /dev/null @@ -1,322 +0,0 @@ -// Headless DvP liquidity end-to-end against a live Canton participant. -// -// Stands in for the trader's wallet (the one piece a browser CIP-0103 -// wallet normally does): it authors the trader's 3 allocations for each -// DvP add/remove, then settles. Exercises the full operator two-call -// flow (request -> wallet authors allocations -> settle) plus a swap, -// on a real ledger -- the seam that can't be driven through the UI -// without a human approving in the wallet popup. -// -// Self-contained: creates its own V2 Registry (admin == pool admin == -// lpRegistrar, the self-registry case), registers base/quote/LP -// instruments, mints to the trader, creates the pool contracts, then -// runs add -> swap -> remove and asserts the on-ledger reserves/LP. -// -// Env (all from the LocalNet bring-up): -// CANTON_LEDGER_URL, CANTON_LEDGER_TOKEN, CANTON_SYNCHRONIZER, -// CANTON_DEX_PACKAGE_ID (e.g. #canton-dex-trading), -// CANTON_USER_ID (default ledger-api-user), -// CANTON_OPERATOR, CANTON_ADMIN, CANTON_TRADER -// (operator == venue; admin == instrument issuer == lpRegistrar; -// trader == the LP/swapper). The user token must have actAs for all -// three parties (a single ledger-api-user with granted rights works). -// -// Run (from services/operator-backend, which has tsx on its path): -// npm run localnet:dvp-e2e -// with the CANTON_* env above exported. - -function req(name: string): string { - const v = process.env[name]; - if (!v) { console.error(`missing env: ${name}`); process.exit(2); } - return v; -} - -const cfg = { - baseUrl: req("CANTON_LEDGER_URL"), - token: req("CANTON_LEDGER_TOKEN"), - sync: req("CANTON_SYNCHRONIZER"), - pkg: req("CANTON_DEX_PACKAGE_ID"), - userId: process.env.CANTON_USER_ID ?? "ledger-api-user", - operator: req("CANTON_OPERATOR"), - admin: req("CANTON_ADMIN"), - trader: req("CANTON_TRADER"), - // AllocationFactory_Allocate is a token-standard INTERFACE choice; it must - // be exercised against the interface id (alloc-instruction-v2 package), - // not the concrete Registry template. - pkgAllocInstr: req("CANTON_ALLOC_INSTR_PACKAGE_ID"), -}; -const lpRegistrar = cfg.admin; // self-registry: admin issues base/quote AND LP - -const BASE = "BTC", QUOTE = "USDC", LP = "BTC-USDC-LP"; -const ADD_BASE = "4.0", ADD_QUOTE = "12000.0"; -const SWAP_IN = "1000.0"; // USDC -> BTC -const CAP = "1000000000.0"; -const RUN = `dvp-${Date.now()}`; -const tid = (m: string) => `${cfg.pkg}:${m}`; -const acct = (p: string) => ({ owner: p, provider: null, id: "" }); -const EXTRA = { context: { values: {} }, meta: { values: {} } }; - -interface Created { contractId: string; templateId: string; createArgument: Record } -type Ev = { CreatedEvent: Created } | { ArchivedEvent: { contractId: string } }; -interface Tx { transaction: { updateId: string; events: Ev[] } } - -// Canton 3.x JSON API encodes Daml Int64 as a JSON string. Coerce every -// integer-valued number before submission. -function encInt(v: unknown): unknown { - if (typeof v === "number") return Number.isInteger(v) ? String(v) : v; - if (Array.isArray(v)) return v.map(encInt); - if (v !== null && typeof v === "object") { - const o: Record = {}; - for (const [k, val] of Object.entries(v)) o[k] = encInt(val); - return o; - } - return v; -} - -async function submit(actAs: string[], cid: string, commands: unknown[]): Promise { - const res = await fetch(`${cfg.baseUrl}/v2/commands/submit-and-wait-for-transaction`, { - method: "POST", - headers: { Authorization: `Bearer ${cfg.token}`, "Content-Type": "application/json" }, - body: JSON.stringify({ - commands: { commandId: cid, userId: cfg.userId, actAs, synchronizerId: cfg.sync, commands: encInt(commands) }, - transactionShape: "TRANSACTION_SHAPE_ACS_DELTA", - }), - }); - const t = await res.text(); - if (!res.ok) throw new Error(`submit ${cid} -> HTTP ${res.status}: ${t}`); - return JSON.parse(t) as Tx; -} -function creates(tx: Tx, suffix: string): Created[] { - return tx.transaction.events - .filter((e): e is { CreatedEvent: Created } => "CreatedEvent" in e) - .map((e) => e.CreatedEvent) - .filter((c) => c.templateId.endsWith(suffix)); -} -async function ledgerEnd(): Promise { - const r = await fetch(`${cfg.baseUrl}/v2/state/ledger-end`, { headers: { Authorization: `Bearer ${cfg.token}` } }); - return ((await r.json()) as { offset: number }).offset; -} -async function acs(party: string, template: string): Promise { - const offset = await ledgerEnd(); - const r = await fetch(`${cfg.baseUrl}/v2/state/active-contracts`, { - method: "POST", - headers: { Authorization: `Bearer ${cfg.token}`, "Content-Type": "application/json" }, - body: JSON.stringify({ - verbose: false, activeAtOffset: offset, - filter: { filtersByParty: { [party]: { cumulative: [ - { identifierFilter: { TemplateFilter: { value: { templateId: tid(template), includeCreatedEventBlob: false } } } }, - ] } } }, - }), - }); - const body = (await r.json()) as Array<{ contractEntry?: { JsActiveContract?: { createdEvent?: Created } } }>; - return body.map((e) => e.contractEntry?.JsActiveContract?.createdEvent).filter((x): x is Created => !!x); -} -async function step(name: string, fn: () => Promise): Promise { - const t0 = Date.now(); - try { const out = await fn(); console.log(` ok ${name} (${Date.now() - t0}ms)`); return out; } - catch (e) { console.error(` FAIL ${name}: ${(e as Error).message}`); throw e; } -} -const eq = (a: unknown, b: unknown, m: string) => { - if (String(a) !== String(b)) throw new Error(`assert ${m}: expected ${b}, got ${a}`); -}; - -// Author one allocation as the trader (the wallet's job): exercise -// AllocationFactory_Allocate on the registry, locking inputHoldingCids. -async function authorAlloc( - regCid: string, spec: unknown, inputHoldingCids: string[], label: string, -): Promise { - const tx = await submit([cfg.trader], `${RUN}-author-${label}`, [{ - ExerciseCommand: { - templateId: `${cfg.pkgAllocInstr}:Splice.Api.Token.AllocationInstructionV2:AllocationFactory`, - contractId: regCid, - choice: "AllocationFactory_Allocate", - choiceArgument: { - settlement: (spec as { __settlement: unknown }).__settlement, - allocation: (spec as { __alloc: unknown }).__alloc, - requestedAt: new Date().toISOString(), - inputHoldingCids, - extraArgs: EXTRA, - actors: [cfg.trader], - }, - }, - }]); - return creates(tx, "CantonDex.Registry.V2:Allocation")[0]!.contractId; -} - -async function main() { - console.log(`run ${RUN}`); - console.log(`operator=${cfg.operator.slice(0, 20)}.. admin=${cfg.admin.slice(0, 20)}.. trader=${cfg.trader.slice(0, 20)}..`); - - // 1. Registry + instruments + trader holdings --------------------------- - const regCid = await step("create Registry.V2 (factory + settlement)", async () => { - const tx = await submit([cfg.admin], `${RUN}-reg`, [{ - CreateCommand: { - templateId: tid("CantonDex.Registry.V2:Registry"), - createArguments: { admin: cfg.admin, users: [cfg.operator, cfg.trader] }, - }, - }]); - return creates(tx, "CantonDex.Registry.V2:Registry")[0]!.contractId; - }); - // RegisterInstrument returns an InstrumentConfig; Mint consumes the - // latest config (BumpSupply) and rotates it. Track per-instrument. - const configCid: Record = {}; - for (const id of [BASE, QUOTE, LP]) { - await step(`register ${id}`, async () => { - const tx = await submit([cfg.admin], `${RUN}-reg-${id}`, [{ - ExerciseCommand: { - templateId: tid("CantonDex.Registry.V2:Registry"), contractId: regCid, - choice: "Registry_RegisterInstrument", - choiceArgument: { - instrumentId: id, decimals: "10", supplyCap: CAP, - holderRequirements: [], issuerRequirements: [], isin: null, cusip: null, - }, - }, - }]); - configCid[id] = creates(tx, "CantonDex.Registry.V2:InstrumentConfig")[0]!.contractId; - }); - } - const mint = (id: string, amt: string, owner: string) => - step(`mint ${amt} ${id} -> ${owner === cfg.trader ? "trader" : owner.slice(0, 8)}`, async () => { - const tx = await submit([cfg.admin, owner], `${RUN}-mint-${id}-${owner.slice(0, 6)}-${Date.now()}`, [{ - ExerciseCommand: { - templateId: tid("CantonDex.Registry.V2:Registry"), contractId: regCid, - choice: "Registry_Mint", - choiceArgument: { configCid: configCid[id], owner, amount: amt, issuerClaims: [] }, - }, - }]); - configCid[id] = creates(tx, "CantonDex.Registry.V2:InstrumentConfig")[0]!.contractId; - return creates(tx, "CantonDex.Registry.V2:Holding")[0]!.contractId; - }); - await mint(BASE, ADD_BASE, cfg.trader); - await mint(QUOTE, ADD_QUOTE, cfg.trader); - await mint(QUOTE, SWAP_IN, cfg.trader); // separate holding for the swap input - - // 2. Pool contracts (operator-authored), as the admin bootstrap does ---- - // Unique poolId per run so we never collide with other pools the - // operator observes (which would make a poolId-based lookup ambiguous). - const poolId = `${BASE}-${QUOTE}-${RUN}`; - const lpInstrumentId = { admin: lpRegistrar, id: LP }; - const poolCid = await step("create Pool", async () => { - const tx = await submit([cfg.operator], `${RUN}-pool`, [{ - CreateCommand: { - templateId: tid("CantonDex.Dex.Pool:Pool"), - createArguments: { - poolId, operator: cfg.operator, lpRegistrar, admin: cfg.admin, - baseInstrumentId: BASE, quoteInstrumentId: QUOTE, lpInstrumentId, - feeBps: "30", - }, - }, - }]); - return creates(tx, "CantonDex.Dex.Pool:Pool")[0]!.contractId; - }); - let stateCid = await step("create PoolState (Unfunded)", async () => { - const tx = await submit([cfg.operator], `${RUN}-state`, [{ - CreateCommand: { - templateId: tid("CantonDex.Dex.PoolState:PoolState"), - createArguments: { - poolId, operator: cfg.operator, lpRegistrar, status: "PS_Unfunded", - reserves: { baseAmount: "0.0", quoteAmount: "0.0" }, totalLpSupply: "0.0", publicReaders: [], - }, - }, - }]); - return creates(tx, "CantonDex.Dex.PoolState:PoolState")[0]!.contractId; - }); - await step("create PoolRules", async () => { - await submit([cfg.operator], `${RUN}-rules`, [{ - CreateCommand: { templateId: tid("CantonDex.Dex.PoolRules:PoolRules"), createArguments: { operator: cfg.operator } }, - }]); - }); - const dvpCid = await step("create PoolLiquidityRules", async () => { - const tx = await submit([cfg.operator, lpRegistrar], `${RUN}-dvp`, [{ - CreateCommand: { - templateId: tid("CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules"), - createArguments: { operator: cfg.operator, lpRegistrar }, - }, - }]); - return creates(tx, "CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules")[0]!.contractId; - }); - let policyCid = await step("create LPTokenPolicy", async () => { - const tx = await submit([lpRegistrar], `${RUN}-policy`, [{ - CreateCommand: { - templateId: tid("CantonDex.Lp.Policy:LPTokenPolicy"), - createArguments: { lpRegistrar, operator: cfg.operator, lpInstrumentId, totalSupply: "0.0", active: true }, - }, - }]); - return creates(tx, "CantonDex.Lp.Policy:LPTokenPolicy")[0]!.contractId; - }); - - const holdingsFor = async (id: string): Promise<{ cid: string; amount: string }[]> => { - const hs = await acs(cfg.trader, "CantonDex.Registry.V2:Holding"); - return hs - .map((c) => ({ cid: c.contractId, p: c.createArgument as { owner: string; instrumentId: string; amount: string; locked?: boolean } })) - .filter((x) => x.p.owner === cfg.trader && x.p.instrumentId === id && !x.p.locked) - .map((x) => ({ cid: x.cid, amount: x.p.amount })); - }; - - // 3. DvP ADD: request -> author 3 allocations -> settle ----------------- - console.log("\n== ADD LIQUIDITY =="); - const reqAdd = await step("PoolLiquidityRules_RequestAddLiquidity", async () => { - const lpAmount = Math.sqrt(parseFloat(ADD_BASE) * parseFloat(ADD_QUOTE)).toFixed(10); - const tx = await submit([cfg.operator], `${RUN}-add-req`, [{ - ExerciseCommand: { - templateId: tid("CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules"), contractId: dvpCid, - choice: "PoolLiquidityRules_RequestAddLiquidity", - choiceArgument: { - poolCid, recipient: cfg.trader, baseAmount: ADD_BASE, quoteAmount: ADD_QUOTE, - lpAmount, requestedAt: new Date().toISOString(), settleAt: null, - }, - }, - }]); - const r = creates(tx, "CantonDex.Dex.LiquidityAllocationRequest:LiquidityAllocationRequest")[0]!; - return { cid: r.contractId, arg: r.createArgument as { allocations: unknown[]; settlement: unknown } }; - }); - const addBaseH = (await holdingsFor(BASE)).find((h) => h.amount === `${ADD_BASE}000000000` || parseFloat(h.amount) === parseFloat(ADD_BASE))!; - const addQuoteH = (await holdingsFor(QUOTE)).find((h) => parseFloat(h.amount) === parseFloat(ADD_QUOTE))!; - const settlement = reqAdd.arg.settlement; - const [baseSpec, quoteSpec, receiptSpec] = reqAdd.arg.allocations; - const wrap = (a: unknown) => ({ __settlement: settlement, __alloc: a }); - const baseDep = await step("trader authors base deposit", () => authorAlloc(regCid, wrap(baseSpec), [addBaseH.cid], "add-base")); - const quoteDep = await step("trader authors quote deposit", () => authorAlloc(regCid, wrap(quoteSpec), [addQuoteH.cid], "add-quote")); - const receipt = await step("trader authors LP receipt", () => authorAlloc(regCid, wrap(receiptSpec), [], "add-receipt")); - const addRes = await step("PoolLiquidityRules_SettleAddLiquidity", async () => { - const tx = await submit([cfg.operator, lpRegistrar], `${RUN}-add-settle`, [{ - ExerciseCommand: { - templateId: tid("CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules"), contractId: dvpCid, - choice: "PoolLiquidityRules_SettleAddLiquidity", - choiceArgument: { - expectedPoolId: poolId, poolCid, poolStateCid: stateCid, lpPolicyCid: policyCid, - requestCid: reqAdd.cid, recipient: cfg.trader, - lpBaseDepositCid: baseDep, lpQuoteDepositCid: quoteDep, lpReceiptCid: receipt, - baseFactoryCid: regCid, quoteFactoryCid: regCid, lpFactoryCid: regCid, - baseQuoteSettleCid: regCid, lpSettleCid: regCid, - baseAmount: ADD_BASE, quoteAmount: ADD_QUOTE, minLpTokens: "0.0", knownTotalLpSupply: "0.0", - requestedAt: new Date().toISOString(), poolAdminExtraArgs: EXTRA, lpRegistrarExtraArgs: EXTRA, - }, - }, - }]); - // This settle tx creates exactly one PoolState (for THIS pool); match - // it by poolId to be unambiguous even if [0] ordering ever changes. - const ps = creates(tx, "CantonDex.Dex.PoolState:PoolState") - .find((c) => (c.createArgument as { poolId: string }).poolId === poolId)!; - stateCid = ps.contractId; - return ps.createArgument as { status: string; reserves: { baseAmount: string; quoteAmount: string }; totalLpSupply: string }; - }); - const expectLp = Math.sqrt(parseFloat(ADD_BASE) * parseFloat(ADD_QUOTE)).toFixed(10); - eq(addRes.status, "PS_Active", "pool active after add"); - eq(parseFloat(addRes.reserves.baseAmount), parseFloat(ADD_BASE), "base reserve"); - eq(parseFloat(addRes.reserves.quoteAmount), parseFloat(ADD_QUOTE), "quote reserve"); - eq(parseFloat(addRes.totalLpSupply).toFixed(6), parseFloat(expectLp).toFixed(6), "LP minted = sqrt(base*quote)"); - console.log(` reserves ${addRes.reserves.baseAmount}/${addRes.reserves.quoteAmount}, LP ${addRes.totalLpSupply} (= sqrt(${ADD_BASE}*${ADD_QUOTE}))`); - // Confirm the trader actually received the LP holding (DvP, not just supply bump). - const lpHeld = (await acs(cfg.trader, "CantonDex.Registry.V2:Holding")) - .map((c) => c.createArgument as { owner: string; instrumentId: string; amount: string; locked?: boolean }) - .filter((p) => p.owner === cfg.trader && p.instrumentId === LP && !p.locked); - eq(lpHeld.length >= 1, true, "trader holds an LP holding"); - eq(parseFloat(lpHeld.reduce((s, h) => s + parseFloat(h.amount), 0).toFixed(6)), parseFloat(expectLp).toFixed(6), "trader LP balance = minted"); - console.log(` trader LP holding: ${lpHeld.map((h) => h.amount).join("+")}`); - - console.log("\n== DvP add settled end-to-end via the wallet-authored allocation path =="); - console.log("PASS: add-liquidity DvP (trader authored all 3 allocations; operator+lpRegistrar settled)"); -} - -main().catch((e) => { console.error("FATAL", (e as Error).message); process.exit(1); }); diff --git a/scripts/run-dpm-sandbox-proof.sh b/scripts/run-dpm-sandbox-proof.sh new file mode 100755 index 00000000..228d5b19 --- /dev/null +++ b/scripts/run-dpm-sandbox-proof.sh @@ -0,0 +1,186 @@ +#!/usr/bin/env bash + +# Portable live-Canton proof using only the DPM SDK pinned by this repository. +# +# This script builds the DAR, starts a throwaway `dpm sandbox` on dynamic ports, +# creates three parties plus one unrestricted LOCAL sandbox user, uploads the +# package closure, runs the live DvP driver, and stops Canton. It does not +# require canton-devkit, Splice LocalNet, a browser, or a production JWT. + +set -euo pipefail + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +RUN_DIR="$(mktemp -d "${TMPDIR:-/tmp}/canton-dex-sandbox.XXXXXX")" +PORT_FILE="$RUN_DIR/ports.json" +LOG_FILE="$RUN_DIR/canton.log" +STDOUT_FILE="$RUN_DIR/canton.stdout.log" +SANDBOX_PID="" + +cleanup() { + local status=$? + trap - EXIT INT TERM + if [[ -n "$SANDBOX_PID" ]] && kill -0 "$SANDBOX_PID" 2>/dev/null; then + kill -INT "$SANDBOX_PID" 2>/dev/null || true + wait "$SANDBOX_PID" 2>/dev/null || true + fi + if [[ "$status" -eq 0 ]]; then + case "$RUN_DIR" in + "${TMPDIR:-/tmp}"/canton-dex-sandbox.*) rm -rf "$RUN_DIR" ;; + *) printf 'refusing to remove unexpected temp path: %s\n' "$RUN_DIR" >&2 ;; + esac + else + printf 'proof failed; Canton logs preserved at %s\n' "$RUN_DIR" >&2 + fi + exit "$status" +} +trap cleanup EXIT INT TERM + +for tool in dpm java node npm curl; do + if ! command -v "$tool" >/dev/null 2>&1; then + printf 'missing prerequisite: %s\n' "$tool" >&2 + exit 2 + fi +done + +printf '%s\n' '==> Installing the pinned SDK and building the DEX' +SDK_VERSION="$(node -e ' + const fs = require("node:fs"); + const yaml = fs.readFileSync(process.argv[1], "utf8"); + const version = yaml.match(/^sdk-version:\s*(.+)$/m)?.[1]?.trim(); + if (!version) process.exit(1); + process.stdout.write(version); +' "$ROOT_DIR/trading/daml.yaml")" +dpm install "$SDK_VERSION" +bash "$ROOT_DIR/scripts/build-trading-surface.sh" +if [[ ! -x "$ROOT_DIR/services/operator-backend/node_modules/.bin/tsx" ]]; then + (cd "$ROOT_DIR/services/operator-backend" && npm ci) +fi + +# Canton 3.5 cannot internally reconnect when its own ports are configured as +# zero. Reserve all six sandbox ports, release them together, and pass +# the concrete values immediately. The tiny release/start race is detected by +# the readiness check and produces preserved logs rather than a false PASS. +read -r LEDGER_PORT ADMIN_PORT JSON_PORT SEQUENCER_PORT SEQUENCER_ADMIN_PORT MEDIATOR_ADMIN_PORT < <(node -e ' + const net = require("node:net"); + const servers = []; + const open = () => new Promise((resolve, reject) => { + const s = net.createServer(); + servers.push(s); + s.once("error", reject); + s.listen(0, "127.0.0.1", () => resolve(s.address().port)); + }); + Promise.all([open(), open(), open(), open(), open(), open()]).then((ports) => { + for (const s of servers) s.close(); + process.stdout.write(`${ports.join(" ")}\n`); + }).catch((e) => { console.error(e.message); process.exit(1); }); +') + +printf '%s\n' '==> Starting throwaway Canton sandbox on reserved loopback ports' +dpm sandbox \ + --ledger-api-port "$LEDGER_PORT" \ + --admin-api-port "$ADMIN_PORT" \ + --json-api-port "$JSON_PORT" \ + --sequencer-public-port "$SEQUENCER_PORT" \ + --sequencer-admin-port "$SEQUENCER_ADMIN_PORT" \ + --mediator-admin-port "$MEDIATOR_ADMIN_PORT" \ + --canton-port-file "$PORT_FILE" \ + --log-file-name "$LOG_FILE" \ + --log-file-appender flat \ + >"$STDOUT_FILE" 2>&1 & +SANDBOX_PID=$! + +JSON_PORT="" +for _ in $(seq 1 120); do + if ! kill -0 "$SANDBOX_PID" 2>/dev/null; then + printf '%s\n' 'Canton exited before becoming ready' >&2 + tail -n 80 "$STDOUT_FILE" >&2 || true + exit 1 + fi + if [[ -s "$PORT_FILE" ]]; then + JSON_PORT="$(node -e ' + const fs = require("node:fs"); + const body = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); + if (body.sandbox?.jsonApi) process.stdout.write(String(body.sandbox.jsonApi)); + ' "$PORT_FILE")" + if [[ -n "$JSON_PORT" ]] && \ + curl -fsS "http://127.0.0.1:${JSON_PORT}/v2/state/ledger-end" >/dev/null 2>&1; then + break + fi + fi + sleep 1 +done +if [[ -z "$JSON_PORT" ]] || \ + ! curl -fsS "http://127.0.0.1:${JSON_PORT}/v2/state/ledger-end" >/dev/null 2>&1; then + printf '%s\n' 'Canton did not become ready within 120 seconds' >&2 + tail -n 80 "$STDOUT_FILE" >&2 || true + exit 1 +fi + +export CANTON_LEDGER_URL="http://127.0.0.1:${JSON_PORT}" +export CANTON_LEDGER_TOKEN="sandbox-auth-disabled" +export CANTON_USER_ID="ledger-api-user" + +parties_json="$(curl -fsS "${CANTON_LEDGER_URL}/v2/parties")" +primary_party="$(DEX_PARTIES_JSON="$parties_json" node -e ' + const body = JSON.parse(process.env.DEX_PARTIES_JSON || "{}"); + const party = body.partyDetails?.find((p) => p.isLocal)?.party; + if (!party) process.exit(1); + process.stdout.write(party); +')" + +# The sandbox has authentication disabled, but command submission still names a +# ledger user. Give this throwaway user unrestricted rights inside this process +# only. Never copy this user policy to a shared or production participant. +curl -fsS -X POST \ + -H "Content-Type: application/json" \ + -d "{\"user\":{\"id\":\"${CANTON_USER_ID}\",\"primaryParty\":\"${primary_party}\",\"isDeactivated\":false,\"identityProviderId\":\"\",\"metadata\":{\"resourceVersion\":\"\",\"annotations\":{}}},\"rights\":[{\"kind\":{\"CanExecuteAsAnyParty\":{\"value\":{}}}},{\"kind\":{\"CanReadAsAnyParty\":{\"value\":{}}}},{\"kind\":{\"ParticipantAdmin\":{\"value\":{}}}}]}" \ + "${CANTON_LEDGER_URL}/v2/users" >/dev/null + +# A liquidity deposit and a swap both move value between the operator and a +# counterparty, so neither counterparty can be the operator itself. Allocate a +# distinct LP/trader and swapper. The sandbox user's CanExecuteAsAnyParty right +# is deliberately scoped to this throwaway process, so no production-style +# permission is implied here. +allocate_party() { + local hint="$1" + local response + response="$(curl -fsS -X POST \ + -H "Content-Type: application/json" \ + -d "{\"partyIdHint\":\"${hint}\",\"userId\":\"${CANTON_USER_ID}\"}" \ + "${CANTON_LEDGER_URL}/v2/parties")" + DEX_ALLOCATED_PARTY_JSON="$response" node -e ' + const body = JSON.parse(process.env.DEX_ALLOCATED_PARTY_JSON || "{}"); + const party = body.partyDetails?.party; + if (!party) process.exit(1); + process.stdout.write(party); +' +} +trader_party="$(allocate_party "dex-lp-${RANDOM}")" +swapper_party="$(allocate_party "dex-swapper-${RANDOM}")" + +export CANTON_OPERATOR="$primary_party" +export CANTON_ADMIN="$primary_party" +export CANTON_LP_REGISTRAR="$primary_party" +export CANTON_TRADER="$trader_party" +export CANTON_SWAPPER="$swapper_party" +export CANTON_DEX_PACKAGE_ID="#canton-dex-trading" +export CANTON_ALLOC_INSTR_PACKAGE_ID="#splice-api-token-allocation-instruction-v2" +unset CANTON_SYNCHRONIZER + +printf ' JSON Ledger API: %s\n' "$CANTON_LEDGER_URL" +printf '%s\n' \ + ' Auth model: local sandbox only; unrestricted throwaway ledger user' \ + ' Roles: operator/admin share the bootstrap party; LP/trader and swapper are distinct' + +printf '%s\n' '==> Uploading the package closure' +DEPLOY_SKIP_BUILD=1 \ +DEPLOY_SKIP_BOOTSTRAP=1 \ +DEPLOY_SEED_MARKETS=0 \ + bash "$ROOT_DIR/scripts/deploy-testnet.sh" + +printf '%s\n' '==> Running the live-Canton DvP proof' +(cd "$ROOT_DIR/services/operator-backend" && npm run live:roundtrip) + +printf '%s\n' \ + '==> PASS: portable live-Canton proof completed' \ + ' The throwaway sandbox is now stopping; no persistent ledger state remains.' diff --git a/scripts/run-localnet-roundtrip.sh b/scripts/run-localnet-roundtrip.sh new file mode 100755 index 00000000..867f9428 --- /dev/null +++ b/scripts/run-localnet-roundtrip.sh @@ -0,0 +1,142 @@ +#!/usr/bin/env bash + +# Clean-clone Canton LocalNet proof for this reference implementation. +# +# Starts (or reuses) a named canton-devkit LocalNet, resolves its JSON Ledger +# API and dev credential without printing the JWT, builds/uploads the package +# closure, and runs the repository's live DvP driver. The instance is left +# running for inspection; the final output prints the exact non-destructive +# `down` command. + +set -euo pipefail +set +x # never shell-trace the LocalNet JWT + +ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +INSTANCE="${1:-canton-dex}" +VERSION="${CANTON_LOCALNET_VERSION:-0.6.12}" + +if [[ "$INSTANCE" == "--help" || "$INSTANCE" == "-h" ]]; then + printf '%s\n' \ + 'Usage: bash scripts/run-localnet-roundtrip.sh [instance-name]' \ + '' \ + 'Optional environment:' \ + ' CANTON_LOCALNET_VERSION=0.6.12 pinned Splice LocalNet version' \ + ' LOCALNET_SKIP_DEPLOY=1 reuse already-uploaded DARs' \ + ' DEX_LOCALNET_OPERATOR= use separate pre-authorized roles' \ + ' DEX_LOCALNET_ADMIN=' \ + ' DEX_LOCALNET_TRADER=' \ + ' DEX_LOCALNET_SWAPPER=' + exit 0 +fi +if [[ ! "$INSTANCE" =~ ^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$ ]]; then + printf 'invalid LocalNet instance name: %s\n' "$INSTANCE" >&2 + exit 2 +fi + +for tool in canton-devkit dpm curl node npm; do + if ! command -v "$tool" >/dev/null 2>&1; then + printf 'missing prerequisite: %s\n' "$tool" >&2 + exit 2 + fi +done + +printf '%s\n' '==> Checking LocalNet host prerequisites' +canton-devkit localnet doctor + +printf '==> Starting/reusing LocalNet %s (Splice %s)\n' "$INSTANCE" "$VERSION" +canton-devkit localnet up --name "$INSTANCE" --version "$VERSION" + +# DevKit shell output is eval-safe. --include-jwt is intentionally scoped to +# this process; the token is never printed by this script. +eval "$(canton-devkit localnet env "$INSTANCE" --format shell --include-jwt)" + +if [[ -z "${CANTON_PARTICIPANT_JSON_APP_PROVIDER_PORT:-}" || \ + -z "${CANTON_APP_PROVIDER_JWT:-}" || \ + -z "${CANTON_APP_PROVIDER_USER:-}" ]]; then + printf '%s\n' 'LocalNet did not expose the app-provider JSON API credential' >&2 + exit 1 +fi + +export CANTON_LEDGER_URL="http://127.0.0.1:${CANTON_PARTICIPANT_JSON_APP_PROVIDER_PORT}" +export CANTON_LEDGER_TOKEN="$CANTON_APP_PROVIDER_JWT" +export CANTON_USER_ID="$CANTON_APP_PROVIDER_USER" + +user_json="$(curl -fsS \ + -H "Authorization: Bearer ${CANTON_LEDGER_TOKEN}" \ + "${CANTON_LEDGER_URL}/v2/users/${CANTON_USER_ID}")" +primary_party="$(DEX_USER_JSON="$user_json" node -e ' + const body = JSON.parse(process.env.DEX_USER_JSON || "{}"); + const party = body.user?.primaryParty; + if (!party) process.exit(1); + process.stdout.write(party); +')" + +# DevKit is only the network lifecycle/credential adapter here; it is not a DEX +# runtime dependency. Allocate missing counterparty roles through the standard +# JSON Ledger API and grant them to the already-authenticated app-provider user. +# Explicit overrides let an integrator exercise pre-provisioned parties instead. +allocate_party() { + local hint="$1" + local response + response="$(curl -fsS -X POST \ + -H "Authorization: Bearer ${CANTON_LEDGER_TOKEN}" \ + -H "Content-Type: application/json" \ + -d "{\"partyIdHint\":\"${hint}\",\"userId\":\"${CANTON_USER_ID}\"}" \ + "${CANTON_LEDGER_URL}/v2/parties")" + DEX_ALLOCATED_PARTY_JSON="$response" node -e ' + const body = JSON.parse(process.env.DEX_ALLOCATED_PARTY_JSON || "{}"); + const party = body.partyDetails?.party; + if (!party) process.exit(1); + process.stdout.write(party); +' +} +export CANTON_OPERATOR="${DEX_LOCALNET_OPERATOR:-$primary_party}" +export CANTON_ADMIN="${DEX_LOCALNET_ADMIN:-$primary_party}" +export CANTON_LP_REGISTRAR="$CANTON_ADMIN" +export CANTON_TRADER="${DEX_LOCALNET_TRADER:-$(allocate_party "dex-lp-${RANDOM}")}" +export CANTON_SWAPPER="${DEX_LOCALNET_SWAPPER:-$(allocate_party "dex-swapper-${RANDOM}")}" +export CANTON_DEX_PACKAGE_ID="${CANTON_DEX_PACKAGE_ID:-#canton-dex-trading}" +export CANTON_ALLOC_INSTR_PACKAGE_ID="${CANTON_ALLOC_INSTR_PACKAGE_ID:-#splice-api-token-allocation-instruction-v2}" + +printf ' JSON Ledger API: %s\n' "$CANTON_LEDGER_URL" +printf ' Ledger user: %s\n' "$CANTON_USER_ID" +printf '%s\n' \ + ' Proof roles: operator/admin use app-provider primary party;' \ + ' LP/trader and swapper are separately allocated unless overridden' + +printf '%s\n' '==> Installing pinned Daml SDK and Node runner dependencies' +SDK_VERSION="$(node -e ' + const fs = require("node:fs"); + const yaml = fs.readFileSync(process.argv[1], "utf8"); + const version = yaml.match(/^sdk-version:\s*(.+)$/m)?.[1]?.trim(); + if (!version) process.exit(1); + process.stdout.write(version); +' "$ROOT_DIR/trading/daml.yaml")" +dpm install "$SDK_VERSION" +if [[ ! -x "$ROOT_DIR/services/operator-backend/node_modules/.bin/tsx" ]]; then + (cd "$ROOT_DIR/services/operator-backend" && npm ci) +fi + +if [[ "${LOCALNET_SKIP_DEPLOY:-0}" != "1" ]]; then + printf '%s\n' '==> Building and uploading the package closure' + DEPLOY_SKIP_BOOTSTRAP=1 \ + DEPLOY_SEED_MARKETS=0 \ + bash "$ROOT_DIR/scripts/deploy-testnet.sh" +else + printf '%s\n' '==> Package deployment skipped (LOCALNET_SKIP_DEPLOY=1)' +fi + +printf '%s\n' '==> Running the live-ledger DvP proof' +(cd "$ROOT_DIR/services/operator-backend" && npm run live:roundtrip) + +printf '%s\n' \ + '' \ + 'LocalNet remains running so you can inspect the created contracts:' \ + " canton-devkit localnet status --name $INSTANCE" \ + " canton-devkit localnet contracts --help" \ + '' \ + 'Stop containers while preserving ledger volumes:' \ + " canton-devkit localnet down --name $INSTANCE" \ + '' \ + 'Destructive cleanup (removes this instance and its ledger state):' \ + " canton-devkit localnet remove --name $INSTANCE" diff --git a/scripts/seed-testnet-pool.ts b/scripts/seed-testnet-pool.ts index 95d0e326..13d37aac 100644 --- a/scripts/seed-testnet-pool.ts +++ b/scripts/seed-testnet-pool.ts @@ -13,7 +13,7 @@ // Registry_Mint on the existing registry // 2. add -- the wallet-authored DvP add (request -> the LP authors its // three allocations -> settle), the flow proven headlessly in -// scripts/localnet-dvp-e2e.ts +// scripts/live-amm-roundtrip.ts // 3. swap -- PoolRules_RequestSwap -> the swapper authors its input // allocation -> PoolRules_Swap, then asserts against the // ledger that the reserves moved by exactly the @@ -24,6 +24,11 @@ // // Every assertion is fatal: a failure exits non-zero. // +// STATE WARNING: mint/add/swap transactions permanently change the selected +// pool and participant holdings, and an interrupted run can leave partial +// state. Use a dedicated test pool, retain the printed run id, and do not point +// this script at production liquidity. +// // Env (ledger + parties): // CANTON_LEDGER_URL, CANTON_LEDGER_TOKEN, CANTON_SYNCHRONIZER, // CANTON_DEX_PACKAGE_ID (e.g. #canton-dex-trading), @@ -126,7 +131,18 @@ interface InstrumentConfigArg { admin: string; instrumentId: string } interface RulesArg { operator: string } interface LiquidityRulesArg { operator: string; lpRegistrar: string } interface RequestArg { allocations: unknown[]; settlement: unknown } -interface SwapRequestResult { settlement: unknown; allocationSpec: unknown } +interface SwapQuoteBinding { + expectedPoolId: string; + poolStateCid: string; + inputSliceCid: string; + outputSliceCids: string[]; + minOutputAmount: string; +} +interface SwapRequestResult { + settlement: unknown; + allocationSpec: unknown; + quoteBinding: SwapQuoteBinding | null; +} const argOf = (c: Created): T => c.createArgument as unknown as T; @@ -242,9 +258,14 @@ async function retrying(what: string, fn: (attempt: number) => Promise, at // The Daml floors the result to 10dp afterwards, which is a no-op on a value // that already carries 10 decimals. function constantProductOut(reserveIn: bigint, reserveOut: bigint, feeBps: number, inputAmount: bigint): bigint { - const feeMultiplier = dec.div(dec.parseDecimal(String(10000 - feeBps)), dec.parseDecimal("10000")); - const amountInAfterFee = dec.mul(inputAmount, feeMultiplier); - return dec.div(dec.mul(amountInAfterFee, reserveOut), reserveIn + amountInAfterFee); + const amountInAfterFee = dec.divFloor( + dec.mulFloor(inputAmount, dec.parseDecimal(String(10000 - feeBps))), + dec.parseDecimal("10000"), + ); + return dec.divFloor( + dec.mulFloor(amountInAfterFee, reserveOut), + reserveIn + amountInAfterFee, + ); } // The LP entitlement PoolLiquidityRules_SettleAddLiquidity bounds the receipt @@ -305,8 +326,8 @@ async function authorAlloc( return only(creates(tx, "CantonDex.Registry.V2:Allocation"), `${label} allocation`).contractId; } -// The Registry is `signatory admin, observer users`, so a party outside `users` -// -- every faucet-created tester -- cannot see the factory it must exercise. +// The Registry is `signatory admin, observer users`, so a separately allocated +// test party outside `users` cannot see the factory it must exercise. // Explicit contract disclosure is the mechanism for exactly this: fetch the // contract's createdEventBlob as someone who CAN see it (the admin) and attach // it to the submitter's command. This is what a registry's off-ledger API @@ -553,6 +574,13 @@ async function main() { if (covered < expectedOut) { throw new Error(`${outputId} slices cover ${dec.formatDecimal(covered)}, need ${dec.formatDecimal(expectedOut)}`); } + const quoteBinding: SwapQuoteBinding = { + expectedPoolId: pool.poolId, + poolStateCid: addState.cid, + inputSliceCid: headInput.contractId, + outputSliceCids, + minOutputAmount: dec.formatDecimal(expectedOut), + }; const inBefore = await balance(swapper, pool.admin, inputId); const outBefore = await balance(swapper, pool.admin, outputId); @@ -565,30 +593,26 @@ async function main() { choiceArgument: { poolCid: ctx.poolC.contractId, swapper, inputInstrumentId: inputId, inputAmount: cfg.swapIn, + quoteBinding, }, }, }]); const result = exercisedResult(tx, "PoolRules_RequestSwap") ?? (await treeExercisedResult(tx.transaction.updateId, cfg.operator, "PoolRules_RequestSwap")); - if (result) return result as SwapRequestResult; - // The participant served neither the choice result nor a tree, so rebuild - // what the choice returns (PoolModel.poolSettlement + - // Utils.mkPrefundedAllocationSpecification). Nothing is taken on trust: - // the registry re-checks the funding at allocate and PoolRules_Swap - // re-checks every leg at settle, so a wrong spec aborts the swap. - console.log(" .. choice result unavailable, rebuilding the allocation spec locally"); - return { - settlement: { executors: [cfg.operator], id: "DexPool", cid: ctx.poolC.contractId, meta: { values: {} } }, - allocationSpec: { - admin: pool.admin, authorizer: acct(swapper), transferLegSides: [], - settlementDeadline: null, nextIterationFunding: { [inputId]: cfg.swapIn }, - committed: false, meta: { values: {} }, - }, - } satisfies SwapRequestResult; + if (!result) { + throw new Error( + "participant did not expose the PoolRules_RequestSwap result in the transaction or transaction tree", + ); + } + const request = result as SwapRequestResult; + if (!request.quoteBinding) throw new Error("PoolRules_RequestSwap returned no quote binding"); + eq(request.quoteBinding.poolStateCid, quoteBinding.poolStateCid, "swap request state binding"); + eq(request.quoteBinding.minOutputAmount, quoteBinding.minOutputAmount, "swap request minimum binding"); + return request; }); - // The swapper is an arbitrary party (a faucet tester in the real flow), so it - // is not an observer of the asset registry. Disclose the registry to it. + // The swapper is an arbitrary, separately allocated party, so it is not an + // observer of the asset registry. Disclose the registry to it. const registryDisclosure = await step("disclose registry to the swapper", () => discloseRegistry(pool.admin, ctx.registryCid)); @@ -612,6 +636,7 @@ async function main() { swapperAllocationCid: swapAlloc, inputSliceCid: headInput.contractId, outputSliceCids, factoryCid: ctx.registryCid, extraArgs: EXTRA, + quoteBinding, }, }, }], [swapper]); @@ -661,6 +686,7 @@ async function main() { console.log(`swapper balances: ${inputId} ${dec.formatDecimal(inBefore)} -> ${dec.formatDecimal(inAfter)}, ${outputId} ${dec.formatDecimal(outBefore)} -> ${dec.formatDecimal(outAfter)}`); console.log(`LP supply ${swapState.arg.totalLpSupply}`); console.log("PASS: existing pool seeded via the wallet-authored DvP add, and a swap settled and asserted against it"); + console.log(`state changed on participant: run=${RUN}, pool=${pool.poolId}`); } /** Unlocked balance of one instrument, as issued by `admin`. */ diff --git a/scripts/testnet-v2registry-trade.ts b/scripts/testnet-v2registry-trade.ts index c05d1245..bd5f30f7 100644 --- a/scripts/testnet-v2registry-trade.ts +++ b/scripts/testnet-v2registry-trade.ts @@ -2,6 +2,12 @@ // V2 Registry as AllocationFactory + SettlementFactory + TransferFactory. // Registers an instrument, mints to alice, posts a MatchedTrade, runs // the V2 allocation accept on both sides, settles via SettleBatch. +// +// STATE WARNING: every run creates a registry, instrument, holdings, and trade +// contracts. Use dedicated parties or a throwaway participant and retain the +// printed run id for cleanup/audit. + +export {}; function required(name: string): string { const v = process.env[name]; @@ -123,7 +129,7 @@ async function queryHoldings(party: string, instrumentId: string) { async function main() { console.log(`run id: ${RUN_ID}`); - console.log(`registry package: canton-dex-trading v0.0.3 (${cfg.pkgDex.slice(0, 12)}…)`); + console.log(`registry package: ${cfg.pkgDex.slice(0, 12)}…`); console.log(`venue: ${cfg.venue}`); console.log(`admin: ${cfg.admin} (instrument issuer)`); console.log(`alice: ${cfg.alice} (sender)`); diff --git a/services/operator-backend/.env.example b/services/operator-backend/.env.example index 73236ff1..8157947f 100644 --- a/services/operator-backend/.env.example +++ b/services/operator-backend/.env.example @@ -31,31 +31,70 @@ CANTON_NETWORK=canton:devnet # Synchronizer id, e.g. global-domain::1220... CANTON_SYNCHRONIZER= -# Daml package hash or prefix for template ids. +# Daml package hash or package-name prefix for template ids (required). CANTON_DEX_PACKAGE_ID= # --- Factory Contract IDs --- -# AllocationFactory contract id (from registry bootstrap). +# Asset-admin AllocationFactory contract id from registry bootstrap (required +# in full mode). CANTON_ALLOC_FACTORY_CID= -# SettlementFactory contract id (from registry bootstrap). +# Asset-admin SettlementFactory contract id from registry bootstrap (required +# in full mode). CANTON_SETTLE_FACTORY_CID= +# When CANTON_LP_REGISTRAR differs from CANTON_ADMIN, set these to the LP +# registrar's Registry.V2 cid (the same cid implements both interfaces). +# They are required in full mode only for distinct registrars. +CANTON_LP_ALLOC_FACTORY_CID= +CANTON_LP_SETTLE_FACTORY_CID= + # --- Server --- # HTTP server port (default: 8080). PORT=8080 +# Bind address (default: 127.0.0.1 for direct runs). Containers override this +# to 0.0.0.0 so nginx/the published port can reach the process. +HOST=127.0.0.1 + # SQLite database path for the indexer (default: ./data/operator.db). DB_PATH=./data/operator.db # Indexer polling interval in milliseconds (default: 5000). INDEXER_INTERVAL_MS=5000 -# Admin auth token for /v1/admin/* routes. If unset, admin routes are unprotected. +# Full-mode testnet startup requires both write tokens below. Generate separate, +# high-entropy values. The server never sends them to the browser automatically. +# The Admin screen can hold short-lived copies in per-tab sessionStorage for a +# validator/operator run; public deployments should use an authenticated BFF. + +# Admin auth token for /v1/admin/* writes. Unset fails closed. OPERATOR_ADMIN_TOKEN= -# CORS allowed origins (comma-separated). If unset, allows all origins. +# Operator auth token for every other state-changing HTTP route. Unset fails +# closed. The testnet server also refuses to start in full mode without it. +DEX_OPERATOR_API_TOKEN= + +# Set to 1 only for an intentionally read-only testnet server. In this mode the +# server starts without the two tokens above and all state-changing routes +# return 401. Read-only computation such as POST /v1/swaps/quote still works. +DEX_READ_ONLY=0 + +# Optional per-caller binding. When set, party-scoped reads and trader-subject +# writes require an X-Caller-Token HS256 JWT with sub= and an exp claim. +# Set an audience as an additional replay boundary when your issuer provides one. +DEX_CALLER_JWT_SECRET= +DEX_CALLER_JWT_AUDIENCE= + +# Trusted hosted-RFQ authority relay. Disabled by default. Enabling it requires +# DEX_CALLER_JWT_SECRET and a participant JWT with actAs rights for each hosted +# trader. Prefer wallet-authored RFQ commands for self-custody. +DEX_HOSTED_RFQ_RELAY=0 + +# CORS allowed origins (comma-separated). If unset, browsers receive no +# Access-Control-Allow-Origin header (default-deny). Same-origin nginx traffic +# does not need CORS; include Vite/preview origins for direct local browser use. # Include the preview port you use for local UI testing. ALLOWED_ORIGINS=http://localhost:5173,http://localhost:4173,http://127.0.0.1:18081 diff --git a/services/operator-backend/package-lock.json b/services/operator-backend/package-lock.json index 2258d1d9..03254599 100644 --- a/services/operator-backend/package-lock.json +++ b/services/operator-backend/package-lock.json @@ -32,9 +32,9 @@ "link": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", - "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", "cpu": [ "ppc64" ], @@ -49,9 +49,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", - "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", "cpu": [ "arm" ], @@ -66,9 +66,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", - "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", "cpu": [ "arm64" ], @@ -83,9 +83,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", - "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", "cpu": [ "x64" ], @@ -100,9 +100,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", - "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", "cpu": [ "arm64" ], @@ -117,9 +117,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", - "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", "cpu": [ "x64" ], @@ -134,9 +134,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", - "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", "cpu": [ "arm64" ], @@ -151,9 +151,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", - "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", "cpu": [ "x64" ], @@ -168,9 +168,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", - "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", "cpu": [ "arm" ], @@ -185,9 +185,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", - "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", "cpu": [ "arm64" ], @@ -202,9 +202,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", - "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", "cpu": [ "ia32" ], @@ -219,9 +219,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", - "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", "cpu": [ "loong64" ], @@ -236,9 +236,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", - "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", "cpu": [ "mips64el" ], @@ -253,9 +253,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", - "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", "cpu": [ "ppc64" ], @@ -270,9 +270,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", - "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", "cpu": [ "riscv64" ], @@ -287,9 +287,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", - "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", "cpu": [ "s390x" ], @@ -304,9 +304,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", - "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", "cpu": [ "x64" ], @@ -321,9 +321,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", - "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", "cpu": [ "arm64" ], @@ -338,9 +338,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", - "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", "cpu": [ "x64" ], @@ -355,9 +355,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", - "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", "cpu": [ "arm64" ], @@ -372,9 +372,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", - "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", "cpu": [ "x64" ], @@ -389,9 +389,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", - "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", "cpu": [ "arm64" ], @@ -406,9 +406,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", - "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", "cpu": [ "x64" ], @@ -423,9 +423,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", - "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", "cpu": [ "arm64" ], @@ -440,9 +440,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", - "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", "cpu": [ "ia32" ], @@ -457,9 +457,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", - "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", "cpu": [ "x64" ], @@ -620,9 +620,9 @@ } }, "node_modules/esbuild": { - "version": "0.27.7", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", - "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -633,32 +633,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.7", - "@esbuild/android-arm": "0.27.7", - "@esbuild/android-arm64": "0.27.7", - "@esbuild/android-x64": "0.27.7", - "@esbuild/darwin-arm64": "0.27.7", - "@esbuild/darwin-x64": "0.27.7", - "@esbuild/freebsd-arm64": "0.27.7", - "@esbuild/freebsd-x64": "0.27.7", - "@esbuild/linux-arm": "0.27.7", - "@esbuild/linux-arm64": "0.27.7", - "@esbuild/linux-ia32": "0.27.7", - "@esbuild/linux-loong64": "0.27.7", - "@esbuild/linux-mips64el": "0.27.7", - "@esbuild/linux-ppc64": "0.27.7", - "@esbuild/linux-riscv64": "0.27.7", - "@esbuild/linux-s390x": "0.27.7", - "@esbuild/linux-x64": "0.27.7", - "@esbuild/netbsd-arm64": "0.27.7", - "@esbuild/netbsd-x64": "0.27.7", - "@esbuild/openbsd-arm64": "0.27.7", - "@esbuild/openbsd-x64": "0.27.7", - "@esbuild/openharmony-arm64": "0.27.7", - "@esbuild/sunos-x64": "0.27.7", - "@esbuild/win32-arm64": "0.27.7", - "@esbuild/win32-ia32": "0.27.7", - "@esbuild/win32-x64": "0.27.7" + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" } }, "node_modules/expand-template": { diff --git a/services/operator-backend/package.json b/services/operator-backend/package.json index 4a907297..75c05e15 100644 --- a/services/operator-backend/package.json +++ b/services/operator-backend/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "https://github.com/canton-foundation/canton-dex", + "url": "https://github.com/srikanth-bitdynamics/Canton-Dex-Reference-Implementation.git", "directory": "services/operator-backend" }, "private": true, @@ -19,17 +19,25 @@ }, "scripts": { "typecheck": "tsc --noEmit", + "typecheck:live-scripts": "tsc -p tsconfig.live-scripts.json", "test": "node --import tsx --test test/*.test.ts", + "test:live:rfq": "node --import tsx --test test/live/canton-live-rfq.test.ts", "dev": "node --import tsx src/dev-server.ts", "testnet": "node --import tsx src/testnet-server.ts", "start": "node --import tsx src/testnet-server.ts", - "localnet:dvp-e2e": "node --import tsx ../../scripts/localnet-dvp-e2e.ts", - "testnet:seed-pool": "node --import tsx ../../scripts/seed-testnet-pool.ts" + "live:roundtrip": "node --import tsx ../../scripts/live-amm-roundtrip.ts", + "live:add-liquidity": "node --import tsx ../../scripts/live-amm-roundtrip.ts --add-only", + "localnet:amm-roundtrip": "node --import tsx ../../scripts/live-amm-roundtrip.ts", + "testnet:seed-pool": "node --import tsx ../../scripts/seed-testnet-pool.ts", + "live:matched-trade": "node --import tsx ../../scripts/testnet-v2registry-trade.ts" }, "dependencies": { "@canton-dex/registry-client": "file:../registry-client", "better-sqlite3": "^12.10.0" }, + "overrides": { + "esbuild": "^0.28.2" + }, "devDependencies": { "@types/better-sqlite3": "^7.6.13", "@types/node": "^20", diff --git a/services/operator-backend/src/dealers/index.ts b/services/operator-backend/src/dealers/index.ts index 94ce6337..05245991 100644 --- a/services/operator-backend/src/dealers/index.ts +++ b/services/operator-backend/src/dealers/index.ts @@ -1,7 +1,7 @@ -// Dealer registry. The operator owns the list of RFQ counterparties: -// who can quote, whose quotes get the "trusted" tier bump in the -// matching policy, and per-dealer telemetry (latency, fill rate) that -// the UI surfaces during compose. +// Dealer directory. The operator curates who the hosted UI offers as an RFQ +// counterparty and the telemetry shown during compose. On-ledger RfqQuote.tier +// is dealer-declared; the operator endorses the considered quotes only when it +// co-authorizes Rfq_Accept. // // Backed by the SQLite indexer DB. Read-only consumers query // `list()`; admin endpoints call `upsert()` / `remove()` behind the diff --git a/services/operator-backend/src/dev-server.ts b/services/operator-backend/src/dev-server.ts index 63619447..923766a2 100644 --- a/services/operator-backend/src/dev-server.ts +++ b/services/operator-backend/src/dev-server.ts @@ -11,14 +11,15 @@ // Order_Fund/Cancel, OrderFundingRequest_Bind, Rfq_Accept, // MatchedTrade_* are stubbed minimally; admin/* re-use built-in create). // -// This is NOT a production server. It is the smallest amount of -// scaffolding that lets the UI demo end-to-end without a Canton -// participant. Production swaps in JsonApiLedger + a real registry. +// This is NOT a production server. It is the smallest amount of scaffolding +// needed to exercise the UI-to-HTTP loop without a Canton participant. It does +// not prove Daml execution or value movement. Live mode swaps in JsonApiLedger +// and a real registry. import { InMemoryLedger } from "./ledger/in-memory.js"; import { OperatorBackend } from "./index.js"; import { startHttpServer } from "./http/index.js"; -import { RegistryClient } from "@canton-dex/registry-client"; +import { FixedRegistryClient } from "@canton-dex/registry-client"; import type { ContractId, Decimal, @@ -26,26 +27,15 @@ import type { Pool, PoolSlice, } from "./types.js"; -import type { ChoiceContextRef } from "@canton-dex/registry-client"; // Stub registry that returns canned factory CIDs for any admin party. -class StubRegistry extends RegistryClient { +class StubRegistry extends FixedRegistryClient { constructor() { - super({ baseUrl: "http://stub-registry" }); - } - override async getFactories(): Promise<{ - allocationFactoryCid: ContractId<"AllocationFactory">; - settlementFactoryCid: ContractId<"SettlementFactory">; - disclosure: never[]; - }> { - return { + super(() => ({ allocationFactoryCid: "#alloc-fac:0" as ContractId<"AllocationFactory">, settlementFactoryCid: "#settle-fac:0" as ContractId<"SettlementFactory">, disclosure: [], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; + })); } } @@ -396,11 +386,9 @@ async function main(): Promise { operator, lpRegistrar, admin, - allocationFactoryCid: "#alloc-fac:0", - settlementFactoryCid: "#settle-fac:0", - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], - network: process.env.CANTON_NETWORK ?? "canton:devnet", + // A sentinel consumed by the dApp shell so the seeded preview can never + // look like a synchronized Canton environment. + network: "preview:in-memory", }, // Operator-write auth: the dev server has no token, so default to the // explicit dev-open bypass unless an operator token is supplied. @@ -412,6 +400,9 @@ async function main(): Promise { .split(",") .map((s) => s.trim()) .filter(Boolean), + // The in-memory demo owns its seeded trader authority. Real-Canton + // testnet-server keeps this trusted relay disabled by default. + hostedRfqEnabled: true, }); // eslint-disable-next-line no-console console.log(`[operator-backend] dev server listening at ${url}`); diff --git a/services/operator-backend/src/http/auth.ts b/services/operator-backend/src/http/auth.ts index b64a8b2e..70e5e85f 100644 --- a/services/operator-backend/src/http/auth.ts +++ b/services/operator-backend/src/http/auth.ts @@ -77,6 +77,7 @@ const OPERATOR_WRITE_EXACT = new Set([ // (It is additionally gated by walletRelayEnabled + a party allowlist in // the handler, but the auth gate is the first line of defence.) "/v1/wallet/submit", + "/v1/registry/allocation-factory", "/v1/pools/swap", "/v1/pools/swap/request", "/v1/pools/add-liquidity/request", diff --git a/services/operator-backend/src/http/caller-auth.ts b/services/operator-backend/src/http/caller-auth.ts index 9029d2d6..b0772f66 100644 --- a/services/operator-backend/src/http/caller-auth.ts +++ b/services/operator-backend/src/http/caller-auth.ts @@ -1,4 +1,4 @@ -// Per-caller party binding for operator-authority write routes. +// Per-caller party binding for private reads and operator-authority writes. // // The operator bearer token (checkOperatorAuth) authenticates the *backend // client* — but on its own it lets any holder name an arbitrary party as the @@ -207,3 +207,36 @@ export function checkCallerBinding( } return { ok: true }; } + +/** + * Bind a party-scoped read (for example `?owner=` or `?trader=`) to the + * verified caller. Admin callers are handled by the HTTP layer before this + * function. Like write binding, this is a no-op when no caller secret is + * configured and fail-closed when it is configured. + */ +export function checkCallerRead( + req: IncomingMessage, + cfg: CallerAuthConfig, + subject: string, +): AuthCheck { + if (!cfg.callerJwtSecret) return { ok: true }; + const caller = callerPartyFromRequest(req, cfg); + if (!caller) { + return { + ok: false, + status: 401, + code: "unauthorized", + message: + "this private read requires a valid X-Caller-Token (per-caller party JWT)", + }; + } + if (caller !== subject) { + return { + ok: false, + status: 403, + code: "forbidden", + message: "caller may only read records for its own party", + }; + } + return { ok: true }; +} diff --git a/services/operator-backend/src/http/index.ts b/services/operator-backend/src/http/index.ts index a97a559c..302bbb8f 100644 --- a/services/operator-backend/src/http/index.ts +++ b/services/operator-backend/src/http/index.ts @@ -1,8 +1,9 @@ // HTTP surface over the operator backend services. // -// Runs on Node's built-in http server (no framework dependency). Not -// production-grade auth; production should put this behind an auth -// proxy that validates the trader's session. +// Runs on Node's built-in http server (no framework dependency). Bearer-token +// gates protect operator/admin writes, and an optional caller JWT binds private +// reads and trader-subject actions to the caller's Canton party. A hosted +// deployment should issue those credentials through its authenticated BFF. // // Endpoints (single-source list; matches `app/web/src/services/ledger.ts`): // @@ -47,9 +48,15 @@ import { mergeDisclosures } from "../ledger/disclosure.js"; import * as dec from "../pool/decimal.js"; import { DealersService } from "../dealers/index.js"; import { checkAdminAuth, checkOperatorAuth, bearerMatches } from "./auth.js"; -import { checkCallerBinding, callerPartyFromRequest, type CallerAuthConfig } from "./caller-auth.js"; +import { + checkCallerBinding, + checkCallerRead, + callerPartyFromRequest, + type CallerAuthConfig, +} from "./caller-auth.js"; import { validateWriteBody, ValidationError } from "./validate.js"; import { RfqAuthError } from "../rfq/index.js"; +import { OrderAuthError } from "../order/index.js"; import { rootLogger } from "../lib/logger.js"; const httpLog = rootLogger.child({ component: "http" }); @@ -105,29 +112,21 @@ function expectField(o: unknown, field: string): T { } /** - * Static context the dApp needs to build trader-authority intents. The - * dApp does not derive these from queries — it would have to guess - * which admin governs which instrument, which factory CID to use, etc. - * Surfacing them here keeps that knowledge on the operator's side. + * Static venue context. Factory CIDs and choice contexts are discovered per + * operation from the relevant V2 registry after exact choice arguments exist. */ export interface DexContext { operator: Party; lpRegistrar: Party; admin: Party; - allocationFactoryCid: string; - settlementFactoryCid: string; - allocationFactoryExtraArgs: { - context: { values: Record }; - meta: { values: Record }; - }; - allocationFactoryDisclosure: DisclosedContract[]; network: string; } export interface DexStatus { network: string; - /** Monotonic counter while this process runs. Stand-in for a real participant offset. */ + /** Latest participant ledger-end offset, or a dev-only local counter. */ slot: number; + /** Whether the most recent configured participant probe succeeded. */ synced: boolean; /** ISO timestamp the server cut this snapshot. */ serverTime: string; @@ -152,10 +151,15 @@ export interface HttpServerConfig { /** Allowlist of actAs parties the wallet relay may forward for. */ walletRelayParties?: string[]; /** - * HS256 secret for per-caller party binding. When set, write + * Trusted hosted-RFQ relay. These routes submit with trader authority and + * therefore require deployment-specific trader rights. Testnet/production + * entrypoints should leave this false unless caller binding is mandatory. + */ + hostedRfqEnabled?: boolean; + /** + * HS256 secret for per-caller party binding. When set, party-scoped reads and * routes that act on behalf of a trader require an X-Caller-Token JWT whose - * `sub` is the caller's party, and reject any request whose subject party is - * not the caller's own. Unset = binding disabled (single trusted backend). + * `sub` is the caller's party. Unset = binding disabled (single trusted backend). */ callerJwtSecret?: string; /** @@ -191,6 +195,24 @@ function pairParams(url: URL): { base: string; quote: string } | undefined { return base && quote ? { base, quote } : undefined; } +function boundedPositiveInt( + url: URL, + name: string, + fallback: number, + maximum: number, +): number { + const raw = url.searchParams.get(name); + if (raw === null) return fallback; + if (!/^\d+$/.test(raw)) { + throw new HttpError(400, "bad_request", `${name} must be a positive integer`); + } + const value = Number(raw); + if (!Number.isSafeInteger(value) || value < 1) { + throw new HttpError(400, "bad_request", `${name} must be a positive integer`); + } + return Math.min(value, maximum); +} + export interface HttpServerHandle { close: () => Promise; /** Base URL carrying the port actually bound, so `port: 0` is usable. */ @@ -202,48 +224,38 @@ export interface HttpServerHandle { export function startHttpServer( cfg: HttpServerConfig, ): Promise { - // Slot is the ledger's latest offset (ACS pruning watermark). We poll - // the participant every 2s and cache the result. Falls back to a local - // counter if the participant query fails so the UI's pill still moves. + // Poll the participant ledger end every 2s. A configured participant that + // cannot be reached must report synced=false; manufacturing a moving local + // slot here would make a broken testnet deployment look healthy. The local + // counter is used only by the in-memory dev server, which supplies no ledger + // URL/token at all. let slot = 0; - let lastPolledOk = false; const slotUrl = (cfg.ledgerUrl ?? "").replace(/\/$/, ""); const slotToken = cfg.ledgerToken; + const hasParticipantProbe = Boolean(slotUrl && slotToken); + let lastPollSucceeded = !hasParticipantProbe; async function pollSlot(): Promise { - if (!slotUrl || !slotToken) { + if (!hasParticipantProbe || !slotUrl || !slotToken) { slot += 1; + lastPollSucceeded = true; return; } try { const res = await fetch( - `${slotUrl}/v2/state/latest-pruned-offsets`, + `${slotUrl}/v2/state/ledger-end`, { headers: { Authorization: `Bearer ${slotToken}` } }, ); if (!res.ok) throw new Error(`HTTP ${res.status}`); - const body = (await res.json()) as { - participantPrunedUpToInclusive?: number; - }; - const offset = body.participantPrunedUpToInclusive; - if (typeof offset === "number" && offset > 0) { - slot = offset; - lastPolledOk = true; - } else { - // Pruned offset is 0 (nothing pruned yet) — fall back to ACS end. - const ledgerEndRes = await fetch( - `${slotUrl}/v2/state/ledger-end`, - { headers: { Authorization: `Bearer ${slotToken}` } }, - ); - if (ledgerEndRes.ok) { - const end = (await ledgerEndRes.json()) as { offset?: number }; - if (typeof end.offset === "number") { - slot = end.offset; - lastPolledOk = true; - } - } + const body = (await res.json()) as { offset?: number }; + if (typeof body.offset !== "number") { + throw new Error("ledger-end response has no numeric offset"); } + slot = body.offset; + lastPollSucceeded = true; } catch { - // Quiet on transient errors; keep the last good value or tick. - if (!lastPolledOk) slot += 1; + // Quiet on transient errors and keep the last genuine ledger offset, but + // expose the failed probe through /v1/status. + lastPollSucceeded = false; } } void pollSlot(); @@ -263,6 +275,7 @@ export function startHttpServer( cfg, cfg.context, () => slot, + () => lastPollSucceeded, cfg.db, allowedOrigins, req, @@ -286,6 +299,11 @@ export function startHttpServer( respondJson(res, 403, { error: e.message, code: "forbidden", requestId }); return; } + if (e instanceof OrderAuthError) { + reqLog.warn("request rejected", { status: 403, code: "forbidden", error: e.message }); + respondJson(res, 403, { error: e.message, code: "forbidden", requestId }); + return; + } if (e instanceof LedgerError && e.kind === "validation") { // A precondition/input failure surfaced by a service or the ledger — // a client error, not a server fault. @@ -336,6 +354,7 @@ async function routeRequest( cfg: HttpServerConfig, context: DexContext, getSlot: () => number, + getSynced: () => boolean, db: Db | undefined, allowedOrigins: string[], req: IncomingMessage, @@ -361,7 +380,10 @@ async function routeRequest( if (corsOrigin) res.setHeader("Access-Control-Allow-Origin", corsOrigin); res.setHeader("Vary", "Origin"); res.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"); - res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-Id"); + res.setHeader( + "Access-Control-Allow-Headers", + "Content-Type, Authorization, X-Caller-Token, X-Request-Id", + ); res.setHeader("Access-Control-Expose-Headers", "X-Request-Id"); if (method === "OPTIONS") { res.statusCode = 204; @@ -391,22 +413,35 @@ async function routeRequest( // === read endpoints ==================================================== if (method === "GET" && path === "/v1/context") { - const [factories, choiceContext] = await Promise.all([ - backend.registry.getFactories(context.admin), - backend.registry.getChoiceContext(context.admin), - ]); + respondJson(res, 200, context); + return; + } + + // Canonical Token Standard V2 allocation-factory discovery. The caller + // supplies the exact Daml JSON AllocationFactory_Allocate argument. + if (method === "POST" && path === "/v1/registry/allocation-factory") { + const body = await readJson<{ + admin: Party; + choiceArguments: Record; + }>(req); + if (typeof body.admin !== "string" || body.admin.length === 0) { + throw new HttpError(400, "bad_request", "admin is required"); + } + if ( + typeof body.choiceArguments !== "object" || + body.choiceArguments === null || + Array.isArray(body.choiceArguments) + ) { + throw new HttpError(400, "bad_request", "choiceArguments must be an object"); + } + const found = await backend.registry.getAllocationFactory( + body.admin, + body.choiceArguments, + ); respondJson(res, 200, { - ...context, - allocationFactoryCid: factories.allocationFactoryCid, - settlementFactoryCid: factories.settlementFactoryCid, - allocationFactoryExtraArgs: { - context: choiceContext.context, - meta: { values: {} }, - }, - allocationFactoryDisclosure: mergeDisclosures( - factories.disclosure, - choiceContext.disclosure, - ), + factoryCid: found.factoryCid, + extraArgs: { context: found.context, meta: { values: {} } }, + disclosure: found.disclosure, }); return; } @@ -415,7 +450,7 @@ async function routeRequest( const body: DexStatus = { network: context.network, slot: getSlot(), - synced: true, + synced: getSynced(), serverTime: new Date().toISOString(), }; respondJson(res, 200, body); @@ -444,6 +479,7 @@ async function routeRequest( if (!trader) { throw new HttpError(400, "bad_request", "missing ?trader= query parameter"); } + requireCallerForPrivateRead(req, callerAuth, trader, adminToken); const all = await backend.order.listOpen(); respondJson( res, @@ -596,6 +632,7 @@ async function routeRequest( if (!owner) { throw new HttpError(400, "bad_request", "missing ?owner= query parameter"); } + requireCallerForPrivateRead(req, callerAuth, owner, adminToken); // Per-contract (UTXO-style) rows. For a summed balance, use /v1/balances. respondJson(res, 200, await loadHoldings(backend, owner)); return; @@ -609,6 +646,7 @@ async function routeRequest( if (!owner) { throw new HttpError(400, "bad_request", "missing ?owner= query parameter"); } + requireCallerForPrivateRead(req, callerAuth, owner, adminToken); const holdings = await loadHoldings(backend, owner); const byInstrument = new Map(); for (const h of holdings) { @@ -675,10 +713,7 @@ async function routeRequest( respondJson(res, 400, { error: "missing ?pair=BASE/QUOTE" }); return; } - const hours = Math.max( - 1, - Math.min(24 * 30, parseInt(url.searchParams.get("hours") ?? "24", 10)), - ); + const hours = boundedPositiveInt(url, "hours", 24, 24 * 30); // `ts` is in milliseconds, so the bound must be too. const since = Date.now() - hours * 3600 * 1000; const rows = db @@ -973,11 +1008,9 @@ async function routeRequest( "missing ?trader= query parameter; the unfiltered view requires the admin token", ); } + if (trader) requireCallerForPrivateRead(req, callerAuth, trader, adminToken); const pair = url.searchParams.get("pair"); - const limit = Math.min( - parseInt(url.searchParams.get("limit") ?? "50", 10), - 500, - ); + const limit = boundedPositiveInt(url, "limit", 50, 500); const where: string[] = []; const args: unknown[] = []; if (trader) { @@ -1017,10 +1050,7 @@ async function routeRequest( // Default to swaps only: LP moves and pause/resume rotate the state too, // and existing callers pass no kind and expect the trade feed alone. const kind = kindParam ?? "swap"; - const limit = Math.min( - parseInt(url.searchParams.get("limit") ?? "50", 10), - 500, - ); + const limit = boundedPositiveInt(url, "limit", 50, 500); const sql = pair ? `SELECT * FROM swaps WHERE kind = ? AND pair = ? ORDER BY ts DESC LIMIT ${limit}` : `SELECT * FROM swaps WHERE kind = ? ORDER BY ts DESC LIMIT ${limit}`; @@ -1117,10 +1147,8 @@ async function routeRequest( "missing ?trader= query parameter; the unfiltered view requires the admin token", ); } - const limit = Math.min( - parseInt(url.searchParams.get("limit") ?? "100", 10), - 500, - ); + if (trader) requireCallerForPrivateRead(req, callerAuth, trader, adminToken); + const limit = boundedPositiveInt(url, "limit", 100, 500); const sql = trader ? `SELECT * FROM rfq_history WHERE trader = ? ORDER BY ts DESC LIMIT ${limit}` : `SELECT * FROM rfq_history ORDER BY ts DESC LIMIT ${limit}`; @@ -1183,6 +1211,7 @@ async function routeRequest( respondJson(res, 200, await backend.rfq.list()); return; } + requireCallerForPrivateRead(req, callerAuth, owner, adminToken); const { rfqs, quotes } = await backend.rfq.list(); respondJson(res, 200, { rfqs: rfqs.filter((r) => r.trader === owner || r.whitelist.includes(owner)), @@ -1192,6 +1221,13 @@ async function routeRequest( } if (method === "POST" && path === "/v1/rfq") { + if (cfg.hostedRfqEnabled === false) { + respondJson(res, 404, { + error: "hosted RFQ relay disabled; use a trader-authorized wallet flow", + code: "not_found", + }); + return; + } const body = await readValidatedJson[0]>(req, "POST /v1/rfq", callerAuth); const result = await backend.rfq.create(body); respondJson(res, 200, result); @@ -1201,6 +1237,13 @@ async function routeRequest( // /v1/rfq/:cid/cancel const rfqCancelMatch = path.match(/^\/v1\/rfq\/([^/]+)\/cancel$/); if (method === "POST" && rfqCancelMatch) { + if (cfg.hostedRfqEnabled === false) { + respondJson(res, 404, { + error: "hosted RFQ relay disabled; use a trader-authorized wallet flow", + code: "not_found", + }); + return; + } const rfqCid = decodeURIComponent(rfqCancelMatch[1]!); // Per-caller binding: cancel acts as the fetched // RFQ's trader, so the body-map binding can't cover it. Resolve the caller @@ -1213,6 +1256,13 @@ async function routeRequest( } if (method === "POST" && path === "/v1/rfq/accept") { + if (cfg.hostedRfqEnabled === false) { + respondJson(res, 404, { + error: "hosted RFQ relay disabled; use a trader-authorized wallet flow", + code: "not_found", + }); + return; + } const body = await readValidatedJson[0]>(req, "POST /v1/rfq/accept", callerAuth); // Same fetch-based binding as cancel: accept acts as the RFQ's trader, so // an operator-token holder must not accept a quote on a trader's behalf. @@ -1224,14 +1274,24 @@ async function routeRequest( if (method === "POST" && path === "/v1/orders/bind") { const body = await readValidatedJson[0]>(req, "POST /v1/orders/bind", callerAuth); - const result = await backend.order.bind(body); + const requireTrader = requireCallerForFetchBoundRoute( + req, + callerAuth, + "binding an order request", + ); + const result = await backend.order.bind({ ...body, requireTrader }); respondJson(res, 200, result); return; } if (method === "POST" && path === "/v1/orders/fund") { const body = await readValidatedJson[0]>(req, "POST /v1/orders/fund", callerAuth); - const result = await backend.order.fund(body); + const requireTrader = requireCallerForFetchBoundRoute( + req, + callerAuth, + "funding an order", + ); + const result = await backend.order.fund({ ...body, requireTrader }); respondJson(res, 200, result); return; } @@ -1240,7 +1300,12 @@ async function routeRequest( const cancelMatch = path.match(/^\/v1\/orders\/([^/]+)\/cancel$/); if (method === "POST" && cancelMatch) { const orderCid = decodeURIComponent(cancelMatch[1]!); - await backend.order.cancel(orderCid as never); + const requireTrader = requireCallerForFetchBoundRoute( + req, + callerAuth, + "cancelling an order", + ); + await backend.order.cancel(orderCid as never, requireTrader); respondJson(res, 204, {}); return; } @@ -1467,20 +1532,35 @@ async function loadHoldings( Array<{ owner: string; instrumentId: string; amount: string; locked: boolean }> > { type H = { owner: string; instrumentId: string; amount: string; locked: boolean }; - const load = async (templateId: string): Promise => { - try { - return await backend.ledger.query({ - templateId, - observingParty: owner as never, - }); - } catch { - return []; - } - }; - const holdings = await load("CantonDex.Registry.V2:Holding"); + let holdings: H[]; + try { + holdings = await backend.ledger.query({ + templateId: "CantonDex.Registry.V2:Holding", + observingParty: owner as never, + }); + } catch { + throw new HttpError( + 503, + "ledger_unavailable", + "unable to load holdings from the ledger", + ); + } return holdings.filter((h) => h.owner === owner); } +function requireCallerForPrivateRead( + req: IncomingMessage, + callerAuth: CallerAuthConfig, + subject: string, + adminToken: string | undefined, +): void { + if (adminToken && bearerMatches(req.headers["authorization"], adminToken)) return; + const binding = checkCallerRead(req, callerAuth, subject); + if (!binding.ok) { + throw new HttpError(binding.status, binding.code, binding.message); + } +} + async function readValidatedJson( req: IncomingMessage, routeKey: string, diff --git a/services/operator-backend/src/index.ts b/services/operator-backend/src/index.ts index 71657735..1f6d900e 100644 --- a/services/operator-backend/src/index.ts +++ b/services/operator-backend/src/index.ts @@ -27,7 +27,7 @@ // templates -- that's a guardrail violation. import type { LedgerSubmitter } from "./ledger/index.js"; -import type { RegistryClient } from "@canton-dex/registry-client"; +import type { RegistryDiscovery } from "@canton-dex/registry-client"; import { AdminService } from "./admin/index.js"; import { OrderService } from "./order/index.js"; @@ -44,7 +44,7 @@ import type { Party } from "./types.js"; export interface OperatorBackendConfig { ledger: LedgerSubmitter; - registry: RegistryClient; + registry: RegistryDiscovery; operatorParty: Party; } @@ -59,7 +59,7 @@ export class OperatorBackend { // that need to drive raw ledger commands. Production callers should // prefer the typed flow modules. readonly ledger: LedgerSubmitter; - readonly registry: RegistryClient; + readonly registry: RegistryDiscovery; readonly operatorParty: Party; constructor(cfg: OperatorBackendConfig) { diff --git a/services/operator-backend/src/ledger/choice-context.ts b/services/operator-backend/src/ledger/choice-context.ts index 17a2cdfb..b0221a02 100644 --- a/services/operator-backend/src/ledger/choice-context.ts +++ b/services/operator-backend/src/ledger/choice-context.ts @@ -1,11 +1,8 @@ -// Shared off-ledger choice-context fetch: wraps the registry's enriched -// context + disclosures into the extraArgs shape the token-standard choices -// take. Used by the pool, order, and matched-trade services. +// Convert one operation-specific registry response into the ExtraArgs shape +// expected by Token Standard choices. Discovery itself stays at the call site +// so a context cannot be fetched without the exact operation arguments. -import type { DisclosedContract } from "@canton-dex/registry-client"; -import { RegistryClient } from "@canton-dex/registry-client"; - -import type { Party } from "../types.js"; +import type { ChoiceContextRef, DisclosedContract } from "@canton-dex/registry-client"; export interface ChoiceContext { extraArgs: { @@ -15,13 +12,14 @@ export interface ChoiceContext { disclosure: DisclosedContract[]; } -export async function fetchChoiceContext( - registry: RegistryClient, - admin: Party, -): Promise { - const ctx = await registry.getChoiceContext(admin); +export function asChoiceContext(ctx: ChoiceContextRef): ChoiceContext { return { extraArgs: { context: ctx.context, meta: { values: {} } }, disclosure: ctx.disclosure, }; } + +export const emptyExtraArgs = { + context: { values: {} }, + meta: { values: {} }, +}; diff --git a/services/operator-backend/src/ledger/json-api.ts b/services/operator-backend/src/ledger/json-api.ts index 774d14d8..1747d91b 100644 --- a/services/operator-backend/src/ledger/json-api.ts +++ b/services/operator-backend/src/ledger/json-api.ts @@ -1,9 +1,9 @@ // JsonApiLedger -- LedgerSubmitter implementation that talks to a // real Canton participant via the JSON Ledger API. // -// This is the production driver. Tests can use it to drive against a -// live `daml start` (or a deployed Canton participant); the in-memory -// driver in `in-memory.ts` is the fast unit-test path. +// This is the live-participant driver. Tests can use it against the repository's +// default `dpm sandbox` proof or a separately operated Canton participant; the +// in-memory driver in `in-memory.ts` is the fast unit-test path. // // JSON API reference: // https://docs.daml.com/json-api/ (general) diff --git a/services/operator-backend/src/matched-trade/index.ts b/services/operator-backend/src/matched-trade/index.ts index a91b7c1d..c208acab 100644 --- a/services/operator-backend/src/matched-trade/index.ts +++ b/services/operator-backend/src/matched-trade/index.ts @@ -1,9 +1,9 @@ // MatchedTrade flow. import type { ContractId, DisclosedContract } from "@canton-dex/registry-client"; -import { RegistryClient } from "@canton-dex/registry-client"; +import type { RegistryDiscovery } from "@canton-dex/registry-client"; -import { fetchChoiceContext, type ChoiceContext } from "../ledger/choice-context.js"; +import { asChoiceContext } from "../ledger/choice-context.js"; import { mergeDisclosures } from "../ledger/disclosure.js"; import { LedgerSubmitter } from "../ledger/index.js"; import { retryOnContention } from "../ledger/submit-with-retry.js"; @@ -49,14 +49,10 @@ export interface SettlementBatchV2 { export class MatchedTradeService { constructor( private readonly ledger: LedgerSubmitter, - private readonly registry: RegistryClient, + private readonly registry: RegistryDiscovery, private readonly operatorParty: Party, ) {} - private choiceContext(admin: Party): Promise { - return fetchChoiceContext(this.registry, admin); - } - async requestAllocations( input: MatchedTradeRequestAllocationsInput, ): Promise[]> { @@ -76,6 +72,31 @@ export class MatchedTradeService { } async settle(input: MatchedTradeSettleInput): Promise { + const plansByAdmin = [...input.batchesByAdmin].map(([admin, batch]) => [ + admin, + { + transferLegs: batch.transferLegs, + allocations: batch.allocationCids.map((allocationCid) => ({ + allocationCid, + extraTransferLegSides: [], + nextIterationFunding: null, + })), + }, + ]); + const preview = await retryOnContention(() => + this.ledger.submit]>>({ + actAs: [this.operatorParty], + commandId: `mt-settle-preview:${input.tradeCid}`, + command: { + kind: "exercise", + templateId: "CantonDex.Dex.MatchedTrade:MatchedTrade", + contractId: input.tradeCid, + choice: "MatchedTrade_PreviewSettlement", + argument: { plansByAdmin }, + }, + }), + ); + const argumentsByAdmin = new Map(preview); const adminEntries: Array<{ admin: Party; batch: SettlementBatchV2; @@ -87,16 +108,18 @@ export class MatchedTradeService { disclosure: DisclosedContract[]; }> = []; for (const [admin, batch] of input.batchesByAdmin) { - const [factories, ctx] = await Promise.all([ - this.registry.getFactories(admin), - this.choiceContext(admin), - ]); + const choiceArguments = argumentsByAdmin.get(admin); + if (!choiceArguments) { + throw new Error(`matched trade preview omitted registry admin ${admin}`); + } + const factory = await this.registry.getSettlementFactory(admin, choiceArguments); + const ctx = asChoiceContext(factory); adminEntries.push({ admin, batch, - factoryCid: factories.settlementFactoryCid, + factoryCid: factory.factoryCid as ContractId<"SettlementFactory">, extraArgs: ctx.extraArgs, - disclosure: mergeDisclosures(factories.disclosure, ctx.disclosure), + disclosure: ctx.disclosure, }); } @@ -156,10 +179,15 @@ export class MatchedTradeService { >; }> = []; for (const [admin, allocationCids] of input.allocationsByAdmin) { - const ctx = await this.choiceContext(admin); + const contexts = await Promise.all( + allocationCids.map((cid) => this.registry.getAllocationCancelContext(admin, cid)), + ); adminEntries.push({ - disclosure: ctx.disclosure, - allocationsToCancel: allocationCids.map((cid) => [cid, ctx.extraArgs]), + disclosure: mergeDisclosures(...contexts.map((ctx) => ctx.disclosure)), + allocationsToCancel: allocationCids.map((cid, index) => [ + cid, + asChoiceContext(contexts[index]!).extraArgs, + ]), }); } diff --git a/services/operator-backend/src/order/index.ts b/services/operator-backend/src/order/index.ts index d8fa95a2..038c34d7 100644 --- a/services/operator-backend/src/order/index.ts +++ b/services/operator-backend/src/order/index.ts @@ -2,9 +2,9 @@ // order, attach the trader-authored funding allocation, then match or cancel. import type { ContractId } from "@canton-dex/registry-client"; -import { RegistryClient } from "@canton-dex/registry-client"; +import type { RegistryDiscovery } from "@canton-dex/registry-client"; -import { fetchChoiceContext, type ChoiceContext } from "../ledger/choice-context.js"; +import { asChoiceContext } from "../ledger/choice-context.js"; import { mergeDisclosures } from "../ledger/disclosure.js"; import { LedgerSubmitter, type SubmitRequest } from "../ledger/index.js"; import { @@ -35,6 +35,8 @@ export interface OrderBindInput { // transaction tree. updateId?: string | null; settlementRef: string; + /** Verified caller party when per-caller binding is enabled. */ + requireTrader?: Party; } export interface OrderBindResult { @@ -54,6 +56,16 @@ export interface OrderFundInput { // The OrderAllocationRequest created at bind. Order_Fund consumes it together // with the pending order after validating the allocation specification. allocationRequestCid?: ContractId<"OrderAllocationRequest"> | null; + /** Verified caller party when per-caller binding is enabled. */ + requireTrader?: Party; +} + +/** Thrown when a caller tries to mutate another trader's order workflow. */ +export class OrderAuthError extends Error { + constructor(message: string) { + super(message); + this.name = "OrderAuthError"; + } } export interface OrderCancelResult { @@ -90,17 +102,17 @@ interface LiveOrder { allocationCid: ContractId<"Allocation"> | null; } +function basicAccount(owner: Party): V2Account { + return { owner, provider: null, id: "" }; +} + export class OrderService { constructor( private readonly ledger: LedgerSubmitter, - private readonly registry: RegistryClient, + private readonly registry: RegistryDiscovery, private readonly operatorParty: Party, ) {} - private choiceContext(admin: Party): Promise { - return fetchChoiceContext(this.registry, admin); - } - async bind(input: OrderBindInput): Promise { // Recover the created request from the transaction tree when a wallet // returns an update id instead of contract ids. @@ -117,6 +129,17 @@ export class OrderService { "order bind: supply fundingRequestCid or an updateId to recover it", ); } + if (input.requireTrader !== undefined) { + const requests = await this.ledger.query<{ contractId: string; trader: Party }>({ + templateId: "CantonDex.Dex.OrderFundingRequest:OrderFundingRequest", + observingParty: this.operatorParty, + }); + const request = requests.find((row) => row.contractId === fundingRequestCid); + if (!request) throw new Error(`Order funding request ${fundingRequestCid} not found`); + if (request.trader !== input.requireTrader) { + throw new OrderAuthError("caller may only bind its own order request"); + } + } const result = await retryOnContention(() => this.ledger.submit({ actAs: [this.operatorParty], @@ -152,6 +175,13 @@ export class OrderService { if (!allocationCid) { throw new Error("order fund: supply allocationCid or an updateId to recover it"); } + if (input.requireTrader !== undefined) { + const order = (await this.listOpen()).find((row) => row.contractId === input.orderCid); + if (!order) throw new Error(`Order ${input.orderCid} not found`); + if (order.trader !== input.requireTrader) { + throw new OrderAuthError("caller may only fund its own order"); + } + } return retryOnContention(() => this.ledger.submit<{ orderCid: ContractId<"Order"> }>({ actAs: [this.operatorParty], @@ -171,19 +201,25 @@ export class OrderService { ); } - async cancel(orderCid: ContractId<"Order">): Promise { + async cancel( + orderCid: ContractId<"Order">, + requireTrader?: Party, + ): Promise { const order = (await this.listOpen()).find((o) => o.contractId === orderCid); if (!order) throw new Error(`Order ${orderCid} not found`); - const [factories, ctx] = await Promise.all([ - this.registry.getFactories(order.admin), - this.choiceContext(order.admin), - ]); + if (requireTrader !== undefined && order.trader !== requireTrader) { + throw new OrderAuthError("caller may only cancel its own order"); + } + const discovered = order.allocationCid + ? await this.registry.getAllocationCancelContext(order.admin, order.allocationCid) + : { context: { values: {} }, disclosure: [] }; + const ctx = asChoiceContext(discovered); const req: SubmitRequest = { actAs: [this.operatorParty], // Cancellation may release holdings visible only to the owner and // registry admin, so include the registry's choice context and disclosure. commandId: `order-cancel:${orderCid}`, - disclosure: mergeDisclosures(factories.disclosure, ctx.disclosure), + disclosure: mergeDisclosures(ctx.disclosure), command: { kind: "exercise", templateId: "CantonDex.Dex.Order:Order", @@ -250,13 +286,14 @@ export class OrderService { /** * Discover crossing orders for a pair and settle each one atomically via - * `OrderMatchExecution_Execute`: a single submission that re-checks the fill + * `OrderMatchExecution_Execute`: one value-moving submission that re-checks the fill * against both orders' own terms, builds the base/quote transfer legs, runs * the settle batch that consumes both funding allocations, rolls each order * onto the allocation that batch minted, and records the settled trade. * - * One submission per match, so there is no window in which the funds have - * moved but an order still points at the allocation the settle archived. + * A read-only Daml preview first supplies the registry with the exact batch. + * The subsequent execute still moves funds, rolls orders, and records the + * trade atomically. * * Each match is settled independently; one failure doesn't abort the * rest of the run. @@ -268,10 +305,6 @@ export class OrderService { }): Promise { const matches = await this.findMatches(input); if (matches.length === 0) return []; - const [factories, ctx] = await Promise.all([ - this.registry.getFactories(input.admin), - this.choiceContext(input.admin), - ]); const out: MatchRunResult[] = []; // One order can fill against several counterparties in a single run, and // each fill archives it and consumes its allocation. Track what the @@ -309,52 +342,58 @@ export class OrderService { if (!buy.allocationCid || !sell.allocationCid) { throw new Error(`match ${matchId}: a matched order has no funding allocation`); } - const acct = (owner: Party): V2Account => ({ - owner, - provider: null, - id: "", - }); + const executionArgument = { + operator: this.operatorParty, + matchId, + match: { + buyerAccount: basicAccount(m.buy.trader), + sellerAccount: basicAccount(m.sell.trader), + baseInstrumentId: m.buy.baseInstrumentId, + quoteInstrumentId: m.buy.quoteInstrumentId, + fillQty: m.quantity, + fillPrice: m.price, + }, + buyOrderCid: buy.cid, + sellOrderCid: sell.cid, + buyerAllocationCid: buy.allocationCid, + sellerAllocationCid: sell.allocationCid, + buyerCommittedFunding: {}, + sellerCommittedFunding: {}, + }; + const settlementArguments = await retryOnContention(() => + this.ledger.submit>({ + actAs: [this.operatorParty], + readAs: [input.admin], + commandId: `order-match-preview:${matchId}`, + command: { + kind: "createAndExercise", + templateId: + "CantonDex.Dex.OrderMatchExecution:OrderMatchExecution", + argument: executionArgument, + choice: "OrderMatchExecution_PreviewSettlement", + choiceArgument: {}, + }, + }), + ); + const factory = await this.registry.getSettlementFactory( + input.admin, + settlementArguments, + ); + const ctx = asChoiceContext(factory); const executed = await retryOnContention(() => this.ledger.submit({ actAs: [this.operatorParty], - // The settle fetches each order's funding allocation and the - // holdings it locked -- `signatory admin, owner`, which the - // operator is not a stakeholder of. readAs the instrument admin so - // it can see them; without it the settle fails CONTRACT_NOT_FOUND - // on the funding it is trying to move. Both orders share an admin - // (asserted by the choice), so one entry covers both sides. readAs: [input.admin], commandId: `order-match:${matchId}`, - // Factory + choice-context disclosure for the registry's own - // contracts. - disclosure: mergeDisclosures(factories.disclosure, ctx.disclosure), + disclosure: ctx.disclosure, command: { kind: "createAndExercise", templateId: "CantonDex.Dex.OrderMatchExecution:OrderMatchExecution", - argument: { - operator: this.operatorParty, - matchId, - match: { - buyerAccount: acct(m.buy.trader), - sellerAccount: acct(m.sell.trader), - baseInstrumentId: m.buy.baseInstrumentId, - quoteInstrumentId: m.buy.quoteInstrumentId, - fillQty: m.quantity, - fillPrice: m.price, - }, - buyOrderCid: buy.cid, - sellOrderCid: sell.cid, - buyerAllocationCid: buy.allocationCid, - sellerAllocationCid: sell.allocationCid, - // [COMPAT] These fields are retained for package lineage and - // ignored by the choice. Each budget comes from its allocation. - buyerCommittedFunding: {}, - sellerCommittedFunding: {}, - }, + argument: executionArgument, choice: "OrderMatchExecution_Execute", choiceArgument: { - factoryCid: factories.settlementFactoryCid, + factoryCid: factory.factoryCid, extraArgs: ctx.extraArgs, }, }, @@ -366,13 +405,19 @@ export class OrderService { const buyRemainderCid = executed.buyRemainderCid ?? null; advance( m.buy, - buyRemainderCid && { cid: buyRemainderCid, allocationCid: buyNext }, + buyRemainderCid && { + cid: buyRemainderCid, + allocationCid: buyNext, + }, ); const sellNext = executed.sellerNextAllocationCid ?? null; const sellRemainderCid = executed.sellRemainderCid ?? null; advance( m.sell, - sellRemainderCid && { cid: sellRemainderCid, allocationCid: sellNext }, + sellRemainderCid && { + cid: sellRemainderCid, + allocationCid: sellNext, + }, ); out.push({ buyCid: m.buy.contractId, diff --git a/services/operator-backend/src/policy/index.ts b/services/operator-backend/src/policy/index.ts index 48b4ab9e..944a0515 100644 --- a/services/operator-backend/src/policy/index.ts +++ b/services/operator-backend/src/policy/index.ts @@ -3,14 +3,12 @@ import { createHash, createHmac } from "node:crypto"; -import { parseDecimal } from "../pool/decimal.js"; import type { Decimal, Party, PolicyReceipt, RankedDealer, RfqQuote, - RfqSide, Time, } from "../types.js"; @@ -19,24 +17,14 @@ import type { export const POLICY_VERSION = "v2.0"; export const POLICY_HASH = "sha256:rfq-policy-v2.0"; -// Compare two Daml Decimal strings exactly (10dp, no IEEE-754) so the -// ranking agrees with the on-ledger Decimal ordering in -// trading/CantonDex/Dex/Rfq.daml. Returns -1 / 0 / 1. -export function compareDecimal(a: string, b: string): number { - const da = parseDecimal(a); - const db = parseDecimal(b); - return da < db ? -1 : da > db ? 1 : 0; -} - export function rankQuotes( - side: RfqSide, quotes: RfqQuote[], now: Time, ): RfqQuote[] { const valid = quotes.filter( (q) => Date.parse(q.expiresAt) > Date.parse(now), ); - // Reproduces `policyCmp` (Rfq.daml:241-256) exactly: trusted tier first, + // Reproduces `policyCmp` in Rfq.daml exactly: trusted tier first, // then LATER expiresAt (more time to act), then EARLIER postedAt // (first-mover), then a deterministic dealer-party tie-break. // @@ -70,7 +58,6 @@ export function rankedDealersOf(ranked: RfqQuote[]): RankedDealer[] { export function buildReceipt(args: { rfqId: string; - side: RfqSide; quotes: RfqQuote[]; acceptedDealer: Party; signedBy: Party; @@ -78,7 +65,7 @@ export function buildReceipt(args: { now?: Time; }): PolicyReceipt { const now = args.now ?? args.signedAt; - const ranked = rankQuotes(args.side, args.quotes, now); + const ranked = rankQuotes(args.quotes, now); const rankedDealers = rankedDealersOf(ranked); const idx = rankedDealers.findIndex((d) => d.party === args.acceptedDealer); if (idx < 0) { diff --git a/services/operator-backend/src/pool/index.ts b/services/operator-backend/src/pool/index.ts index 3c8743c0..69b769a9 100644 --- a/services/operator-backend/src/pool/index.ts +++ b/services/operator-backend/src/pool/index.ts @@ -3,10 +3,10 @@ import { createHash } from "node:crypto"; import { LedgerError } from "../ledger/index.js"; -import type { ContractId, DisclosedContract } from "@canton-dex/registry-client"; -import { RegistryClient } from "@canton-dex/registry-client"; +import type { ContractId } from "@canton-dex/registry-client"; +import type { RegistryDiscovery } from "@canton-dex/registry-client"; -import { fetchChoiceContext, type ChoiceContext } from "../ledger/choice-context.js"; +import { asChoiceContext } from "../ledger/choice-context.js"; import { mergeDisclosures } from "../ledger/disclosure.js"; import { LedgerSubmitter } from "../ledger/index.js"; import { recoverCreatedAllocations } from "../ledger/recover.js"; @@ -31,9 +31,52 @@ import type { V2SettlementInfo, } from "../types.js"; -interface RegistryExtraArgs { - context: { values: Record }; - meta: { values: Record }; +type ChoiceArguments = Record; + +interface AllocationInstructionResult { + output?: { + tag?: string; + value?: { allocationCid?: string }; + }; +} + +interface AddLiquidityAllocationPlan { + baseReceiver: ChoiceArguments; + quoteReceiver: ChoiceArguments; + lpMintSender: ChoiceArguments; +} + +interface AddLiquiditySettlementPlan { + baseQuoteBatch: ChoiceArguments; + lpBatch: ChoiceArguments; +} + +interface RemoveLiquidityAllocationPlan { + lpBurnReceiver: ChoiceArguments; +} + +interface RemoveLiquiditySettlementPlan { + baseQuoteBatch: ChoiceArguments; + lpBatch: ChoiceArguments; +} + +function completedAllocationCid( + result: AllocationInstructionResult, + operation: string, +): ContractId<"Allocation"> { + const tag = result.output?.tag; + const allocationCid = result.output?.value?.allocationCid; + if ( + (tag !== "AllocationInstructionResult_Completed" && tag !== "Completed") || + !allocationCid + ) { + throw new LedgerError( + "unsupported", + `${operation}: registry did not complete allocation creation synchronously`, + false, + ); + } + return allocationCid as ContractId<"Allocation">; } export interface PoolSwapInput { @@ -140,10 +183,6 @@ export interface PoolRequestSwapResult { allocationSpec: V2AllocationSpecification; settlement: V2SettlementInfo; quoteBinding: PoolSwapQuoteBinding; - // The pool-admin allocation factory the swapper allocates under. - factoryCid: ContractId<"AllocationFactory">; - allocationFactoryExtraArgs: RegistryExtraArgs; - allocationFactoryDisclosure: DisclosedContract[]; } // === DvP liquidity ========================================== @@ -187,13 +226,6 @@ export interface PoolRequestAddLiquidityResult extends LiquidityMatch { // The on-ledger specs the wallet authors, in canonical order. allocations: V2AllocationSpecification[]; settlement: V2SettlementInfo; - // Distinct factories for pool-admin vs lpRegistrar allocations. - depositFactoryCid: ContractId<"AllocationFactory">; - lpFactoryCid: ContractId<"AllocationFactory">; - depositFactoryExtraArgs: RegistryExtraArgs; - lpFactoryExtraArgs: RegistryExtraArgs; - depositFactoryDisclosure: DisclosedContract[]; - lpFactoryDisclosure: DisclosedContract[]; } export interface PoolSettleAddLiquidityInput { @@ -240,12 +272,6 @@ export interface PoolRequestRemoveLiquidityResult { // The on-ledger specs the holder authors. allocations: V2AllocationSpecification[]; settlement: V2SettlementInfo; - depositFactoryCid: ContractId<"AllocationFactory">; - lpFactoryCid: ContractId<"AllocationFactory">; - depositFactoryExtraArgs: RegistryExtraArgs; - lpFactoryExtraArgs: RegistryExtraArgs; - depositFactoryDisclosure: DisclosedContract[]; - lpFactoryDisclosure: DisclosedContract[]; } export interface PoolSettleRemoveLiquidityInput { @@ -302,14 +328,10 @@ function normalizePoolStatus(raw: unknown): Pool["status"] { export class PoolService { constructor( private readonly ledger: LedgerSubmitter, - private readonly registry: RegistryClient, + private readonly registry: RegistryDiscovery, private readonly operatorParty: Party, ) {} - private choiceContext(admin: Party): Promise { - return fetchChoiceContext(this.registry, admin); - } - private async rulesCid(): Promise> { const rules = await this.ledger.query({ templateId: "CantonDex.Dex.PoolRules:PoolRules", @@ -534,8 +556,6 @@ export class PoolService { false, ); } - const factories = await this.registry.getFactories(pool.admin); - const ctx = await this.choiceContext(pool.admin); const inputIsBase = input.inputInstrumentId === pool.baseInstrumentId; const inputSlices = inputIsBase ? pool.baseSlices : pool.quoteSlices; const outputSlices = inputIsBase ? pool.quoteSlices : pool.baseSlices; @@ -564,31 +584,53 @@ export class PoolService { updateId: input.updateId ?? null, }); const commandId = `pool-swap:${input.poolCid}:${swapKey}`; + const swapArgument = { + expectedPoolId: pool.poolId, + poolCid: input.poolCid, + poolStateCid: binding.poolStateCid, + swapperAccount: input.swapperAccount, + inputInstrumentId: input.inputInstrumentId, + inputAmount: input.inputAmount, + minOutputAmount: input.minOutputAmount, + swapperAllocationCid, + inputSliceCid: binding.inputSliceCid, + outputSliceCids: binding.outputSliceCids, + quoteBinding: binding, + }; + const settlementArguments = await retryOnContention(() => + this.ledger.submit>({ + actAs: [this.operatorParty], + readAs: input.swapperAccount.owner ? [input.swapperAccount.owner] : [], + commandId: `${commandId}:preview`, + command: { + kind: "exercise", + templateId: "CantonDex.Dex.PoolRules:PoolRules", + contractId: pool.rulesCid, + choice: "PoolRules_PreviewSwapSettlement", + argument: swapArgument, + }, + }), + ); + const settlementFactory = await this.registry.getSettlementFactory( + pool.admin, + settlementArguments, + ); + const settlementContext = asChoiceContext(settlementFactory); return retryOnContention(() => this.ledger.submit({ actAs: [this.operatorParty], readAs: input.swapperAccount.owner ? [input.swapperAccount.owner] : [], commandId, - disclosure: mergeDisclosures(factories.disclosure, ctx.disclosure), + disclosure: settlementContext.disclosure, command: { kind: "exercise", templateId: "CantonDex.Dex.PoolRules:PoolRules", contractId: pool.rulesCid, choice: "PoolRules_Swap", argument: { - expectedPoolId: pool.poolId, - poolCid: input.poolCid, - poolStateCid: binding.poolStateCid, - swapperAccount: input.swapperAccount, - inputInstrumentId: input.inputInstrumentId, - inputAmount: input.inputAmount, - minOutputAmount: input.minOutputAmount, - swapperAllocationCid, - inputSliceCid: binding.inputSliceCid, - outputSliceCids: binding.outputSliceCids, - factoryCid: factories.settlementFactoryCid, - extraArgs: ctx.extraArgs, - quoteBinding: binding, + ...swapArgument, + factoryCid: settlementFactory.factoryCid, + extraArgs: settlementContext.extraArgs, }, }, }), @@ -626,10 +668,6 @@ export class PoolService { outputSliceCids: selectCoveringPrefix(outputSlices, amountOut), minOutputAmount: input.minOutputAmount, }; - const [factories, ctx] = await Promise.all([ - this.registry.getFactories(pool.admin), - this.choiceContext(pool.admin), - ]); const result = await this.ledger.submit<{ settlement: V2SettlementInfo; allocationSpec: V2AllocationSpecification; @@ -664,12 +702,6 @@ export class PoolService { allocationSpec: result.allocationSpec, settlement: result.settlement, quoteBinding: result.quoteBinding, - factoryCid: factories.allocationFactoryCid, - allocationFactoryExtraArgs: ctx.extraArgs, - allocationFactoryDisclosure: mergeDisclosures( - factories.disclosure, - ctx.disclosure, - ), }; } @@ -689,22 +721,42 @@ export class PoolService { return { pool, liquidityRulesCid: this.requirePoolLiquidityRules(pool) }; } - private async loadLiquidityFactories(pool: Pool) { - const [depositFactories, lpFactories] = await Promise.all([ - this.registry.getFactories(pool.admin), - this.registry.getFactories(pool.lpRegistrar), - ]); - return { depositFactories, lpFactories }; - } - - private async loadLiquiditySurface(pool: Pool) { - const [{ depositFactories, lpFactories }, depositContext, lpContext] = - await Promise.all([ - this.loadLiquidityFactories(pool), - this.choiceContext(pool.admin), - this.choiceContext(pool.lpRegistrar), - ]); - return { depositFactories, lpFactories, depositContext, lpContext }; + private async createRegistryAllocation( + admin: Party, + choiceArguments: ChoiceArguments, + commandId: string, + actAs: Party[], + ): Promise<{ + allocationCid: ContractId<"Allocation">; + factoryCid: ContractId<"AllocationFactory">; + }> { + const discovered = await this.registry.getAllocationFactory( + admin, + choiceArguments, + ); + const context = asChoiceContext(discovered); + const result = await retryOnContention(() => + this.ledger.submit({ + actAs, + commandId, + disclosure: context.disclosure, + command: { + kind: "exerciseInterface", + interfaceId: + "Splice.Api.Token.AllocationInstructionV2:AllocationFactory", + contractId: discovered.factoryCid, + choice: "AllocationFactory_Allocate", + argument: { + ...choiceArguments, + extraArgs: context.extraArgs, + }, + }, + }), + ); + return { + allocationCid: completedAllocationCid(result, commandId), + factoryCid: discovered.factoryCid, + }; } /** Read back a newly-created liquidity request. */ @@ -848,8 +900,6 @@ export class PoolService { }), ); const req = await this.fetchRequest(requestCid); - const { depositFactories, lpFactories, depositContext, lpContext } = - await this.loadLiquiditySurface(pool); return { ...match, requestCid, @@ -858,18 +908,6 @@ export class PoolService { quoteAmount: input.quoteAmount, allocations: req.allocations, settlement: req.settlement, - depositFactoryCid: depositFactories.allocationFactoryCid, - lpFactoryCid: lpFactories.allocationFactoryCid, - depositFactoryExtraArgs: depositContext.extraArgs, - lpFactoryExtraArgs: lpContext.extraArgs, - depositFactoryDisclosure: mergeDisclosures( - depositFactories.disclosure, - depositContext.disclosure, - ), - lpFactoryDisclosure: mergeDisclosures( - lpFactories.disclosure, - lpContext.disclosure, - ), }; } @@ -877,8 +915,6 @@ export class PoolService { async settleAddLiquidity(input: PoolSettleAddLiquidityInput): Promise { const { pool, liquidityRulesCid } = await this.fetchLiquidityPool(input.poolCid); const lpPolicyCid = await this.fetchLpAssetPolicy(pool); - const { depositFactories, lpFactories, depositContext, lpContext } = - await this.loadLiquiditySurface(pool); // Resolve the three created allocation cids + the binding. On the // operator-discovery path (updateId-only wallet, e.g. PartyLayer) the @@ -896,23 +932,92 @@ export class PoolService { "settleAddLiquidity: supply the 3 allocation cids or an updateId to recover them", ); } + const flowKey = requestCid ?? acceptanceCid ?? input.updateId; + if (!flowKey) { + throw new Error("settleAddLiquidity: request, acceptance, or update id is required"); + } + const preparation = { + expectedPoolId: pool.poolId, + poolCid: input.poolCid, + poolStateCid: pool.poolStateCid, + lpPolicyCid, + requestCid: requestCid ?? null, + acceptanceCid: acceptanceCid ?? null, + recipient: input.recipient, + lpBaseDepositCid, + lpQuoteDepositCid, + lpReceiptCid, + baseAmount: input.baseAmount, + quoteAmount: input.quoteAmount, + minLpTokens: input.minLpTokens, + knownTotalLpSupply: input.knownTotalLpSupply, + }; + const allocationPlan = await retryOnContention(() => + this.ledger.submit({ + actAs: [this.operatorParty, pool.lpRegistrar], + commandId: `lp-add-preview-allocations:${flowKey}`, + command: { + kind: "exercise", + templateId: "CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules", + contractId: liquidityRulesCid, + choice: "PoolLiquidityRules_PreviewAddAllocations", + argument: { preparation, requestedAt: input.requestedAt }, + }, + }), + ); + const [operatorBaseReceiver, operatorQuoteReceiver, registrarMint] = + await Promise.all([ + this.createRegistryAllocation( + pool.admin, + allocationPlan.baseReceiver, + `lp-add-base-receiver:${flowKey}`, + [this.operatorParty], + ), + this.createRegistryAllocation( + pool.admin, + allocationPlan.quoteReceiver, + `lp-add-quote-receiver:${flowKey}`, + [this.operatorParty], + ), + this.createRegistryAllocation( + pool.lpRegistrar, + allocationPlan.lpMintSender, + `lp-add-mint-sender:${flowKey}`, + [pool.lpRegistrar], + ), + ]); + const settlementPlan = await retryOnContention(() => + this.ledger.submit({ + actAs: [this.operatorParty, pool.lpRegistrar], + commandId: `lp-add-preview-settlement:${flowKey}`, + command: { + kind: "exercise", + templateId: "CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules", + contractId: liquidityRulesCid, + choice: "PoolLiquidityRules_PreviewAddSettlement", + argument: { + preparation, + operatorBaseReceiverCid: operatorBaseReceiver.allocationCid, + operatorQuoteReceiverCid: operatorQuoteReceiver.allocationCid, + registrarMintCid: registrarMint.allocationCid, + }, + }, + }), + ); + const [poolSettlementFactory, lpSettlementFactory] = await Promise.all([ + this.registry.getSettlementFactory(pool.admin, settlementPlan.baseQuoteBatch), + this.registry.getSettlementFactory(pool.lpRegistrar, settlementPlan.lpBatch), + ]); + const poolSettlementContext = asChoiceContext(poolSettlementFactory); + const lpSettlementContext = asChoiceContext(lpSettlementFactory); - // Split-admin DvP: the base/quote batch settles under pool.admin and the - // LP-mint batch under pool.lpRegistrar, so each carries its own registry - // choice context. For the self-registry both contexts are empty. - // - // The LP's deposit holdings are `signatory admin, owner`, so the operator - // cannot see them. Registry discovery supplies the transaction-wide - // disclosures needed to validate the nested choices. return retryOnContention(() => this.ledger.submit({ actAs: [this.operatorParty, pool.lpRegistrar], - commandId: `lp-add-settle:${requestCid ?? acceptanceCid ?? input.updateId}`, + commandId: `lp-add-settle:${flowKey}`, disclosure: mergeDisclosures( - depositFactories.disclosure, - lpFactories.disclosure, - depositContext.disclosure, - lpContext.disclosure, + poolSettlementContext.disclosure, + lpSettlementContext.disclosure, ), command: { kind: "exercise", @@ -920,28 +1025,18 @@ export class PoolService { contractId: liquidityRulesCid, choice: "PoolLiquidityRules_SettleAddLiquidity", argument: { - expectedPoolId: pool.poolId, - poolCid: input.poolCid, - poolStateCid: pool.poolStateCid, - lpPolicyCid, - requestCid: requestCid ?? null, - acceptanceCid: acceptanceCid ?? null, - recipient: input.recipient, - lpBaseDepositCid, - lpQuoteDepositCid, - lpReceiptCid, - baseFactoryCid: depositFactories.allocationFactoryCid, - quoteFactoryCid: depositFactories.allocationFactoryCid, - lpFactoryCid: lpFactories.allocationFactoryCid, - baseQuoteSettleCid: depositFactories.settlementFactoryCid, - lpSettleCid: lpFactories.settlementFactoryCid, - baseAmount: input.baseAmount, - quoteAmount: input.quoteAmount, - minLpTokens: input.minLpTokens, - knownTotalLpSupply: input.knownTotalLpSupply, + ...preparation, + baseFactoryCid: operatorBaseReceiver.factoryCid, + quoteFactoryCid: operatorQuoteReceiver.factoryCid, + lpFactoryCid: registrarMint.factoryCid, + baseQuoteSettleCid: poolSettlementFactory.factoryCid, + lpSettleCid: lpSettlementFactory.factoryCid, requestedAt: input.requestedAt, - poolAdminExtraArgs: depositContext.extraArgs, - lpRegistrarExtraArgs: lpContext.extraArgs, + poolAdminExtraArgs: poolSettlementContext.extraArgs, + lpRegistrarExtraArgs: lpSettlementContext.extraArgs, + operatorBaseReceiverCid: operatorBaseReceiver.allocationCid, + operatorQuoteReceiverCid: operatorQuoteReceiver.allocationCid, + registrarMintCid: registrarMint.allocationCid, }, }, }), @@ -1012,8 +1107,6 @@ export class PoolService { }), ); const req = await this.fetchRequest(requestCid); - const { depositFactories, lpFactories, depositContext, lpContext } = - await this.loadLiquiditySurface(pool); return { requestCid, knownTotalLpSupply: pool.totalLpSupply, @@ -1023,18 +1116,6 @@ export class PoolService { quoteOuts: plan.quote.outs, allocations: req.allocations, settlement: req.settlement, - depositFactoryCid: depositFactories.allocationFactoryCid, - lpFactoryCid: lpFactories.allocationFactoryCid, - depositFactoryExtraArgs: depositContext.extraArgs, - lpFactoryExtraArgs: lpContext.extraArgs, - depositFactoryDisclosure: mergeDisclosures( - depositFactories.disclosure, - depositContext.disclosure, - ), - lpFactoryDisclosure: mergeDisclosures( - lpFactories.disclosure, - lpContext.disclosure, - ), }; } @@ -1044,8 +1125,6 @@ export class PoolService { // Re-derive from current state; drift since /request aborts at settle. const plan = this.deriveRemovePlan(pool, input.lpTokensToRedeem, input.knownTotalLpSupply); const lpPolicyCid = await this.fetchLpAssetPolicy(pool); - const { depositFactories, lpFactories, depositContext, lpContext } = - await this.loadLiquiditySurface(pool); // Operator-discovery path (updateId-only wallet): recover the 3 created // allocation cids [base receipt, quote receipt, burn-sender] + acceptance. @@ -1061,23 +1140,77 @@ export class PoolService { "settleRemoveLiquidity: supply the 3 allocation cids or an updateId to recover them", ); } + const flowKey = requestCid ?? acceptanceCid ?? input.updateId; + if (!flowKey) { + throw new Error("settleRemoveLiquidity: request, acceptance, or update id is required"); + } + const preparation = { + expectedPoolId: pool.poolId, + poolCid: input.poolCid, + poolStateCid: pool.poolStateCid, + lpPolicyCid, + requestCid: requestCid ?? null, + acceptanceCid: acceptanceCid ?? null, + holder: input.holder, + lpTokensToRedeem: input.lpTokensToRedeem, + knownTotalLpSupply: input.knownTotalLpSupply, + minBaseOut: input.minBaseOut, + minQuoteOut: input.minQuoteOut, + baseSliceCids: plan.base.sliceCids, + quoteSliceCids: plan.quote.sliceCids, + holderBaseReceiptCid, + holderQuoteReceiptCid, + holderBurnSenderCid, + }; + const allocationPlan = await retryOnContention(() => + this.ledger.submit({ + actAs: [this.operatorParty, pool.lpRegistrar], + commandId: `lp-remove-preview-allocations:${flowKey}`, + command: { + kind: "exercise", + templateId: "CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules", + contractId: liquidityRulesCid, + choice: "PoolLiquidityRules_PreviewRemoveAllocations", + argument: { preparation, requestedAt: input.requestedAt }, + }, + }), + ); + const registrarBurnReceiver = await this.createRegistryAllocation( + pool.lpRegistrar, + allocationPlan.lpBurnReceiver, + `lp-remove-burn-receiver:${flowKey}`, + [pool.lpRegistrar], + ); + const settlementPlan = await retryOnContention(() => + this.ledger.submit({ + actAs: [this.operatorParty, pool.lpRegistrar], + commandId: `lp-remove-preview-settlement:${flowKey}`, + command: { + kind: "exercise", + templateId: "CantonDex.Dex.PoolLiquidityRules:PoolLiquidityRules", + contractId: liquidityRulesCid, + choice: "PoolLiquidityRules_PreviewRemoveSettlement", + argument: { + preparation, + registrarBurnReceiverCid: registrarBurnReceiver.allocationCid, + }, + }, + }), + ); + const [poolSettlementFactory, lpSettlementFactory] = await Promise.all([ + this.registry.getSettlementFactory(pool.admin, settlementPlan.baseQuoteBatch), + this.registry.getSettlementFactory(pool.lpRegistrar, settlementPlan.lpBatch), + ]); + const poolSettlementContext = asChoiceContext(poolSettlementFactory); + const lpSettlementContext = asChoiceContext(lpSettlementFactory); - // Split-admin DvP: base/quote batch under pool.admin, LP-burn batch under - // pool.lpRegistrar — each carries its own registry choice context. - // For the self-registry both contexts are empty. - // - // The holder's allocations can reference contracts the operator cannot - // see. Registry discovery supplies the transaction-wide disclosures needed - // to validate the nested choices. return retryOnContention(() => this.ledger.submit({ actAs: [this.operatorParty, pool.lpRegistrar], - commandId: `lp-remove-settle:${requestCid ?? acceptanceCid ?? input.updateId}`, + commandId: `lp-remove-settle:${flowKey}`, disclosure: mergeDisclosures( - depositFactories.disclosure, - lpFactories.disclosure, - depositContext.disclosure, - lpContext.disclosure, + poolSettlementContext.disclosure, + lpSettlementContext.disclosure, ), command: { kind: "exercise", @@ -1085,30 +1218,18 @@ export class PoolService { contractId: liquidityRulesCid, choice: "PoolLiquidityRules_SettleRemoveLiquidity", argument: { - expectedPoolId: pool.poolId, - poolCid: input.poolCid, - poolStateCid: pool.poolStateCid, - lpPolicyCid, - requestCid: requestCid ?? null, - acceptanceCid: acceptanceCid ?? null, - holder: input.holder, - lpTokensToRedeem: input.lpTokensToRedeem, - knownTotalLpSupply: input.knownTotalLpSupply, - minBaseOut: input.minBaseOut, - minQuoteOut: input.minQuoteOut, - baseSliceCids: plan.base.sliceCids, - quoteSliceCids: plan.quote.sliceCids, - holderBaseReceiptCid, - holderQuoteReceiptCid, - holderBurnSenderCid, - baseFactoryCid: depositFactories.allocationFactoryCid, - quoteFactoryCid: depositFactories.allocationFactoryCid, - lpFactoryCid: lpFactories.allocationFactoryCid, - baseQuoteSettleCid: depositFactories.settlementFactoryCid, - lpSettleCid: lpFactories.settlementFactoryCid, + ...preparation, + // These two fields are retained for the deployed choice shape. + // Remove-liquidity does not create base/quote allocations here. + baseFactoryCid: registrarBurnReceiver.factoryCid, + quoteFactoryCid: registrarBurnReceiver.factoryCid, + lpFactoryCid: registrarBurnReceiver.factoryCid, + baseQuoteSettleCid: poolSettlementFactory.factoryCid, + lpSettleCid: lpSettlementFactory.factoryCid, requestedAt: input.requestedAt, - poolAdminExtraArgs: depositContext.extraArgs, - lpRegistrarExtraArgs: lpContext.extraArgs, + poolAdminExtraArgs: poolSettlementContext.extraArgs, + lpRegistrarExtraArgs: lpSettlementContext.extraArgs, + registrarBurnReceiverCid: registrarBurnReceiver.allocationCid, }, }, }), diff --git a/services/operator-backend/src/rfq/index.ts b/services/operator-backend/src/rfq/index.ts index 7b65d56e..f6607fbc 100644 --- a/services/operator-backend/src/rfq/index.ts +++ b/services/operator-backend/src/rfq/index.ts @@ -124,10 +124,11 @@ export class RfqService { } /** - * Create an RFQ on the trader's behalf. The Rfq template is signatory - * trader, so this submission carries the trader's authority — in - * production the trader's wallet does this, but the operator backend - * accepts the call here so the dApp can drive the live demo path. + * Trusted-custodial exception: create an RFQ with the trader's authority. + * The public HTTP route is disabled unless DEX_HOSTED_RFQ_RELAY and caller + * binding are configured, and the participant user must already have actAs + * rights for that trader. A self-custody deployment hands this command to the + * trader's wallet instead. */ async create(input: RfqCreateInput): Promise<{ rfqCid: ContractId<"Rfq"> }> { const rfqCid = await retryOnContention(() => @@ -186,7 +187,7 @@ export class RfqService { throw new RfqAuthError("caller may only accept its own RFQ"); } const quotes = await this.fetchQuotes(input.consideredQuoteCids); - const ranked = rankQuotes(rfq.side, quotes, input.now); + const ranked = rankQuotes(quotes, input.now); const accepted = quotes.find( (q) => q.contractId === input.acceptedQuoteCid, ); @@ -208,7 +209,6 @@ export class RfqService { // Rfq_Accept choice computes its own copy from the same inputs. const receipt = buildReceipt({ rfqId: rfq.rfqId, - side: rfq.side, quotes, acceptedDealer: accepted.dealer, signedBy: this.operatorParty, diff --git a/services/operator-backend/src/testnet-server.ts b/services/operator-backend/src/testnet-server.ts index 9d2a4ca9..c032924c 100644 --- a/services/operator-backend/src/testnet-server.ts +++ b/services/operator-backend/src/testnet-server.ts @@ -1,6 +1,6 @@ -// Testnet server. Same HTTP shim as dev-server.ts, but pointed at a -// real Canton participant via JsonApiLedger. Used for the smoke-test -// path against the deployed DEX on a remote testnet. +// Remote-participant runtime entrypoint. It uses the same HTTP API surface as +// dev-server.ts, but JsonApiLedger submits to a real Canton participant and a +// DEX package that the operator has deployed to a controlled testnet. // // Required env vars: // CANTON_LEDGER_URL Base URL of the JSON Ledger API. @@ -8,14 +8,31 @@ // CANTON_OPERATOR Operator party (DEX market venue). // CANTON_LP_REGISTRAR LP registrar party. // CANTON_ADMIN Asset admin party. +// CANTON_DEX_PACKAGE_ID Hash (or `#canton-dex-trading`) for template ids. +// +// Defaulted / optional: // CANTON_USER_ID JSON Ledger API user id (default: ledger-api-user). // CANTON_NETWORK Display label, e.g. canton:devnet. // CANTON_SYNCHRONIZER Synchronizer id, e.g. global-domain::1220... -// CANTON_DEX_PACKAGE_ID Hash (or `#canton-dex-trading`) for template ids. // -// Optional: -// CANTON_ALLOC_FACTORY_CID AllocationFactory contract id. -// CANTON_SETTLE_FACTORY_CID SettlementFactory contract id. +// Required in full/write mode (optional only with DEX_READ_ONLY=1): +// DEX_OPERATOR_API_TOKEN Bearer token for non-admin state-changing routes. +// OPERATOR_ADMIN_TOKEN Bearer token for /v1/admin/* writes. +// CANTON_ALLOC_FACTORY_CID Asset-admin AllocationFactory contract id. +// CANTON_SETTLE_FACTORY_CID Asset-admin SettlementFactory contract id. +// CANTON_LP_ALLOC_FACTORY_CID LP-registry AllocationFactory contract id +// when lpRegistrar != admin. +// CANTON_LP_SETTLE_FACTORY_CID LP-registry SettlementFactory contract id +// when lpRegistrar != admin. +// DEX_READ_ONLY=1 Start without API write tokens; state-changing +// routes fail closed, while reads/read-only quotes +// remain usable. +// +// Optional trusted relay (disabled by default): +// DEX_HOSTED_RFQ_RELAY=1 Allow the HTTP RFQ create/cancel/accept routes to +// submit with trader authority. Requires +// DEX_CALLER_JWT_SECRET and participant rights for +// every hosted trader. This is not self-custody. // // Why this lives next to dev-server.ts and not in place of it: the // in-memory dev server is the fast local path for UI development. The @@ -29,8 +46,12 @@ import { openDb } from "./indexer/db.js"; import { Indexer } from "./indexer/index.js"; import { IdempotentLedger } from "./indexer/idempotency.js"; import { DealersService } from "./dealers/index.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; +import { FixedRegistryClient, RegistryError } from "@canton-dex/registry-client"; +import type { + ContractId, + FactoryRefs, + Party, +} from "@canton-dex/registry-client"; import { rootLogger } from "./lib/logger.js"; const log = rootLogger.child({ component: "testnet-server" }); @@ -44,24 +65,24 @@ function required(name: string): string { return v; } -// Lightweight registry client: returns the configured factory CIDs for -// every admin. Production deployments use a real registry index. -class FixedRegistry extends RegistryClient { - constructor( - private readonly allocCid: ContractId<"AllocationFactory">, - private readonly settleCid: ContractId<"SettlementFactory">, - ) { - super({ baseUrl: "http://fixed-registry" }); - } - override async getFactories() { - return { - allocationFactoryCid: this.allocCid, - settlementFactoryCid: this.settleCid, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; +// Lightweight registry client for the two reference registrars. It is +// intentionally explicit per admin: returning one registry CID for every +// party breaks LP issuance as soon as asset governance and LP custody are +// separated. Deployments that list arbitrary third-party assets should replace +// this map with the registry HTTP discovery client. +class ConfiguredRegistry extends FixedRegistryClient { + constructor(factoriesByAdmin: ReadonlyMap) { + super((admin) => { + const factories = factoriesByAdmin.get(admin); + if (!factories) { + throw new RegistryError( + "factory-stale", + `no configured factory mapping for admin=${admin}`, + false, + ); + } + return factories; + }); } } @@ -71,18 +92,68 @@ async function main(): Promise { const operator = required("CANTON_OPERATOR"); const lpRegistrar = required("CANTON_LP_REGISTRAR"); const admin = required("CANTON_ADMIN"); + const dexPackageId = required("CANTON_DEX_PACKAGE_ID"); const userId = process.env.CANTON_USER_ID ?? "ledger-api-user"; const network = process.env.CANTON_NETWORK ?? "canton:devnet"; - const allocCid = (process.env.CANTON_ALLOC_FACTORY_CID ?? - "PENDING_ALLOC_FACTORY") as ContractId<"AllocationFactory">; - const settleCid = (process.env.CANTON_SETTLE_FACTORY_CID ?? - "PENDING_SETTLE_FACTORY") as ContractId<"SettlementFactory">; + const readOnly = process.env.DEX_READ_ONLY === "1"; + const hostedRfqEnabled = process.env.DEX_HOSTED_RFQ_RELAY === "1"; + const callerJwtSecret = process.env.DEX_CALLER_JWT_SECRET || undefined; + if (readOnly && hostedRfqEnabled) { + log.error("invalid mode: DEX_HOSTED_RFQ_RELAY cannot be enabled with DEX_READ_ONLY"); + process.exit(1); + } + if (hostedRfqEnabled && !callerJwtSecret) { + required("DEX_CALLER_JWT_SECRET"); + } + // Fail at startup instead of presenting a deceptively healthy but unusable + // full-mode server. Read-only operation must be chosen explicitly. + const operatorToken = readOnly + ? undefined + : required("DEX_OPERATOR_API_TOKEN"); + const adminToken = readOnly + ? undefined + : required("OPERATOR_ADMIN_TOKEN"); + const allocCid = (readOnly + ? process.env.CANTON_ALLOC_FACTORY_CID || "PENDING_ALLOC_FACTORY" + : required("CANTON_ALLOC_FACTORY_CID")) as ContractId<"AllocationFactory">; + const settleCid = (readOnly + ? process.env.CANTON_SETTLE_FACTORY_CID || "PENDING_SETTLE_FACTORY" + : required("CANTON_SETTLE_FACTORY_CID")) as ContractId<"SettlementFactory">; + const lpAllocCid = (lpRegistrar === admin + ? allocCid + : readOnly + ? process.env.CANTON_LP_ALLOC_FACTORY_CID || "PENDING_LP_ALLOC_FACTORY" + : required("CANTON_LP_ALLOC_FACTORY_CID")) as ContractId<"AllocationFactory">; + const lpSettleCid = (lpRegistrar === admin + ? settleCid + : readOnly + ? process.env.CANTON_LP_SETTLE_FACTORY_CID || "PENDING_LP_SETTLE_FACTORY" + : required("CANTON_LP_SETTLE_FACTORY_CID")) as ContractId<"SettlementFactory">; + + const factoriesByAdmin = new Map([ + [ + admin, + { + allocationFactoryCid: allocCid, + settlementFactoryCid: settleCid, + disclosure: [], + }, + ], + [ + lpRegistrar, + { + allocationFactoryCid: lpAllocCid, + settlementFactoryCid: lpSettleCid, + disclosure: [], + }, + ], + ]); const rawLedger = new JsonApiLedger({ baseUrl, token, applicationId: userId, - templateIdPrefix: process.env.CANTON_DEX_PACKAGE_ID, + templateIdPrefix: dexPackageId, synchronizerId: process.env.CANTON_SYNCHRONIZER, }); @@ -98,7 +169,7 @@ async function main(): Promise { const backend = new OperatorBackend({ ledger, - registry: new FixedRegistry(allocCid, settleCid), + registry: new ConfiguredRegistry(factoriesByAdmin), operatorParty: operator, }); @@ -141,27 +212,26 @@ async function main(): Promise { operator, lpRegistrar, admin, - allocationFactoryCid: allocCid, - settlementFactoryCid: settleCid, - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network, }, db, - adminToken: process.env.OPERATOR_ADMIN_TOKEN, + adminToken, // Operator token gates all non-admin writes; fail-closed on testnet // (no DEX_DEV_OPEN bypass here). - operatorToken: process.env.DEX_OPERATOR_API_TOKEN, - devOpen: process.env.DEX_DEV_OPEN === "1", - // Wallet relay OFF unless explicitly enabled, with a party allowlist. - walletRelayEnabled: process.env.DEX_DEV_WALLET_RELAY === "1", - walletRelayParties: (process.env.DEX_DEV_RELAY_PARTIES ?? "") - .split(",") - .map((s) => s.trim()) - .filter(Boolean), - // Per-caller party binding: when set, trader-subject write routes - // require an X-Caller-Token JWT whose `sub` is the caller's party. - callerJwtSecret: process.env.DEX_CALLER_JWT_SECRET || undefined, + operatorToken, + // The in-memory dev server is the only entrypoint allowed to honor + // DEX_DEV_OPEN. A stray deployment environment variable must never bypass + // the testnet/production write gate (including explicit read-only mode). + devOpen: false, + // The arbitrary-command wallet relay is confined to dev-server.ts. A + // deployment must use a real wallet/BFF boundary; testnet-server never + // honors DEX_DEV_WALLET_RELAY even if it leaks into the environment. + walletRelayEnabled: false, + walletRelayParties: [], + hostedRfqEnabled, + // Per-caller party binding: when set, party-scoped reads and trader-subject + // writes require an X-Caller-Token JWT whose `sub` is the caller's party. + callerJwtSecret, // Optional `aud` claim the caller JWT must carry (defence against a token // minted for another service being replayed here). callerJwtAudience: process.env.DEX_CALLER_JWT_AUDIENCE || undefined, @@ -177,6 +247,9 @@ async function main(): Promise { network, db: dbPath, indexerIntervalMs: Number(process.env.INDEXER_INTERVAL_MS ?? 5000), + mode: readOnly ? "read-only" : "full", + registryAdmins: Array.from(factoriesByAdmin.keys()), + hostedRfqEnabled, }); // Graceful shutdown: drain HTTP requests, stop indexer, flush DB. diff --git a/services/operator-backend/test/auth.test.ts b/services/operator-backend/test/auth.test.ts index fac8a90a..b9ffb638 100644 --- a/services/operator-backend/test/auth.test.ts +++ b/services/operator-backend/test/auth.test.ts @@ -17,24 +17,7 @@ import { isOperatorWrite, checkOperatorAuth, } from "../src/http/auth.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; - -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories() { - return { - allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, - settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } -} +import { StubRegistry } from "./stub-registry.js"; function startServer( extra: Partial, @@ -54,10 +37,6 @@ function startServer( operator: "op" as never, lpRegistrar: "lp" as never, admin: "ad" as never, - allocationFactoryCid: "#alloc:0", - settlementFactoryCid: "#settle:0", - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network: "canton:test", }, ...extra, @@ -257,4 +236,50 @@ describe("wallet relay + CORS", () => { await close(); } }); + + it("CORS preflight permits the per-caller JWT header", async () => { + const { url, close } = await startServer({ devOpen: true }); + try { + const res = await fetch(`${url}/v1/pools/swap`, { + method: "OPTIONS", + headers: { + Origin: "http://localhost:5173", + "Access-Control-Request-Method": "POST", + "Access-Control-Request-Headers": "X-Caller-Token", + }, + }); + await res.text(); + assert.equal(res.status, 204); + assert.match( + res.headers.get("access-control-allow-headers") ?? "", + /X-Caller-Token/i, + ); + } finally { + await close(); + } + }); +}); + +describe("hosted RFQ relay", () => { + it("returns 404 when a deployment disables trader-authority relay", async () => { + const { url, close } = await startServer({ + devOpen: true, + hostedRfqEnabled: false, + }); + try { + const status = await post(url, "/v1/rfq", { + trader: "trader", + rfqId: "rfq-disabled", + pair: "BTC/USDC", + side: "RFQ_Buy", + size: "1.0", + expiresAt: "2030-01-01T00:00:00Z", + whitelist: [], + createdAt: "2026-01-01T00:00:00Z", + }); + assert.equal(status, 404); + } finally { + await close(); + } + }); }); diff --git a/services/operator-backend/test/caller-auth.test.ts b/services/operator-backend/test/caller-auth.test.ts index 72e66887..55759df1 100644 --- a/services/operator-backend/test/caller-auth.test.ts +++ b/services/operator-backend/test/caller-auth.test.ts @@ -9,6 +9,7 @@ import type { IncomingMessage } from "node:http"; import { checkCallerBinding, + checkCallerRead, routeBindsCaller, verifyHs256, } from "../src/http/caller-auth.js"; @@ -182,3 +183,28 @@ describe("checkCallerBinding", () => { assert.equal(r.ok, true); }); }); + +describe("checkCallerRead", () => { + const cfg = { callerJwtSecret: SECRET }; + + it("is disabled when no caller secret is configured", () => { + assert.equal( + checkCallerRead(reqWith(), { callerJwtSecret: undefined }, BOB).ok, + true, + ); + }); + + it("requires a valid caller token when enabled", () => { + const result = checkCallerRead(reqWith(), cfg, ALICE); + assert.equal(result.ok, false); + assert.equal((result as { status: number }).status, 401); + }); + + it("allows only the caller's own party", () => { + const req = reqWith(signHs256({ sub: ALICE })); + assert.equal(checkCallerRead(req, cfg, ALICE).ok, true); + const denied = checkCallerRead(req, cfg, BOB); + assert.equal(denied.ok, false); + assert.equal((denied as { status: number }).status, 403); + }); +}); diff --git a/services/operator-backend/test/canton-e2e.test.ts b/services/operator-backend/test/canton-e2e.test.ts deleted file mode 100644 index 1053f1c2..00000000 --- a/services/operator-backend/test/canton-e2e.test.ts +++ /dev/null @@ -1,242 +0,0 @@ -// Canton-backed end-to-end test for the operator backend. -// -// This test drives the SAME flow code as the InMemoryLedger test -// (`rfq.test.ts`) but against a real Canton participant via the -// JSON Ledger API. It is gated on the `CANTON_E2E` env var so it -// doesn't run in CI by default; running it requires a Canton -// sandbox with the canton-dex DARs uploaded and the operator party -// allocated. -// -// How to run: -// -// 1. Boot a sandbox with the DEX DARs: -// $ cd trading && daml build -// $ cd .. && daml sandbox \ -// --port 6865 \ -// --json-api-port 7575 \ -// --dar trading/.daml/dist/canton-dex-trading-0.0.1.dar -// -// OR use `daml start` from a project that depends on the DAR. -// -// 2. Allocate parties + get a JWT: -// $ daml ledger allocate-parties operator alice orca jump galaxy -// $ daml-helper request-token --party operator > /tmp/operator.jwt -// -// 3. Run the test: -// $ CANTON_E2E=1 \ -// CANTON_JSON_API_URL=http://localhost:7575 \ -// CANTON_JSON_API_TOKEN=$(cat /tmp/operator.jwt) \ -// CANTON_OPERATOR_PARTY=operator \ -// npm test -// -// What it verifies: -// - The JsonApiLedger driver successfully submits an Rfq + RfqQuote -// creates and an Rfq_Accept exercise. -// - The receipt the operator backend computes off-chain matches the -// receipt the on-chain Rfq_Accept choice produces. -// - The MatchedTrade carries the policy receipt in -// SettlementInfo.meta exactly as PolicyReceipt.daml encodes it. - -import assert from "node:assert/strict"; -import { test, before } from "node:test"; - -import { - JsonApiLedger, - OperatorBackend, - POLICY_VERSION, - verifyReceipt, -} from "../src/index.ts"; -import type { ContractId, Party, Rfq, RfqQuote } from "../src/types.ts"; -import { RegistryClient } from "@canton-dex/registry-client"; - -const e2eEnabled = process.env.CANTON_E2E === "1"; - -// Skip the entire suite when not enabled. node:test supports per-test -// `skip` but we want a single skip message at suite level. -if (!e2eEnabled) { - test("Canton E2E (skipped: set CANTON_E2E=1 to enable)", { skip: true }, () => {}); -} - -if (e2eEnabled) { - const baseUrl = required("CANTON_JSON_API_URL"); - const token = required("CANTON_JSON_API_TOKEN"); - const operator = required("CANTON_OPERATOR_PARTY") as Party; - const trader = required("CANTON_TRADER_PARTY") as Party; - const dealerJump = required("CANTON_DEALER_JUMP") as Party; - const dealerOrca = required("CANTON_DEALER_ORCA") as Party; - - const ledger = new JsonApiLedger({ - baseUrl, - token, - applicationId: "canton-dex-e2e", - }); - - // The integration test only needs the registry client for the - // factories endpoint. For the RFQ flow we don't actually settle the - // resulting MatchedTrade so the factories aren't read; a stub is - // sufficient. - // Inline-defined stub (avoid forward reference to a class declared - // later in the file). - const registry = new (class extends RegistryClient { - constructor() { - super({ baseUrl }); - } - override async getFactories(): Promise<{ - allocationFactoryCid: ContractId<"AllocationFactory">; - settlementFactoryCid: ContractId<"SettlementFactory">; - disclosure: never[]; - }> { - return { - allocationFactoryCid: - "stub-not-used-in-rfq" as ContractId<"AllocationFactory">, - settlementFactoryCid: - "stub-not-used-in-rfq" as ContractId<"SettlementFactory">, - disclosure: [], - }; - } - override async getChoiceContext() { - return { context: { values: {} }, disclosure: [] }; - } - })(); - - const backend = new OperatorBackend({ - ledger, - registry, - operatorParty: operator, - }); - - test("Canton E2E: RFQ accept produces MatchedTrade with PolicyReceipt", async () => { - const now = new Date().toISOString(); - const expiresIn1h = new Date(Date.now() + 60 * 60 * 1000).toISOString(); - const expiresIn30s = new Date(Date.now() + 30 * 1000).toISOString(); - const rfqId = `rfq-e2e-${Date.now()}`; - - // 1. Trader creates the Rfq. - const rfqCid = (await ledger.submit>({ - actAs: [trader], - commandId: `seed-rfq-${rfqId}`, - command: { - kind: "create", - templateId: "CantonDex.Dex.Rfq:Rfq", - argument: { - trader, - operator, - rfqId, - pair: "BTC/USDC", - side: "RFQ_Buy", - size: "5.0", - expiresAt: expiresIn1h, - whitelist: [dealerOrca, dealerJump], - createdAt: now, - }, - }, - })) as ContractId<"Rfq">; - - // 2. Two dealers post quotes. - const quoteJump = await ledger.submit>({ - actAs: [dealerJump], - commandId: `quote-jump-${rfqId}`, - command: { - kind: "create", - templateId: "CantonDex.Dex.Rfq:RfqQuote", - argument: { - dealer: dealerJump, - trader, - operator, - rfqId, - price: "60510.00", - expiresAt: expiresIn30s, - postedAt: now, - tier: "TierTrusted", - }, - }, - }); - const quoteOrca = await ledger.submit>({ - actAs: [dealerOrca], - commandId: `quote-orca-${rfqId}`, - command: { - kind: "create", - templateId: "CantonDex.Dex.Rfq:RfqQuote", - argument: { - dealer: dealerOrca, - trader, - operator, - rfqId, - price: "60530.00", - expiresAt: expiresIn30s, - postedAt: now, - tier: "TierTrusted", - }, - }, - }); - - // 3. Operator backend drives Rfq_Accept (joint trader+operator). - const result = await backend.rfq.accept({ - rfqCid, - acceptedQuoteCid: quoteJump, - consideredQuoteCids: [quoteJump, quoteOrca], - admin: required("CANTON_BTC_ADMIN") as Party, - now, - }); - - assert.equal( - result.receipt.acceptedDealer, - dealerJump, - "Jump should be accepted as the policy-ranked quote", - ); - assert.equal(result.receipt.acceptedRank, 1); - assert.equal(result.receipt.consideredCount, 2); - assert.equal(result.receipt.policyVersion, POLICY_VERSION); - assert.equal(verifyReceipt(result.receipt), true, "receipt verifies"); - // The cid format from JSON API is implementation-defined; just - // sanity-check it exists. - assert.ok(typeof result.tradeCid === "string"); - assert.ok((result.tradeCid as string).length > 0); - }); - - test("Canton E2E: rfq.list returns visible RFQs and quotes", async () => { - const list = await backend.rfq.list(); - assert.ok(Array.isArray(list.rfqs)); - assert.ok(Array.isArray(list.quotes)); - }); - - test("Canton E2E: rfq.cancel archives an open Rfq", async () => { - const now = new Date().toISOString(); - const expiresIn1h = new Date(Date.now() + 60 * 60 * 1000).toISOString(); - const rfqId = `rfq-cancel-${Date.now()}`; - - const rfqCid = (await ledger.submit>({ - actAs: [trader], - commandId: `seed-cancel-${rfqId}`, - command: { - kind: "create", - templateId: "CantonDex.Dex.Rfq:Rfq", - argument: { - trader, - operator, - rfqId, - pair: "BTC/USDC", - side: "RFQ_Buy", - size: "1.0", - expiresAt: expiresIn1h, - whitelist: [dealerOrca], - createdAt: now, - }, - }, - })) as ContractId<"Rfq">; - - await backend.rfq.cancel({ rfqCid }); - - const after = await backend.rfq.list(); - const stillThere = after.rfqs.find( - (r: Rfq) => r.contractId === rfqCid, - ); - assert.equal(stillThere, undefined, "cancelled Rfq should be archived"); - }); -} - -function required(name: string): string { - const v = process.env[name]; - if (!v) throw new Error(`required env: ${name}`); - return v; -} diff --git a/services/operator-backend/test/decimal-money.test.ts b/services/operator-backend/test/decimal-money.test.ts index 91dfcb79..01ff8e2f 100644 --- a/services/operator-backend/test/decimal-money.test.ts +++ b/services/operator-backend/test/decimal-money.test.ts @@ -1,13 +1,11 @@ -// On-ledger amounts must go through the BigInt decimal module, not -// IEEE-754. Pins (1) the matching-engine quote-leg amount = price*quantity at -// 10dp round-half-even, and (2) rankQuotes price ordering via exact decimal -// comparison. +// On-ledger amounts must go through the BigInt decimal module, not IEEE-754. +// The RFQ cases also pin the exact non-price policy ordering used on-ledger. import { describe, it } from "node:test"; import assert from "node:assert/strict"; import * as dec from "../src/pool/decimal.js"; -import { rankQuotes, compareDecimal } from "../src/policy/index.js"; +import { rankQuotes } from "../src/policy/index.js"; import type { RfqQuote } from "../src/types.js"; describe("quote-leg amount via decimal module", () => { @@ -71,16 +69,6 @@ describe("floored decimal ops mirror the pool's payout rounding", () => { }); }); -describe("compareDecimal is exact", () => { - it("orders by decimal value, not float", () => { - assert.equal(compareDecimal("60510.00", "60530.00"), -1); - assert.equal(compareDecimal("60530.00", "60510.00"), 1); - assert.equal(compareDecimal("1.0", "1.0000000000"), 0); - // A pair where float subtraction could lose precision but decimal must not. - assert.equal(compareDecimal("0.1000000001", "0.1000000002"), -1); - }); -}); - function mkQuote(o: { dealer: string; price?: string; @@ -117,7 +105,7 @@ describe("rankQuotes reproduces the on-ledger policyCmp (v2.0)", () => { mkQuote({ dealer: "mid", expiresAt: "2026-01-01T05:00:00Z" }), ]; assert.deepEqual( - rankQuotes("RFQ_Buy", quotes, now).map((q) => q.dealer), + rankQuotes(quotes, now).map((q) => q.dealer), ["latest", "mid", "soon"], ); }); @@ -129,13 +117,11 @@ describe("rankQuotes reproduces the on-ledger policyCmp (v2.0)", () => { ]; // Same expiry and postedAt, so the dealer tie-break decides -- price does // not enter the comparison at all, and the side does not change it. - for (const side of ["RFQ_Buy", "RFQ_Sell"] as const) { - assert.deepEqual( - rankQuotes(side, quotes, now).map((q) => q.dealer), - ["cheap", "dear"], - `${side}: ordered by dealer tie-break, not price`, - ); - } + assert.deepEqual( + rankQuotes(quotes, now).map((q) => q.dealer), + ["cheap", "dear"], + "ordered by dealer tie-break, not price", + ); }); it("trusted tier ranks ahead of whitelist regardless of expiry", () => { @@ -151,7 +137,7 @@ describe("rankQuotes reproduces the on-ledger policyCmp (v2.0)", () => { expiresAt: "2026-01-01T02:00:00Z", }), ]; - assert.equal(rankQuotes("RFQ_Buy", quotes, now)[0]?.dealer, "sooner-trusted"); + assert.equal(rankQuotes(quotes, now)[0]?.dealer, "sooner-trusted"); }); it("breaks an expiry tie by earlier postedAt, then by dealer", () => { @@ -161,7 +147,7 @@ describe("rankQuotes reproduces the on-ledger policyCmp (v2.0)", () => { mkQuote({ dealer: "c-late", postedAt: "2026-01-01T00:00:05Z" }), ]; assert.deepEqual( - rankQuotes("RFQ_Buy", quotes, now).map((q) => q.dealer), + rankQuotes(quotes, now).map((q) => q.dealer), ["a-early", "b-late", "c-late"], ); }); @@ -172,7 +158,7 @@ describe("rankQuotes reproduces the on-ledger policyCmp (v2.0)", () => { mkQuote({ dealer: "lapsed", expiresAt: "2025-12-31T23:00:00Z" }), ]; assert.deepEqual( - rankQuotes("RFQ_Buy", quotes, now).map((q) => q.dealer), + rankQuotes(quotes, now).map((q) => q.dealer), ["live"], ); }); diff --git a/services/operator-backend/test/deployment-wiring.test.ts b/services/operator-backend/test/deployment-wiring.test.ts new file mode 100644 index 00000000..d72d94bd --- /dev/null +++ b/services/operator-backend/test/deployment-wiring.test.ts @@ -0,0 +1,42 @@ +// Static deployment guards for defects that can survive TypeScript and unit +// tests: wrong working-directory defaults, missing template qualification, +// public container ports, skipped native install scripts, and root runtimes. + +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; + +const ROOT = join(import.meta.dirname, "..", "..", ".."); +const read = (path: string) => readFileSync(join(ROOT, path), "utf8"); + +describe("deployment wiring", () => { + it("requires a DEX package prefix before registry bootstrap", () => { + const deploy = read("scripts/deploy-testnet.sh"); + const bootstrap = read("scripts/bootstrap-registry.ts"); + assert.match(deploy, /CANTON_DEX_PACKAGE_ID; do/); + assert.match(bootstrap, /required\("CANTON_DEX_PACKAGE_ID"\)/); + assert.match(bootstrap, /templateIdPrefix:\s*dexPackageId/); + }); + + it("anchors the default bootstrap config beside the script", () => { + const bootstrap = read("scripts/bootstrap-registry.ts"); + assert.match(bootstrap, /fileURLToPath\(import\.meta\.url\)/); + assert.match(bootstrap, /resolve\(scriptDir,\s*"bootstrap-registry\.json"\)/); + }); + + it("keeps the Compose backend private behind nginx", () => { + const compose = read("docker-compose.yml"); + const backend = compose.split(/^ frontend:/m)[0] ?? compose; + assert.match(backend, /^ expose:/m); + assert.doesNotMatch(backend, /^ ports:/m); + assert.match(backend, /CANTON_LP_ALLOC_FACTORY_CID/); + assert.match(backend, /CANTON_LP_SETTLE_FACTORY_CID/); + }); + + it("installs the native SQLite binding and runs the backend as non-root", () => { + const dockerfile = read("Dockerfile.backend"); + assert.match(dockerfile, /WORKDIR \/app\/services\/operator-backend\s+RUN npm ci\s/m); + assert.match(dockerfile, /^USER node$/m); + }); +}); diff --git a/services/operator-backend/test/docs-harness.ts b/services/operator-backend/test/docs-harness.ts index 1297ce12..afd6ca17 100644 --- a/services/operator-backend/test/docs-harness.ts +++ b/services/operator-backend/test/docs-harness.ts @@ -34,9 +34,12 @@ function walk(dir: string): string[] { return out; } -/** Every markdown file the guards read: docs/** plus the top-level README. */ +/** Canonical docs plus every top-level project Markdown file. */ export function docFiles(): string[] { - return [...walk(join(ROOT, "docs")), join(ROOT, "README.md")]; + const topLevel = readdirSync(ROOT, { withFileTypes: true }) + .filter((entry) => entry.isFile() && entry.name.endsWith(".md")) + .map((entry) => join(ROOT, entry.name)); + return [...walk(join(ROOT, "docs")), ...topLevel]; } /** Drop bold/italic markers: `does **not** define` must match `does not define`. */ diff --git a/services/operator-backend/test/docs-hosted-scope.test.ts b/services/operator-backend/test/docs-hosted-scope.test.ts new file mode 100644 index 00000000..edd9569c --- /dev/null +++ b/services/operator-backend/test/docs-hosted-scope.test.ts @@ -0,0 +1,77 @@ +// Keep historical deployment feedback separate from the API this repository +// actually implements. A past external report referenced a public hostname, +// faucet, and /v1/testnet wrapper that are not present in this tree. + +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join, relative } from "node:path"; + +import { ROOT, docFiles } from "./docs-harness.ts"; + +describe("hosted deployment scope", () => { + it("does not advertise the retired external hostname", () => { + const hits = docFiles().filter((file) => + /testnet-dex\.bitdynamics\.cc/i.test(readFileSync(file, "utf8")), + ); + assert.deepEqual( + hits.map((file) => relative(ROOT, file)), + [], + "The old hosted endpoint is not provisioned by this repository. " + + "Keep historical reports as provenance, not current setup instructions.", + ); + }); + + it("does not present this repository as a current public deployment", () => { + const security = readFileSync(join(ROOT, "SECURITY.md"), "utf8"); + assert.doesNotMatch( + security, + /package version on the public testnet\s+is the deployed surface/i, + "SECURITY.md must describe source support without inventing a hosted service.", + ); + assert.match( + security, + /does not provision or promise a public testnet deployment/i, + ); + }); + + it("has no hidden /v1/testnet route implementation", () => { + const server = readFileSync( + join(ROOT, "services/operator-backend/src/http/index.ts"), + "utf8", + ); + assert.doesNotMatch( + server, + /["'`]\/v1\/testnet(?:\/|["'`])/, + "A /v1/testnet route was added. Document and secure it explicitly, or " + + "keep deployment wrappers outside the reference API.", + ); + }); + + it("states the current repository boundary in the canonical docs", () => { + const api = readFileSync(join(ROOT, "docs/reference/http-api.md"), "utf8"); + const nonGoals = readFileSync(join(ROOT, "docs/concepts/non-goals.md"), "utf8"); + const feedback = readFileSync( + join(ROOT, "docs/reference/ecosystem-feedback.md"), + "utf8", + ); + + assert.match(api, /has no `\/v1\/testnet\/\*` namespace, party faucet/i); + assert.match(nonGoals, /does not create parties, mint faucet assets/i); + assert.match(feedback, /does \*\*not\*\* provision a public hostname/i); + }); + + it("keeps npm package metadata on this reference repository", () => { + const expected = + "https://github.com/srikanth-bitdynamics/Canton-Dex-Reference-Implementation.git"; + for (const packagePath of [ + "app/web/package.json", + "services/operator-backend/package.json", + ]) { + const manifest = JSON.parse(readFileSync(join(ROOT, packagePath), "utf8")) as { + repository?: { url?: string }; + }; + assert.equal(manifest.repository?.url, expected, packagePath); + } + }); +}); diff --git a/services/operator-backend/test/docs-references.test.ts b/services/operator-backend/test/docs-references.test.ts index fb8f5021..7dbc66d5 100644 --- a/services/operator-backend/test/docs-references.test.ts +++ b/services/operator-backend/test/docs-references.test.ts @@ -1,7 +1,7 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { existsSync, readFileSync, readdirSync } from "node:fs"; -import { dirname, join, relative } from "node:path"; +import { basename, dirname, join, relative } from "node:path"; import { ROOT, docFiles, sentences } from "./docs-harness.ts"; @@ -101,6 +101,101 @@ describe("documentation references", () => { assert.deepEqual(missing, []); }); + it("every line-linked Daml test points at its declaration", () => { + const stale: string[] = []; + for (const file of docFiles()) { + const source = readFileSync(file, "utf8"); + for (const match of source.matchAll( + /\[`?(test[A-Z]\w*)`?\]\(([^)#]+\.daml)#L(\d+)\)/g, + )) { + const [, testName, rawPath, rawLine] = match; + const target = join(dirname(file), decodeURIComponent(rawPath!)); + if (!existsSync(target)) continue; + const lineNumber = Number(rawLine); + const lines = readFileSync(target, "utf8").split("\n"); + const line = lines[lineNumber - 1] ?? ""; + const nextLine = lines[lineNumber] ?? ""; + const declaration = new RegExp(`^${testName}\\s*:\\s*Script\\b`); + const pointsToDeclaration = declaration.test(line); + const pointsToInvariant = /^-- \| Proves\b/.test(line) && declaration.test(nextLine); + if (!pointsToDeclaration && !pointsToInvariant) { + stale.push( + `${relative(ROOT, file)} -> ${rawPath}#L${lineNumber} ` + + `(expected ${testName} declaration, found ${JSON.stringify(line.trim())})`, + ); + } + } + } + assert.deepEqual(stale, []); + }); + + it("every line-linked Daml source symbol points at its declaration", () => { + const stale: string[] = []; + for (const file of docFiles()) { + const source = readFileSync(file, "utf8"); + for (const match of source.matchAll( + /\[`([^`]+)`\]\(([^)#]+\.daml)#L(\d+)\)/g, + )) { + const [, symbol, rawPath, rawLine] = match; + const target = join(dirname(file), decodeURIComponent(rawPath!)); + if (!existsSync(target) || !relative(ROOT, target).startsWith("trading/")) continue; + const lineNumber = Number(rawLine); + const line = readFileSync(target, "utf8").split("\n")[lineNumber - 1] ?? ""; + const escaped = symbol!.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const declaration = new RegExp( + `(?:template\\s+${escaped}\\b|(?:nonconsuming\\s+)?choice\\s+${escaped}\\b|` + + `${escaped}\\s*:\\s|interface instance\\s+(?:\\w+\\.)?${escaped}\\b)`, + ); + if (!declaration.test(line)) { + stale.push( + `${relative(ROOT, file)} -> ${rawPath}#L${lineNumber} ` + + `(expected ${symbol} declaration, found ${JSON.stringify(line.trim())})`, + ); + } + } + } + assert.deepEqual(stale, []); + }); + + it("the testing matrix accounts for every Daml Script declaration", () => { + const testFiles = filesBelow( + join(ROOT, "trading-tests", "CantonDex", "Tests"), + ".daml", + ); + const actualByFile = new Map(); + for (const file of testFiles) { + const declarations = [ + ...readFileSync(file, "utf8").matchAll(/^\s*test[A-Z]\w*\s*:\s*Script\b/gm), + ].length; + if (declarations > 0) { + actualByFile.set(basename(file), declarations); + } + } + + const matrix = readFileSync(join(ROOT, "docs/reference/testing.md"), "utf8"); + const documentedByFile = new Map(); + for (const match of matrix.matchAll( + /\[`([^`/]+Tests\.daml)`\]\([^)]*\)\s*\|\s*(\d+)\s*\|/g, + )) { + documentedByFile.set(match[1]!, Number(match[2])); + } + + assert.deepEqual( + Object.fromEntries([...documentedByFile].sort()), + Object.fromEntries([...actualByFile].sort()), + ); + + const total = [...actualByFile.values()].reduce((sum, count) => sum + count, 0); + for (const relativePath of ["README.md", "docs/getting-started.md"]) { + const source = readFileSync(join(ROOT, relativePath), "utf8"); + assert.match( + source, + new RegExp(`\\b${total}\\s+(?:Daml Script )?test`), + `${relativePath} does not advertise the actual ${total}-script total`, + ); + } + }); + it("every documented Daml choice identifier is still declared", () => { const damlFiles = [ ...filesBelow(join(ROOT, "trading"), ".daml"), diff --git a/services/operator-backend/test/indexer-order-book-fill.test.ts b/services/operator-backend/test/indexer-order-book-fill.test.ts index baf51990..3e9ac1b4 100644 --- a/services/operator-backend/test/indexer-order-book-fill.test.ts +++ b/services/operator-backend/test/indexer-order-book-fill.test.ts @@ -17,29 +17,12 @@ import { Indexer } from "../src/indexer/index.js"; import { OrderService } from "../src/order/index.js"; import { MatchingLedger } from "./matching-ledger.js"; import type { Order } from "../src/types.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; +import { StubRegistry } from "./stub-registry.js"; const OPERATOR = "operator::1220ab"; const BUYER = "alice::1220ab"; const SELLER = "bob::1220ab"; -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories() { - return { - allocationFactoryCid: "#alloc-fac:0" as ContractId<"AllocationFactory">, - settlementFactoryCid: "#settle-fac:0" as ContractId<"SettlementFactory">, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } -} - function mkOrder( contractId: string, trader: string, diff --git a/services/operator-backend/test/indexer-projection-exactness.test.ts b/services/operator-backend/test/indexer-projection-exactness.test.ts index 1ed5afb1..ca95c54a 100644 --- a/services/operator-backend/test/indexer-projection-exactness.test.ts +++ b/services/operator-backend/test/indexer-projection-exactness.test.ts @@ -15,24 +15,7 @@ import { openDb, type Db } from "../src/indexer/db.js"; import { InMemoryLedger } from "../src/ledger/in-memory.js"; import { OperatorBackend } from "../src/index.js"; import { startHttpServer } from "../src/http/index.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; - -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories() { - return { - allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, - settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } -} +import { StubRegistry } from "./stub-registry.js"; // Ten-decimal values whose trailing zeros a float would drop, and one whose // last digit float subtraction would move. @@ -68,10 +51,6 @@ before(async () => { operator: "op" as never, lpRegistrar: "lp" as never, admin: "ad" as never, - allocationFactoryCid: "#alloc:0", - settlementFactoryCid: "#settle:0", - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network: "canton:test", }, devOpen: true, diff --git a/services/operator-backend/test/instruments-route.test.ts b/services/operator-backend/test/instruments-route.test.ts index af29e546..5dab6149 100644 --- a/services/operator-backend/test/instruments-route.test.ts +++ b/services/operator-backend/test/instruments-route.test.ts @@ -8,24 +8,7 @@ import { InMemoryLedger } from "../src/ledger/in-memory.js"; import type { SubscriptionFilter } from "../src/ledger/index.js"; import { OperatorBackend } from "../src/index.js"; import { startHttpServer } from "../src/http/index.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; - -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories() { - return { - allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, - settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } -} +import { StubRegistry } from "./stub-registry.js"; // Returns what a participant returns: Int64 as a string, Optional Text as null. class ConfigLedger extends InMemoryLedger { @@ -56,10 +39,6 @@ before(async () => { operator: "op" as never, lpRegistrar: "lp" as never, admin: "ad" as never, - allocationFactoryCid: "#alloc:0", - settlementFactoryCid: "#settle:0", - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network: "canton:test", }, devOpen: true, diff --git a/services/operator-backend/test/live/canton-live-rfq.test.ts b/services/operator-backend/test/live/canton-live-rfq.test.ts new file mode 100644 index 00000000..935360db --- /dev/null +++ b/services/operator-backend/test/live/canton-live-rfq.test.ts @@ -0,0 +1,315 @@ +// Canton-backed RFQ service integration test. +// +// This drives the same RfqService code as `rfq.test.ts`, but through +// JsonApiLedger against an already-running Canton participant. It does not +// start the HTTP server, dApp, or a wallet, and it does not fund or settle the +// MatchedTrade. CANTON_LIVE_RFQ gates all live submissions. +// +// Prerequisites: +// - the current canton-dex trading DAR and dependencies are uploaded; +// - the five configured parties exist; +// - the JWT has actAs rights for operator, trader, and both dealers. +// CANTON_BTC_ADMIN is data on the resulting trade, not an authorizer here. +// +// Run from services/operator-backend: +// $ CANTON_LIVE_RFQ=1 \ +// CANTON_JSON_API_URL=... CANTON_JSON_API_TOKEN=... \ +// CANTON_OPERATOR_PARTY=... CANTON_TRADER_PARTY=... \ +// CANTON_DEALER_JUMP=... CANTON_DEALER_ORCA=... \ +// CANTON_BTC_ADMIN=... npm run test:live:rfq +// +// What it verifies: +// - real Rfq/RfqQuote creates and Rfq_Accept/cancel exercises; +// - exact CIDs returned by RfqService.list; +// - the choice result's receipt verifies and equals the PolicyReceipt stored +// on the queried MatchedTrade. +// +// STATE WARNING: the accept case leaves one MatchedTrade. Use a throwaway +// LocalNet or dedicated test parties. The RFQ id printed by node:test identifies +// the run if manual cleanup is needed. + +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import { + JsonApiLedger, + OperatorBackend, + POLICY_VERSION, + verifyReceipt, +} from "../../src/index.ts"; +import type { + ContractId, + Party, + PolicyReceipt, + Rfq, + RfqQuote, +} from "../../src/types.ts"; +import { FixedRegistryClient } from "@canton-dex/registry-client"; + +const liveEnabled = process.env.CANTON_LIVE_RFQ === "1"; + +// Skip the entire suite when not enabled. node:test supports per-test +// `skip` but we want a single skip message at suite level. +if (!liveEnabled) { + test( + "Canton live RFQ (skipped: set CANTON_LIVE_RFQ=1 to enable)", + { skip: true }, + () => {}, + ); +} + +if (liveEnabled) { + const baseUrl = required("CANTON_JSON_API_URL"); + const token = required("CANTON_JSON_API_TOKEN"); + const operator = required("CANTON_OPERATOR_PARTY") as Party; + const trader = required("CANTON_TRADER_PARTY") as Party; + const dealerJump = required("CANTON_DEALER_JUMP") as Party; + const dealerOrca = required("CANTON_DEALER_ORCA") as Party; + const btcAdmin = required("CANTON_BTC_ADMIN") as Party; + const runId = `${Date.now()}-${process.pid}`; + console.info(`[canton-rfq-live] run id: ${runId}`); + + interface MatchedTradeContract { + contractId: ContractId<"MatchedTrade">; + venue: Party; + admin: Party; + policyReceipt: PolicyReceipt | null; + } + + const ledger = new JsonApiLedger({ + baseUrl, + token, + applicationId: "canton-dex-live-rfq", + }); + + // The integration test only needs the registry client for the + // factories endpoint. For the RFQ flow we don't actually settle the + // resulting MatchedTrade so the factories aren't read; a stub is + // sufficient. + // Inline-defined stub (avoid forward reference to a class declared + // later in the file). + const registry = new FixedRegistryClient(() => ({ + allocationFactoryCid: + "stub-not-used-in-rfq" as ContractId<"AllocationFactory">, + settlementFactoryCid: + "stub-not-used-in-rfq" as ContractId<"SettlementFactory">, + disclosure: [], + })); + + const backend = new OperatorBackend({ + ledger, + registry, + operatorParty: operator, + }); + + test("Canton live RFQ: accept produces MatchedTrade with PolicyReceipt", async () => { + const now = new Date().toISOString(); + const expiresIn1h = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + const expiresIn15m = new Date(Date.now() + 15 * 60 * 1000).toISOString(); + const rfqId = `rfq-live-${runId}`; + + // 1. Trader creates the Rfq. + const rfqCid = (await ledger.submit>({ + actAs: [trader], + commandId: `seed-rfq-${rfqId}`, + command: { + kind: "create", + templateId: "CantonDex.Dex.Rfq:Rfq", + argument: { + trader, + operator, + rfqId, + pair: "BTC/USDC", + side: "RFQ_Buy", + size: "5.0", + expiresAt: expiresIn1h, + whitelist: [dealerOrca, dealerJump], + createdAt: now, + }, + }, + })) as ContractId<"Rfq">; + + // 2. Two dealers post quotes. + const quoteJump = await ledger.submit>({ + actAs: [dealerJump], + commandId: `quote-jump-${rfqId}`, + command: { + kind: "create", + templateId: "CantonDex.Dex.Rfq:RfqQuote", + argument: { + dealer: dealerJump, + trader, + operator, + rfqId, + price: "60510.00", + expiresAt: expiresIn15m, + postedAt: now, + tier: "TierTrusted", + }, + }, + }); + const quoteOrca = await ledger.submit>({ + actAs: [dealerOrca], + commandId: `quote-orca-${rfqId}`, + command: { + kind: "create", + templateId: "CantonDex.Dex.Rfq:RfqQuote", + argument: { + dealer: dealerOrca, + trader, + operator, + rfqId, + price: "60530.00", + expiresAt: expiresIn15m, + postedAt: now, + tier: "TierTrusted", + }, + }, + }); + + // 3. Operator backend drives Rfq_Accept (joint trader+operator). + const result = await backend.rfq.accept({ + rfqCid, + acceptedQuoteCid: quoteJump, + consideredQuoteCids: [quoteJump, quoteOrca], + admin: btcAdmin, + now, + }); + + assert.equal( + result.receipt.acceptedDealer, + dealerJump, + "Jump should be accepted as the policy-ranked quote", + ); + assert.equal(result.receipt.acceptedRank, 1); + assert.equal(result.receipt.consideredCount, 2); + assert.equal(result.receipt.policyVersion, POLICY_VERSION); + assert.equal(verifyReceipt(result.receipt), true, "receipt verifies"); + assert.ok(typeof result.tradeCid === "string"); + assert.ok((result.tradeCid as string).length > 0); + + const trades = await ledger.query({ + templateId: "CantonDex.Dex.MatchedTrade:MatchedTrade", + observingParty: operator, + }); + const trade = trades.find((candidate) => candidate.contractId === result.tradeCid); + assert.ok(trade, "Rfq_Accept result CID must identify a visible MatchedTrade"); + assert.equal(trade.venue, operator); + assert.equal(trade.admin, btcAdmin); + assert.deepEqual( + trade.policyReceipt, + result.receipt, + "queried MatchedTrade must store the choice result's PolicyReceipt", + ); + }); + + test("Canton live RFQ: list returns the exact visible RFQ and quote CIDs", async () => { + const now = new Date().toISOString(); + const expiresAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + const rfqId = `rfq-list-${runId}`; + const { rfqCid } = await backend.rfq.create({ + trader, + rfqId, + pair: "BTC/USDC", + side: "RFQ_Buy", + size: "2.0", + expiresAt, + whitelist: [dealerOrca], + createdAt: now, + }); + const quoteCid = await ledger.submit>({ + actAs: [dealerOrca], + commandId: `quote-list-${runId}`, + command: { + kind: "create", + templateId: "CantonDex.Dex.Rfq:RfqQuote", + argument: { + dealer: dealerOrca, + trader, + operator, + rfqId, + price: "60520.00", + expiresAt, + postedAt: now, + tier: "TierTrusted", + }, + }, + }); + + try { + const list = await backend.rfq.list(); + assert.equal( + list.rfqs.find((rfq) => rfq.contractId === rfqCid)?.rfqId, + rfqId, + "list must include the RFQ created by this case", + ); + assert.equal( + list.quotes.find((quote) => quote.contractId === quoteCid)?.rfqId, + rfqId, + "list must include the quote created by this case", + ); + } finally { + await Promise.all([ + backend.rfq.cancel({ rfqCid }), + ledger.submit({ + actAs: [dealerOrca], + commandId: `withdraw-list-quote-${runId}`, + command: { + kind: "exercise", + templateId: "CantonDex.Dex.Rfq:RfqQuote", + contractId: quoteCid, + choice: "RfqQuote_Withdraw", + argument: {}, + }, + }), + ]); + } + }); + + test("Canton live RFQ: cancel archives an open Rfq", async () => { + const now = new Date().toISOString(); + const expiresIn1h = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + const rfqId = `rfq-cancel-${runId}`; + + const rfqCid = (await ledger.submit>({ + actAs: [trader], + commandId: `seed-cancel-${rfqId}`, + command: { + kind: "create", + templateId: "CantonDex.Dex.Rfq:Rfq", + argument: { + trader, + operator, + rfqId, + pair: "BTC/USDC", + side: "RFQ_Buy", + size: "1.0", + expiresAt: expiresIn1h, + whitelist: [dealerOrca], + createdAt: now, + }, + }, + })) as ContractId<"Rfq">; + + const beforeCancel = await backend.rfq.list(); + assert.equal( + beforeCancel.rfqs.find((rfq) => rfq.contractId === rfqCid)?.rfqId, + rfqId, + "created RFQ must be visible before cancellation", + ); + + await backend.rfq.cancel({ rfqCid }); + + const after = await backend.rfq.list(); + const stillThere = after.rfqs.find( + (r: Rfq) => r.contractId === rfqCid, + ); + assert.equal(stillThere, undefined, "cancelled Rfq should be archived"); + }); +} + +function required(name: string): string { + const v = process.env[name]; + if (!v) throw new Error(`required env: ${name}`); + return v; +} diff --git a/services/operator-backend/test/match-leg-shape.test.ts b/services/operator-backend/test/match-leg-shape.test.ts index c1c520e6..2cbd8725 100644 --- a/services/operator-backend/test/match-leg-shape.test.ts +++ b/services/operator-backend/test/match-leg-shape.test.ts @@ -12,21 +12,32 @@ import { join } from "node:path"; import { OrderService } from "../src/order/index.js"; import { InMemoryLedger } from "../src/ledger/in-memory.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; +import { FixedRegistryClient } from "@canton-dex/registry-client"; +import type { + ChoiceArguments, + ContractId, + FactoryChoiceContextRef, + Party, +} from "@canton-dex/registry-client"; import type { Order } from "../src/types.js"; -class StubRegistry extends RegistryClient { - constructor() { super({ baseUrl: "http://stub" }); } - override async getFactories() { - return { +class StubRegistry extends FixedRegistryClient { + settlementArguments: ChoiceArguments | null = null; + + constructor() { + super(() => ({ allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, disclosure: [] as never[], - }; + })); } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; + + override async getSettlementFactory( + admin: Party, + choiceArguments: ChoiceArguments, + ): Promise { + this.settlementArguments = choiceArguments; + return super.getSettlementFactory(admin, choiceArguments); } } @@ -36,9 +47,22 @@ class CapturingLedger extends InMemoryLedger { override async submit(req: any): Promise { this.captured.push(req.command); if (req.command.kind === "createAndExercise") { + if (req.command.choice === "OrderMatchExecution_PreviewSettlement") { + return { + settlement: { + executors: ["op"], id: "preview-match", cid: null, meta: { values: {} }, + }, + transferLegs: [], + allocations: [], + actors: ["op"], + extraArgs: { context: { values: {} }, meta: { values: {} } }, + } as R; + } return { buyerNextAllocationCid: null, sellerNextAllocationCid: null, + buyRemainderCid: null, + sellRemainderCid: null, } as R; } return null as R; @@ -62,14 +86,28 @@ class CapturingLedger extends InMemoryLedger { describe("match execution argument", () => { it("carries an Account-shaped pair the settle factory can settle", async () => { const ledger = new CapturingLedger(); - const svc = new OrderService(ledger, new StubRegistry(), "op" as never); + const registry = new StubRegistry(); + const svc = new OrderService(ledger, registry, "op" as never); await svc.runMatching({ baseInstrumentId: "dBTC", quoteInstrumentId: "dUSD", admin: "ad" as never, }); - const exec = ledger.captured.find((c) => c?.kind === "createAndExercise"); + const previewIndex = ledger.captured.findIndex( + (c) => c?.choice === "OrderMatchExecution_PreviewSettlement", + ); + const executeIndex = ledger.captured.findIndex( + (c) => c?.choice === "OrderMatchExecution_Execute", + ); + const exec = ledger.captured[executeIndex]; assert.ok(exec, "no OrderMatchExecution was submitted"); + assert.ok(previewIndex >= 0, "the exact settlement argument was not previewed"); + assert.ok(previewIndex < executeIndex, "registry discovery must happen before execution"); assert.equal(exec.choice, "OrderMatchExecution_Execute"); assert.equal(exec.choiceArgument.factoryCid, "#settle:0"); + assert.equal( + (registry.settlementArguments?.settlement as { id?: string })?.id, + "preview-match", + "the registry receives the exact Daml preview result", + ); const match = exec.argument.match; for (const side of ["buyerAccount", "sellerAccount"] as const) { diff --git a/services/operator-backend/test/matched-trade.test.ts b/services/operator-backend/test/matched-trade.test.ts index 80c0d097..a86d8cac 100644 --- a/services/operator-backend/test/matched-trade.test.ts +++ b/services/operator-backend/test/matched-trade.test.ts @@ -3,10 +3,10 @@ import assert from "node:assert/strict"; import { RegistryClient } from "@canton-dex/registry-client"; import type { - ChoiceContextRef, + ChoiceArguments, ContractId, DisclosedContract, - FactoryRefs, + FactoryChoiceContextRef, Party, } from "@canton-dex/registry-client"; @@ -23,9 +23,43 @@ import type { class CapturingLedger implements LedgerSubmitter { lastSubmit: SubmitRequest | null = null; + readonly submissions: SubmitRequest[] = []; async submit(req: SubmitRequest): Promise { this.lastSubmit = req; + this.submissions.push(req); + const command = req.command as { + choice?: string; + argument?: { + plansByAdmin?: Array<[ + Party, + { + transferLegs: V2TransferLeg[]; + allocations: unknown[]; + }, + ]>; + }; + }; + if (command.choice === "MatchedTrade_PreviewSettlement") { + return (command.argument?.plansByAdmin ?? []).map(([admin, plan]) => [ + admin, + { + settlement: { + executors: ["operator"], + id: `matched-trade:${admin}`, + cid: null, + meta: { values: {} }, + }, + transferLegs: plan.transferLegs, + allocations: plan.allocations, + actors: ["operator"], + extraArgs: { + context: { values: {} }, + meta: { values: {} }, + }, + }, + ]) as R; + } return "#result:0" as R; } @@ -47,22 +81,39 @@ function disclosed(tag: string): DisclosedContract { } class ContextRegistry extends RegistryClient { + readonly settlementLookups: Array<{ + admin: Party; + choiceArguments: ChoiceArguments; + }> = []; + readonly cancelLookups: Array<{ admin: Party; allocationId: string }> = []; + constructor() { super({ baseUrl: "http://stub" }); } - override async getFactories(admin: Party): Promise { + override async getSettlementFactory( + admin: Party, + choiceArguments: ChoiceArguments, + ): Promise { + this.settlementLookups.push({ admin, choiceArguments }); return { - allocationFactoryCid: `#alloc:${admin}` as ContractId<"AllocationFactory">, - settlementFactoryCid: `#settle:${admin}` as ContractId<"SettlementFactory">, - disclosure: [disclosed(`factory-${admin}`)], + factoryCid: `#settle:${admin}` as ContractId<"TokenStandardFactory">, + context: { values: { [`ctx.${admin}`]: true } }, + disclosure: [ + disclosed(`factory-${admin}`), + disclosed(`context-${admin}`), + ], }; } - override async getChoiceContext(admin: Party): Promise { + override async getAllocationCancelContext( + admin: Party, + allocationId: string, + ) { + this.cancelLookups.push({ admin, allocationId }); return { - context: { values: { [`ctx.${admin}`]: true } }, - disclosure: [disclosed(`context-${admin}`)], + context: { values: { [`ctx.${admin}.${allocationId}`]: true } }, + disclosure: [disclosed(`cancel-${admin}-${allocationId}`)], }; } } @@ -81,9 +132,10 @@ function leg(id: string, instrumentId: string): V2TransferLeg { describe("MatchedTradeService", () => { it("settle threads per-admin choice context and legs into each SettlementBatchV2", async () => { const ledger = new CapturingLedger(); + const registry = new ContextRegistry(); const svc = new MatchedTradeService( ledger, - new ContextRegistry(), + registry, "operator" as Party, ); @@ -177,6 +229,22 @@ describe("MatchedTradeService", () => { assert.deepEqual(adminABatch!.extraArgs.context.values, { "ctx.adminA": true }); assert.deepEqual(adminBBatch!.extraArgs.context.values, { "ctx.adminB": true }); + + const preview = ledger.submissions.find( + (s) => (s.command as { choice?: string }).choice === "MatchedTrade_PreviewSettlement", + ); + assert.ok(preview, "settlement runs the on-ledger preview first"); + assert.deepEqual( + registry.settlementLookups.map(({ admin }) => admin), + ["adminA", "adminB"], + ); + for (const { admin, choiceArguments } of registry.settlementLookups) { + assert.equal( + (choiceArguments.settlement as { id: string }).id, + `matched-trade:${admin}`, + "the exact preview result is sent to that admin's settlement endpoint", + ); + } const disclosureBlobs = submit.disclosure!.map((d) => d.createdEventBlob); assert.deepEqual(new Set(disclosureBlobs), new Set([ "factory-adminA", @@ -189,9 +257,10 @@ describe("MatchedTradeService", () => { it("cancel threads the matching admin context for each allocation group", async () => { const ledger = new CapturingLedger(); + const registry = new ContextRegistry(); const svc = new MatchedTradeService( ledger, - new ContextRegistry(), + registry, "operator" as Party, ); @@ -216,13 +285,22 @@ describe("MatchedTradeService", () => { }; assert.equal(cmd.choice, "MatchedTrade_Cancel"); assert.deepEqual(cmd.argument.allocationsToCancel, [ - ["#a:0", { context: { values: { "ctx.adminA": true } }, meta: { values: {} } }], - ["#a:1", { context: { values: { "ctx.adminA": true } }, meta: { values: {} } }], - ["#b:0", { context: { values: { "ctx.adminB": true } }, meta: { values: {} } }], + ["#a:0", { context: { values: { "ctx.adminA.#a:0": true } }, meta: { values: {} } }], + ["#a:1", { context: { values: { "ctx.adminA.#a:1": true } }, meta: { values: {} } }], + ["#b:0", { context: { values: { "ctx.adminB.#b:0": true } }, meta: { values: {} } }], + ]); + assert.deepEqual(registry.cancelLookups, [ + { admin: "adminA", allocationId: "#a:0" }, + { admin: "adminA", allocationId: "#a:1" }, + { admin: "adminB", allocationId: "#b:0" }, ]); assert.deepEqual( new Set(submit.disclosure?.map((d) => d.createdEventBlob)), - new Set(["context-adminA", "context-adminB"]), + new Set([ + "cancel-adminA-#a:0", + "cancel-adminA-#a:1", + "cancel-adminB-#b:0", + ]), ); }); }); diff --git a/services/operator-backend/test/matching-ledger.ts b/services/operator-backend/test/matching-ledger.ts index 6c63ec2f..2d61c85b 100644 --- a/services/operator-backend/test/matching-ledger.ts +++ b/services/operator-backend/test/matching-ledger.ts @@ -139,7 +139,8 @@ export class MatchingLedger implements LedgerSubmitter { get executes(): CreateAndExerciseCommand[] { return this.commands.filter( - (c): c is CreateAndExerciseCommand => c.kind === "createAndExercise", + (c): c is CreateAndExerciseCommand => + c.kind === "createAndExercise" && c.choice === "OrderMatchExecution_Execute", ); } @@ -158,6 +159,14 @@ export class MatchingLedger implements LedgerSubmitter { if (cmd.kind !== "createAndExercise") { throw new Error(`unexpected ${cmd.kind} submission`); } + if (cmd.choice === "OrderMatchExecution_PreviewSettlement") { + const arg = cmd.argument as ExecuteArgument; + return { + previewFor: arg.matchId, + actors: [arg.operator], + extraArgs: { context: { values: {} }, meta: { values: {} } }, + } as R; + } return this.execute(cmd.argument as ExecuteArgument) as R; } diff --git a/services/operator-backend/test/order-fill-recording.test.ts b/services/operator-backend/test/order-fill-recording.test.ts index e6bd5384..7ff25d63 100644 --- a/services/operator-backend/test/order-fill-recording.test.ts +++ b/services/operator-backend/test/order-fill-recording.test.ts @@ -5,24 +5,7 @@ import { OrderService } from "../src/order/index.js"; import type { LedgerSubmitter, SubmitRequest } from "../src/ledger/index.js"; import type { Order } from "../src/types.js"; import { MatchingLedger, type ExecuteArgument } from "./matching-ledger.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; - -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories() { - return { - allocationFactoryCid: "#alloc-fac:0" as ContractId<"AllocationFactory">, - settlementFactoryCid: "#settle-fac:0" as ContractId<"SettlementFactory">, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } -} +import { StubRegistry } from "./stub-registry.js"; // eslint-disable-next-line @typescript-eslint/no-explicit-any function mkOrder(o: Record): Order { @@ -79,15 +62,20 @@ const bid = (o: Record): Order => }); describe("OrderService.runMatching settlement", () => { - it("settles a match in exactly one submission", async () => { + it("settles a match in one value-moving submission after a read-only preview", async () => { const ledger = new MatchingLedger([ask({}), bid({})]); const results = await service(ledger).runMatching(RUN); assert.equal(results.length, 1); assert.equal(results[0]!.error, undefined); - // Settlement, both order transitions, and the trade record are atomic. - assert.equal(ledger.submissions.length, 1); + // Discovery gets an exact on-ledger preview first. Settlement, both order + // transitions, and the trade record then remain one atomic submission. + assert.equal(ledger.submissions.length, 2); + assert.equal( + (ledger.submissions[0]!.command as { choice?: string }).choice, + "OrderMatchExecution_PreviewSettlement", + ); assert.equal( ledger.executes[0]!.templateId, "CantonDex.Dex.OrderMatchExecution:OrderMatchExecution", @@ -97,7 +85,7 @@ describe("OrderService.runMatching settlement", () => { // without readAs the admin the operator cannot see them and the settle // fails CONTRACT_NOT_FOUND on a real ledger. assert.ok( - (ledger.submissions[0]!.readAs ?? []).includes(RUN.admin), + (ledger.submissions[1]!.readAs ?? []).includes(RUN.admin), "the settle must readAs the instrument admin", ); assert.equal( @@ -109,11 +97,12 @@ describe("OrderService.runMatching settlement", () => { it("records a partial-fill remainder in the settlement transaction", async () => { const ledger = new MatchingLedger([ask({}), bid({ remainingQty: "3" })]); - // The ledger rejects any second submission. A correct match still succeeds - // because it records its funded remainder in the settlement transaction. + // The ledger allows preview + execute but rejects any third submission. A + // correct match succeeds because it records its funded remainder inside + // the value-moving settlement transaction. const flaky: LedgerSubmitter = { submit: async (req: SubmitRequest): Promise => { - if (ledger.submissions.length > 0) throw new Error("ledger unavailable"); + if (ledger.submissions.length > 1) throw new Error("ledger unavailable"); return ledger.submit(req); }, subscribe: ledger.subscribe.bind(ledger), @@ -332,7 +321,7 @@ describe("OrderService.runMatching settlement", () => { assert.equal(results[0]!.buyRemainderCid, null, "the bid did not close out"); assert.equal(results.length, 1); - assert.equal(ledger.submissions.length, 1); + assert.equal(ledger.submissions.length, 2, "preview + one value-moving execute"); assert.deepEqual( ledger.executes.map((e) => (e.argument as ExecuteArgument).buyOrderCid), ["#bid:1"], diff --git a/services/operator-backend/test/order-route-pair-param.test.ts b/services/operator-backend/test/order-route-pair-param.test.ts index dc036666..c84b0eac 100644 --- a/services/operator-backend/test/order-route-pair-param.test.ts +++ b/services/operator-backend/test/order-route-pair-param.test.ts @@ -8,24 +8,7 @@ import assert from "node:assert/strict"; import { InMemoryLedger } from "../src/ledger/in-memory.js"; import { OperatorBackend } from "../src/index.js"; import { startHttpServer } from "../src/http/index.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; - -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories() { - return { - allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, - settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } -} +import { StubRegistry } from "./stub-registry.js"; let baseUrl: string; let close: () => Promise; @@ -44,10 +27,6 @@ before(async () => { operator: "op" as never, lpRegistrar: "lp" as never, admin: "ad" as never, - allocationFactoryCid: "#alloc:0", - settlementFactoryCid: "#settle:0", - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network: "canton:test", }, devOpen: true, diff --git a/services/operator-backend/test/order.test.ts b/services/operator-backend/test/order.test.ts index bbeb5398..08ce5d27 100644 --- a/services/operator-backend/test/order.test.ts +++ b/services/operator-backend/test/order.test.ts @@ -7,21 +7,15 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; -import { OrderService } from "../src/order/index.js"; +import { OrderAuthError, OrderService } from "../src/order/index.js"; import type { LedgerSubmitter, SubmitRequest, SubscriptionFilter, LedgerEvent, } from "../src/ledger/index.js"; -import { RegistryClient } from "@canton-dex/registry-client"; import type { ContractId } from "@canton-dex/registry-client"; - -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } -} +import { StubRegistry } from "./stub-registry.js"; const FUNDING_TEMPLATE = "abcdef:CantonDex.Dex.OrderFundingRequest:OrderFundingRequest"; @@ -54,6 +48,7 @@ const BIND_SPEC = { class CapturingLedger implements LedgerSubmitter { lastSubmit: SubmitRequest | null = null; treeEvents: Array<{ contractId: string; templateId: string }> = []; + queryRows: unknown[] = []; async submit(req: SubmitRequest): Promise { this.lastSubmit = req; return { @@ -72,7 +67,7 @@ class CapturingLedger implements LedgerSubmitter { // no streaming in this stub } async query(_f: SubscriptionFilter): Promise { - return []; + return this.queryRows as T[]; } } @@ -136,3 +131,53 @@ describe("OrderService.bind", () => { ); }); }); + +describe("OrderService caller binding", () => { + it("binds only the funding request owned by the verified caller", async () => { + const ledger = new CapturingLedger(); + ledger.queryRows = [{ contractId: "00abc", trader: "alice" }]; + const svc = new OrderService(ledger, new StubRegistry(), "op" as never); + + await assert.rejects( + () => svc.bind({ + fundingRequestCid: "00abc" as ContractId<"OrderFundingRequest">, + settlementRef: "ref-auth", + requireTrader: "mallory" as never, + }), + OrderAuthError, + ); + assert.equal(ledger.lastSubmit, null); + + await svc.bind({ + fundingRequestCid: "00abc" as ContractId<"OrderFundingRequest">, + settlementRef: "ref-auth", + requireTrader: "alice" as never, + }); + assert.equal(commandOf(ledger).contractId, "00abc"); + }); + + it("fund and cancel reject another trader's order", async () => { + const ledger = new CapturingLedger(); + ledger.queryRows = [{ + contractId: "00order", + trader: "alice", + status: "Pending", + allocationCid: null, + }]; + const svc = new OrderService(ledger, new StubRegistry(), "op" as never); + + await assert.rejects( + () => svc.fund({ + orderCid: "00order" as ContractId<"Order">, + allocationCid: "00alloc" as ContractId<"Allocation">, + requireTrader: "mallory" as never, + }), + OrderAuthError, + ); + await assert.rejects( + () => svc.cancel("00order" as ContractId<"Order">, "mallory" as never), + OrderAuthError, + ); + assert.equal(ledger.lastSubmit, null); + }); +}); diff --git a/services/operator-backend/test/pool-status-normalisation.test.ts b/services/operator-backend/test/pool-status-normalisation.test.ts index 0363ea9d..bdf3d7e7 100644 --- a/services/operator-backend/test/pool-status-normalisation.test.ts +++ b/services/operator-backend/test/pool-status-normalisation.test.ts @@ -6,8 +6,7 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { PoolService } from "../src/pool/index.ts"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef } from "@canton-dex/registry-client"; +import { StubRegistry } from "./stub-registry.js"; import type { LedgerEvent, LedgerSubmitter, @@ -18,22 +17,6 @@ import type { Party } from "../src/types.ts"; const OPERATOR = "operator::test" as Party; -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories() { - return { - allocationFactoryCid: "#f:0" as never, - settlementFactoryCid: "#f:0" as never, - disclosure: [], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } -} - /** Serves one pool whose PoolState carries whatever status the test sets. */ function ledgerServing(status: string): LedgerSubmitter { return { diff --git a/services/operator-backend/test/pool.test.ts b/services/operator-backend/test/pool.test.ts index e1513548..4cb26016 100644 --- a/services/operator-backend/test/pool.test.ts +++ b/services/operator-backend/test/pool.test.ts @@ -15,12 +15,13 @@ import type { SubscriptionFilter, LedgerEvent, } from "../src/ledger/index.js"; -import { RegistryClient } from "@canton-dex/registry-client"; +import { + FixedRegistryClient, + RegistryClient, +} from "@canton-dex/registry-client"; import type { - ChoiceContextRef, ContractId, DisclosedContract, - FactoryRefs, } from "@canton-dex/registry-client"; import type { LPTokenPolicy, @@ -29,19 +30,13 @@ import type { Party, } from "../src/types.js"; -class StubRegistry extends RegistryClient { +class StubRegistry extends FixedRegistryClient { constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories(_admin: Party): Promise { - return { + super(() => ({ allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, disclosure: [], - }; - } - override async getChoiceContext(_admin: Party): Promise { - return { context: { values: {} }, disclosure: [] }; + })); } } @@ -53,20 +48,13 @@ function disclosed(contractId: string): DisclosedContract { }; } -class PerAdminRegistry extends StubRegistry { - override async getFactories(admin: Party): Promise { - return { +class PerAdminRegistry extends FixedRegistryClient { + constructor() { + super((admin: Party) => ({ allocationFactoryCid: `#alloc:${admin}` as ContractId<"AllocationFactory">, settlementFactoryCid: `#settle:${admin}` as ContractId<"SettlementFactory">, disclosure: [disclosed("#shared-rules"), disclosed(`#factory:${admin}`)], - }; - } - - override async getChoiceContext(admin: Party): Promise { - return { - context: { values: { [`ctx.${admin}`]: true } }, - disclosure: [disclosed("#shared-rules"), disclosed(`#context:${admin}`)], - }; + })); } } @@ -80,6 +68,7 @@ class CapturingLedger implements LedgerSubmitter { servePolicy = true; acceptances: LiquidityAllocationAcceptanceContract[] = []; treeEvents: Array<{ contractId: string; templateId: string }> = []; + private allocationCounter = 0; private readonly policies: LPTokenPolicy[]; constructor(private readonly pool: Pool, policyOrPolicies: LPTokenPolicy | LPTokenPolicy[]) { this.policies = Array.isArray(policyOrPolicies) @@ -88,6 +77,32 @@ class CapturingLedger implements LedgerSubmitter { } async submit(req: SubmitRequest): Promise { this.lastSubmit = req; + const choice = (req.command as { choice?: string }).choice; + if (choice === "PoolLiquidityRules_PreviewAddAllocations") { + return { + baseReceiver: {}, + quoteReceiver: {}, + lpMintSender: {}, + } as R; + } + if (choice === "PoolLiquidityRules_PreviewRemoveAllocations") { + return { lpBurnReceiver: {} } as R; + } + if ( + choice === "PoolLiquidityRules_PreviewAddSettlement" || + choice === "PoolLiquidityRules_PreviewRemoveSettlement" + ) { + return { baseQuoteBatch: {}, lpBatch: {} } as R; + } + if (choice === "AllocationFactory_Allocate") { + const allocationCid = `#created-allocation:${this.allocationCounter++}`; + return { + output: { + tag: "AllocationInstructionResult_Completed", + value: { allocationCid }, + }, + } as R; + } return "#result:0" as R; } async treeCreatedEvents() { @@ -471,7 +486,7 @@ describe("PoolService DvP liquidity", () => { assert.equal(ledger.lastSubmit, null); }); - it("settleAddLiquidity is co-signed and threads requestCid + both registries' factories + per-admin contexts", async () => { + it("settleAddLiquidity is co-signed and threads requestCid + both self-registry factory sets", async () => { const pool = mkPool(0, 0); const ledger = new CapturingLedger(pool, mkLpPolicy()); const svc = new PoolService(ledger, new PerAdminRegistry(), "op" as never); @@ -508,13 +523,14 @@ describe("PoolService DvP liquidity", () => { assert.equal(cmd.argument.lpFactoryCid, "#alloc:lp"); assert.equal(cmd.argument.baseQuoteSettleCid, "#settle:ad"); assert.equal(cmd.argument.lpSettleCid, "#settle:lp"); - // Split-admin contexts threaded separately, not collapsed. + // The fixed self-registry requires no operation-specific context. The two + // admin slots still remain separate and must never collapse to one field. assert.deepEqual(cmd.argument.poolAdminExtraArgs, { - context: { values: { "ctx.ad": true } }, + context: { values: {} }, meta: { values: {} }, }); assert.deepEqual(cmd.argument.lpRegistrarExtraArgs, { - context: { values: { "ctx.lp": true } }, + context: { values: {} }, meta: { values: {} }, }); assert.equal(cmd.argument.extraArgs, undefined, "no collapsed single extraArgs"); @@ -523,12 +539,45 @@ describe("PoolService DvP liquidity", () => { "#shared-rules", "#factory:ad", "#factory:lp", - "#context:ad", - "#context:lp", ])); assert.equal(disclosureIds.length, new Set(disclosureIds).size); }); + it("stops before allocation when operation-specific registry discovery fails", async () => { + const pool = mkPool(0, 0); + const ledger = new CapturingLedger(pool, mkLpPolicy()); + const svc = new PoolService( + ledger, + new RegistryClient({ + baseUrl: "https://registry.example", + fetchImpl: async () => new Response(null, { status: 404 }), + }), + "op" as never, + ); + + await assert.rejects( + svc.settleAddLiquidity({ + poolCid: pool.contractId, + requestCid: "#req:unsupported" as never, + recipient: "lp" as never, + lpBaseDepositCid: "#b:unsupported" as never, + lpQuoteDepositCid: "#q:unsupported" as never, + lpReceiptCid: "#r:unsupported" as never, + baseAmount: "10.0", + quoteAmount: "200000.0", + minLpTokens: "0.0", + knownTotalLpSupply: "0.0", + requestedAt, + }), + /registry: not-found: \/registry\/allocation-instruction\/v2\/allocation-factory/, + ); + assert.equal( + (ledger.lastSubmit!.command as { choice?: string }).choice, + "PoolLiquidityRules_PreviewAddAllocations", + "only the read-only plan may run before registry discovery fails", + ); + }); + it("settleAddLiquidity binds to acceptance evidence when no live request is supplied", async () => { const pool = mkPool(0, 0); const ledger = new CapturingLedger(pool, mkLpPolicy()); @@ -730,13 +779,13 @@ describe("PoolService DvP liquidity", () => { assert.deepEqual(ledger.lastSubmit!.actAs, ["op", "lp"]); assert.equal(cmd.argument.requestCid, "#req:1"); assert.equal(cmd.argument.holderBurnSenderCid, "#burn:0"); - // Split-admin contexts threaded separately, not collapsed. + // Fixed self-registry contexts are empty but remain separate per admin. assert.deepEqual(cmd.argument.poolAdminExtraArgs, { - context: { values: { "ctx.ad": true } }, + context: { values: {} }, meta: { values: {} }, }); assert.deepEqual(cmd.argument.lpRegistrarExtraArgs, { - context: { values: { "ctx.lp": true } }, + context: { values: {} }, meta: { values: {} }, }); assert.equal(cmd.argument.extraArgs, undefined, "no collapsed single extraArgs"); @@ -745,8 +794,6 @@ describe("PoolService DvP liquidity", () => { "#shared-rules", "#factory:ad", "#factory:lp", - "#context:ad", - "#context:lp", ])); assert.equal(disclosureIds.length, new Set(disclosureIds).size); }); diff --git a/services/operator-backend/test/read-exposure.test.ts b/services/operator-backend/test/read-exposure.test.ts index a9f64dd4..050b2925 100644 --- a/services/operator-backend/test/read-exposure.test.ts +++ b/services/operator-backend/test/read-exposure.test.ts @@ -5,30 +5,32 @@ import assert from "node:assert/strict"; import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { createHmac } from "node:crypto"; import { openDb, type Db } from "../src/indexer/db.js"; import { InMemoryLedger } from "../src/ledger/in-memory.js"; +import type { SubscriptionFilter } from "../src/ledger/index.js"; import { OperatorBackend } from "../src/index.js"; import { startHttpServer } from "../src/http/index.js"; import { aggregateBook } from "../src/order/matching.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; +import { StubRegistry } from "./stub-registry.js"; import type { Order } from "../src/types.js"; const ADMIN_TOKEN = "admin-secret"; - -class StubRegistry extends RegistryClient { - constructor() { super({ baseUrl: "http://stub" }); } - override async getFactories() { - return { - allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, - settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } +const CALLER_SECRET = "caller-secret"; + +function callerToken(sub: string): string { + const encode = (value: string | Buffer) => + Buffer.from(value).toString("base64url"); + const header = encode(JSON.stringify({ alg: "HS256", typ: "JWT" })); + const payload = encode(JSON.stringify({ + sub, + exp: Math.floor(Date.now() / 1000) + 3600, + })); + const signature = encode( + createHmac("sha256", CALLER_SECRET).update(`${header}.${payload}`).digest(), + ); + return `${header}.${payload}.${signature}`; } let baseUrl: string; @@ -54,11 +56,10 @@ before(async () => { port: 0, host: "127.0.0.1", adminToken: ADMIN_TOKEN, + callerJwtSecret: CALLER_SECRET, context: { operator: "op" as never, lpRegistrar: "lp" as never, admin: "ad" as never, - allocationFactoryCid: "#alloc:0", settlementFactoryCid: "#settle:0", - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network: "canton:test", + network: "canton:test", }, devOpen: true, }); @@ -71,9 +72,12 @@ after(async () => { rmSync(dir, { recursive: true, force: true }); }); -const get = async (p: string, token?: string) => { +const get = async (p: string, token?: string, caller?: string) => { const r = await fetch(`${baseUrl}${p}`, { - headers: token ? { authorization: `Bearer ${token}` } : {}, + headers: { + ...(token ? { authorization: `Bearer ${token}` } : {}), + ...(caller ? { "x-caller-token": callerToken(caller) } : {}), + }, }); return { status: r.status, body: (await r.json().catch(() => ({}))) as any }; }; @@ -86,11 +90,19 @@ describe("GET /v1/trades scoping", () => { }); it("serves a scoped read", async () => { - const r = await get("/v1/trades?trader=alice"); + const r = await get("/v1/trades?trader=alice", undefined, "alice"); assert.equal(r.status, 200); assert.equal(r.body.length, 1); }); + it("rejects a missing or mismatched caller on a scoped read", async () => { + assert.equal((await get("/v1/trades?trader=alice")).status, 401); + assert.equal( + (await get("/v1/trades?trader=alice", undefined, "mallory")).status, + 403, + ); + }); + it("the admin token still gets the unfiltered view", async () => { const r = await get("/v1/trades", ADMIN_TOKEN); assert.equal(r.status, 200); @@ -98,6 +110,21 @@ describe("GET /v1/trades scoping", () => { }); }); +describe("party-scoped ACS reads", () => { + for (const path of [ + "/v1/orders?trader=alice", + "/v1/holdings?owner=alice", + "/v1/balances?owner=alice", + ]) { + it(`${path} binds the query party to the caller`, async () => { + assert.equal((await get(path)).status, 401); + assert.equal((await get(path, undefined, "mallory")).status, 403); + assert.equal((await get(path, undefined, "alice")).status, 200); + assert.equal((await get(path, ADMIN_TOKEN)).status, 200); + }); + } +}); + describe("GET /v1/orders/matches", () => { it("serves only the terms, not the whole orders", async () => { const r = await get("/v1/orders/matches?pair=dBTC/dUSD"); @@ -129,3 +156,61 @@ describe("aggregateBook", () => { assert.equal(bids[0]!.size, "0.0000000001", "float renders this as 1e-10"); }); }); + +describe("bounded history queries", () => { + it("rejects malformed or non-positive limits", async () => { + assert.equal( + (await get("/v1/trades?trader=alice&limit=-1", undefined, "alice")).status, + 400, + ); + assert.equal((await get("/v1/swaps?limit=not-a-number")).status, 400); + }); + + it("clamps oversized limits instead of emitting an unbounded query", async () => { + assert.equal( + (await get("/v1/trades?trader=alice&limit=999999", undefined, "alice")).status, + 200, + ); + }); +}); + +describe("holding query failures", () => { + it("returns an error instead of presenting a ledger failure as a zero balance", async () => { + class FailingHoldingLedger extends InMemoryLedger { + override async query(filter: SubscriptionFilter): Promise { + if (filter.templateId?.endsWith("Registry.V2:Holding")) { + throw new Error("participant unavailable"); + } + return []; + } + } + + const handle = await startHttpServer({ + backend: new OperatorBackend({ + ledger: new FailingHoldingLedger(), + registry: new StubRegistry(), + operatorParty: "op" as never, + }), + port: 0, + host: "127.0.0.1", + callerJwtSecret: CALLER_SECRET, + context: { + operator: "op" as never, + lpRegistrar: "lp" as never, + admin: "ad" as never, + network: "canton:test", + }, + devOpen: true, + }); + try { + const response = await fetch(`${handle.url}/v1/balances?owner=alice`, { + headers: { "x-caller-token": callerToken("alice") }, + }); + assert.equal(response.status, 503); + const body = await response.json() as { error?: string }; + assert.equal(body.error, "unable to load holdings from the ledger"); + } finally { + await handle.close(); + } + }); +}); diff --git a/services/operator-backend/test/registry-client.test.ts b/services/operator-backend/test/registry-client.test.ts index bdb19f8f..932f1b19 100644 --- a/services/operator-backend/test/registry-client.test.ts +++ b/services/operator-backend/test/registry-client.test.ts @@ -3,54 +3,156 @@ import { describe, it } from "node:test"; import { RegistryClient, - type ChoiceContextRef, - type ContractId, + RegistryError, + type ChoiceArguments, } from "@canton-dex/registry-client"; -describe("RegistryClient.getChoiceContext", () => { - it("fetches and caches the registry-supplied context + disclosure", async () => { - let calls = 0; - const expected: ChoiceContextRef = { - context: { values: { "dex.choiceContext": true } }, - disclosure: [ - { - contractId: "#registry:0" as ContractId<"Registry">, - templateId: "CantonDex.Registry.V2:Registry", - createdEventBlob: "payload", - }, - ], +const disclosed = { + contractId: "#registry-rules:0", + templateId: "Registry:Rules", + contractKeyHash: "key-hash", + createdEventBlob: "created-event-base64", + synchronizerId: "domain::id", +}; + +function factoryWire(factoryId: string, marker: string) { + return { + factoryId, + choiceContext: { + choiceContextData: { values: { marker } }, + disclosedContracts: [disclosed], + }, + }; +} + +function json(value: unknown, status = 200): Response { + return new Response(JSON.stringify(value), { + status, + headers: { "Content-Type": "application/json" }, + }); +} + +describe("RegistryClient operation-specific Token Standard V2 discovery", () => { + it("POSTs the exact allocation choice argument and never reuses its response", async () => { + const choiceArguments: ChoiceArguments = { + expectedAdmin: "admin-a", + allocation: { settlement: { id: "swap-42" } }, }; + let calls = 0; const client = new RegistryClient({ - baseUrl: "https://registry.example", - fetchImpl: async (input) => { + baseUrl: "https://registry.example/base/", + authToken: "registry-token", + fetchImpl: async (input, init) => { calls += 1; assert.equal( input.toString(), - "https://registry.example/registry/choice-context/admin-a", + "https://registry.example/registry/allocation-instruction/v2/allocation-factory", + ); + assert.equal(init?.method, "POST"); + assert.equal( + (init?.headers as Record).Authorization, + "Bearer registry-token", ); - return new Response(JSON.stringify(expected), { - status: 200, - headers: { "Content-Type": "application/json" }, + assert.deepEqual(JSON.parse(String(init?.body)), { choiceArguments }); + return json(factoryWire("#allocation-factory:0", `call-${calls}`)); + }, + }); + + const first = await client.getAllocationFactory("admin-a", choiceArguments); + const second = await client.getAllocationFactory("admin-a", choiceArguments); + + assert.equal(first.factoryCid, "#allocation-factory:0"); + assert.deepEqual(first.context.values, { marker: "call-1" }); + assert.deepEqual(first.disclosure, [disclosed]); + assert.deepEqual(second.context.values, { marker: "call-2" }); + assert.equal(calls, 2, "choice context may be specific to one exercise"); + }); + + it("resolves each admin's settlement endpoint and sends the exact preview", async () => { + const preview = { + settlement: { executors: ["operator"], id: "match-7", cid: null }, + allocations: [{ allocationCid: "#allocation:7" }], + }; + const client = new RegistryClient({ + baseUrl: (admin) => `https://${admin}.registry.example/`, + fetchImpl: async (input, init) => { + assert.equal( + input.toString(), + "https://admin-b.registry.example/registry/allocation/v2/settlement-factory", + ); + assert.deepEqual(JSON.parse(String(init?.body)), { + choiceArguments: preview, }); + return json(factoryWire("#settlement-factory:b", "settle-b")); }, }); - const first = await client.getChoiceContext("admin-a"); - const second = await client.getChoiceContext("admin-a"); + const got = await client.getSettlementFactory("admin-b", preview); - assert.deepEqual(first, expected); - assert.deepEqual(second, expected); - assert.equal(calls, 1); + assert.equal(got.factoryCid, "#settlement-factory:b"); + assert.deepEqual(got.context.values, { marker: "settle-b" }); }); - it("falls back to empty context when the registry has no endpoint", async () => { + it("uses allocation-specific cancel and withdraw context endpoints", async () => { + const seen: Array<{ url: string; body: unknown }> = []; const client = new RegistryClient({ baseUrl: "https://registry.example", - fetchImpl: async () => new Response(null, { status: 404 }), + fetchImpl: async (input, init) => { + seen.push({ + url: input.toString(), + body: JSON.parse(String(init?.body)), + }); + return json({ + choiceContextData: { values: { operation: seen.length } }, + disclosedContracts: [], + }); + }, }); - const ctx = await client.getChoiceContext("admin-b"); + const cancel = await client.getAllocationCancelContext( + "admin-a", + "#allocation/with spaces", + { reason: "user-request" }, + ); + const withdraw = await client.getAllocationWithdrawContext( + "admin-a", + "#allocation/with spaces", + ); - assert.deepEqual(ctx, { context: { values: {} }, disclosure: [] }); + assert.deepEqual(seen, [ + { + url: + "https://registry.example/registry/allocations/v2/%23allocation%2Fwith%20spaces/choice-contexts/cancel", + body: { meta: { reason: "user-request" } }, + }, + { + url: + "https://registry.example/registry/allocations/v2/%23allocation%2Fwith%20spaces/choice-contexts/withdraw", + body: { meta: {} }, + }, + ]); + assert.deepEqual(cancel.context.values, { operation: 1 }); + assert.deepEqual(withdraw.context.values, { operation: 2 }); }); + + it("fails closed for missing or malformed canonical responses", async () => { + const missing = new RegistryClient({ + baseUrl: "https://registry.example", + fetchImpl: async () => new Response(null, { status: 404 }), + }); + await assert.rejects( + missing.getAllocationFactory("admin-a", { allocation: "exact" }), + (error) => error instanceof RegistryError && error.kind === "not-found", + ); + + const malformed = new RegistryClient({ + baseUrl: "https://registry.example", + fetchImpl: async () => json({ factoryId: "#factory:0" }), + }); + await assert.rejects( + malformed.getSettlementFactory("admin-a", { settlement: "exact" }), + (error) => error instanceof RegistryError && error.kind === "malformed", + ); + }); + }); diff --git a/services/operator-backend/test/rfq-read-scoping.test.ts b/services/operator-backend/test/rfq-read-scoping.test.ts index f2a3875f..cf7526e8 100644 --- a/services/operator-backend/test/rfq-read-scoping.test.ts +++ b/services/operator-backend/test/rfq-read-scoping.test.ts @@ -5,33 +5,32 @@ import { describe, it, before, after } from "node:test"; import assert from "node:assert/strict"; +import { createHmac } from "node:crypto"; import { InMemoryLedger } from "../src/ledger/in-memory.js"; import type { SubscriptionFilter } from "../src/ledger/index.js"; import { OperatorBackend } from "../src/index.js"; import { startHttpServer } from "../src/http/index.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; +import { StubRegistry } from "./stub-registry.js"; const ALICE = "alice"; const BOB = "bob"; const DEALER = "northwind"; const ADMIN_TOKEN = "admin-secret"; +const CALLER_SECRET = "caller-secret"; -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories() { - return { - allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, - settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } +function callerToken(sub: string): string { + const encode = (value: string | Buffer) => + Buffer.from(value).toString("base64url"); + const header = encode(JSON.stringify({ alg: "HS256", typ: "JWT" })); + const payload = encode(JSON.stringify({ + sub, + exp: Math.floor(Date.now() / 1000) + 3600, + })); + const signature = encode( + createHmac("sha256", CALLER_SECRET).update(`${header}.${payload}`).digest(), + ); + return `${header}.${payload}.${signature}`; } class RfqLedger extends InMemoryLedger { @@ -66,14 +65,11 @@ before(async () => { port: 0, host: "127.0.0.1", adminToken: ADMIN_TOKEN, + callerJwtSecret: CALLER_SECRET, context: { operator: "op" as never, lpRegistrar: "lp" as never, admin: "ad" as never, - allocationFactoryCid: "#alloc:0", - settlementFactoryCid: "#settle:0", - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network: "canton:test", }, devOpen: true, @@ -86,9 +82,12 @@ after(async () => { await close(); }); -const get = async (path: string, token?: string) => { +const get = async (path: string, token?: string, caller?: string) => { const res = await fetch(`${baseUrl}${path}`, { - headers: token ? { authorization: `Bearer ${token}` } : {}, + headers: { + ...(token ? { authorization: `Bearer ${token}` } : {}), + ...(caller ? { "x-caller-token": callerToken(caller) } : {}), + }, }); return { status: res.status, body: await res.json().catch(() => ({})) as any }; }; @@ -101,14 +100,14 @@ describe("GET /v1/rfq scoping", () => { }); it("a trader sees only their own RFQs and quotes", async () => { - const r = await get(`/v1/rfq?owner=${ALICE}`); + const r = await get(`/v1/rfq?owner=${ALICE}`, undefined, ALICE); assert.equal(r.status, 200); assert.deepEqual(r.body.rfqs.map((x: any) => x.rfqId), ["a1"]); assert.deepEqual(r.body.quotes.map((x: any) => x.rfqId), ["a1"]); }); it("one trader cannot see another's size or the prices quoted to them", async () => { - const r = await get(`/v1/rfq?owner=${ALICE}`); + const r = await get(`/v1/rfq?owner=${ALICE}`, undefined, ALICE); const leaked = JSON.stringify(r.body); assert.ok(!leaked.includes("b1"), "bob's RFQ id leaked"); assert.ok(!leaked.includes("50.0"), "bob's size leaked"); @@ -116,11 +115,19 @@ describe("GET /v1/rfq scoping", () => { }); it("a whitelisted dealer sees the RFQ and its own quotes", async () => { - const r = await get(`/v1/rfq?owner=${DEALER}`); + const r = await get(`/v1/rfq?owner=${DEALER}`, undefined, DEALER); assert.deepEqual(r.body.rfqs.map((x: any) => x.rfqId), ["a1"], "whitelisted on a1 only"); assert.equal(r.body.quotes.length, 2, "its own quotes on both"); }); + it("rejects a missing or mismatched caller on a scoped RFQ read", async () => { + assert.equal((await get(`/v1/rfq?owner=${ALICE}`)).status, 401); + assert.equal( + (await get(`/v1/rfq?owner=${ALICE}`, undefined, BOB)).status, + 403, + ); + }); + it("refuses an unscoped /v1/rfq/history without the admin token", async () => { // Same exposure as /v1/rfq: each settled row names the trader, the pair, // the winning dealer and its rank. diff --git a/services/operator-backend/test/rfq.test.ts b/services/operator-backend/test/rfq.test.ts index 926fd3b6..e439c1f3 100644 --- a/services/operator-backend/test/rfq.test.ts +++ b/services/operator-backend/test/rfq.test.ts @@ -1,4 +1,4 @@ -// End-to-end test for the operator backend's RFQ accept flow. +// Service-level integration test for the operator backend's RFQ accept flow. // Drives the InMemoryLedger with handlers that mimic Daml choice // semantics, then exercises RfqService.accept and asserts on the // resulting MatchedTrade + PolicyReceipt. @@ -21,26 +21,15 @@ import type { PolicyReceipt, } from "../src/types.ts"; import { RfqAuthError } from "../src/rfq/index.ts"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef } from "@canton-dex/registry-client"; +import { FixedRegistryClient } from "@canton-dex/registry-client"; -class StubRegistry extends RegistryClient { +class StubRegistry extends FixedRegistryClient { constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories(): Promise<{ - allocationFactoryCid: ContractId<"AllocationFactory">; - settlementFactoryCid: ContractId<"SettlementFactory">; - disclosure: never[]; - }> { - return { + super(() => ({ allocationFactoryCid: "#alloc-fac:0" as ContractId<"AllocationFactory">, settlementFactoryCid: "#settle-fac:0" as ContractId<"SettlementFactory">, disclosure: [], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; + })); } } @@ -161,7 +150,7 @@ function setupLedger(): InMemoryLedger { return ledger; } -test("RFQ accept end-to-end through operator backend", async () => { +test("RFQ accept across the operator service boundary", async () => { const ledger = setupLedger(); const registry = new StubRegistry(); const operator: Party = "operator::test"; diff --git a/services/operator-backend/test/server-port.test.ts b/services/operator-backend/test/server-port.test.ts index cdf188e1..b70d35af 100644 --- a/services/operator-backend/test/server-port.test.ts +++ b/services/operator-backend/test/server-port.test.ts @@ -28,4 +28,54 @@ describe("server entrypoints", () => { ); }); } + + it("testnet-server never enables the development write bypass", () => { + const source = readFileSync(join(SRC, "testnet-server.ts"), "utf8"); + assert.match(source, /devOpen:\s*false/); + assert.doesNotMatch( + source, + /devOpen:\s*process\.env\.DEX_DEV_OPEN/, + "testnet-server must not honor the in-memory server's auth bypass", + ); + }); + + it("testnet-server never enables the arbitrary-command wallet relay", () => { + const source = readFileSync(join(SRC, "testnet-server.ts"), "utf8"); + assert.match(source, /walletRelayEnabled:\s*false/); + assert.doesNotMatch( + source, + /walletRelayEnabled:\s*process\.env\.DEX_DEV_WALLET_RELAY/, + "testnet-server must not forward wallet commands under its participant JWT", + ); + }); + + it("testnet-server makes hosted trader-authority RFQ relay opt-in", () => { + const source = readFileSync(join(SRC, "testnet-server.ts"), "utf8"); + assert.match( + source, + /hostedRfqEnabled\s*=\s*process\.env\.DEX_HOSTED_RFQ_RELAY\s*===\s*"1"/, + ); + assert.match(source, /hostedRfqEnabled\s*&&\s*!callerJwtSecret/); + assert.match(source, /readOnly\s*&&\s*hostedRfqEnabled/); + }); + + it("testnet-server uses the per-admin fixed self-registry adapter", () => { + const source = readFileSync(join(SRC, "testnet-server.ts"), "utf8"); + assert.match(source, /class ConfiguredRegistry extends FixedRegistryClient/); + assert.match(source, /super\(\(admin\)\s*=>/); + assert.match(source, /factoriesByAdmin\.get\(admin\)/); + assert.match(source, /registry:\s*new ConfiguredRegistry\(factoriesByAdmin\)/); + assert.match(source, /required\("CANTON_LP_ALLOC_FACTORY_CID"\)/); + assert.match(source, /required\("CANTON_LP_SETTLE_FACTORY_CID"\)/); + }); + + it("dev-server identifies seeded state as an in-memory preview", () => { + const source = readFileSync(join(SRC, "dev-server.ts"), "utf8"); + assert.match(source, /network:\s*"preview:in-memory"/); + assert.doesNotMatch( + source, + /network:\s*process\.env\.CANTON_NETWORK/, + "The seeded server must not masquerade as a Canton network via an env label", + ); + }); }); diff --git a/services/operator-backend/test/status-sync.test.ts b/services/operator-backend/test/status-sync.test.ts new file mode 100644 index 00000000..0918ac31 --- /dev/null +++ b/services/operator-backend/test/status-sync.test.ts @@ -0,0 +1,51 @@ +// /v1/status must distinguish a healthy in-memory demo from a configured +// participant that cannot be reached. A fake moving slot would let deployment +// smoke checks pass while Canton is offline. + +import { after, before, describe, it } from "node:test"; +import assert from "node:assert/strict"; + +import { StubRegistry } from "./stub-registry.js"; +import { startHttpServer } from "../src/http/index.js"; +import { OperatorBackend } from "../src/index.js"; +import { InMemoryLedger } from "../src/ledger/in-memory.js"; + +let baseUrl = ""; +let close: () => Promise; + +before(async () => { + const backend = new OperatorBackend({ + ledger: new InMemoryLedger(), + registry: new StubRegistry(), + operatorParty: "op" as never, + }); + const handle = await startHttpServer({ + backend, + port: 0, + host: "127.0.0.1", + context: { + operator: "op" as never, + lpRegistrar: "lp" as never, + admin: "ad" as never, + network: "canton:test", + }, + // Deliberately unreachable. Merely configuring a participant must switch + // status out of the in-memory dev-counter behavior. + ledgerUrl: "http://127.0.0.1:1", + ledgerToken: "test-token", + }); + baseUrl = handle.url; + close = handle.close; +}); + +after(async () => close()); + +describe("participant sync status", () => { + it("reports unsynced instead of inventing a live slot", async () => { + const res = await fetch(`${baseUrl}/v1/status`); + assert.equal(res.status, 200); + const body = (await res.json()) as { slot: number; synced: boolean }; + assert.equal(body.synced, false); + assert.equal(body.slot, 0); + }); +}); diff --git a/services/operator-backend/test/stub-registry.ts b/services/operator-backend/test/stub-registry.ts new file mode 100644 index 00000000..b5f97352 --- /dev/null +++ b/services/operator-backend/test/stub-registry.ts @@ -0,0 +1,19 @@ +import { + FixedRegistryClient, + type ContractId, + type FactoryRefs, + type Party, +} from "@canton-dex/registry-client"; + +/** Fixed self-registry used by backend tests that do not exercise discovery. */ +export class StubRegistry extends FixedRegistryClient { + constructor( + factoriesForAdmin: (admin: Party) => FactoryRefs = () => ({ + allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, + settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, + disclosure: [], + }), + ) { + super(factoriesForAdmin); + } +} diff --git a/services/operator-backend/test/swaps-kind-filter.test.ts b/services/operator-backend/test/swaps-kind-filter.test.ts index 246d1e22..41fa05b2 100644 --- a/services/operator-backend/test/swaps-kind-filter.test.ts +++ b/services/operator-backend/test/swaps-kind-filter.test.ts @@ -12,24 +12,7 @@ import { openDb, type Db } from "../src/indexer/db.js"; import { InMemoryLedger } from "../src/ledger/in-memory.js"; import { OperatorBackend } from "../src/index.js"; import { startHttpServer } from "../src/http/index.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; - -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories() { - return { - allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, - settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } -} +import { StubRegistry } from "./stub-registry.js"; let baseUrl: string; let close: () => Promise; @@ -76,10 +59,6 @@ before(async () => { operator: "op" as never, lpRegistrar: "lp" as never, admin: "ad" as never, - allocationFactoryCid: "#alloc:0", - settlementFactoryCid: "#settle:0", - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network: "canton:test", }, devOpen: true, diff --git a/services/operator-backend/test/test-taxonomy-boundaries.test.ts b/services/operator-backend/test/test-taxonomy-boundaries.test.ts new file mode 100644 index 00000000..842cbb8a --- /dev/null +++ b/services/operator-backend/test/test-taxonomy-boundaries.test.ts @@ -0,0 +1,169 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; +import { existsSync, readFileSync, readdirSync } from "node:fs"; +import { join, resolve } from "node:path"; + +const ROOT = resolve(import.meta.dirname, "..", "..", ".."); + +function textFilesBelow(dir: string): string[] { + const files: string[] = []; + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) { + continue; + } + const path = join(dir, entry.name); + if (entry.isDirectory()) files.push(...textFilesBelow(path)); + else if (/\.(?:daml|json|md|mjs|sh|ts|tsx|yml)$/.test(entry.name)) files.push(path); + } + return files; +} + +describe("test taxonomy boundaries", () => { + it("keeps opt-in Canton tests outside the ordinary offline test glob", () => { + const packageJson = JSON.parse( + readFileSync(join(ROOT, "services/operator-backend/package.json"), "utf8"), + ) as { scripts: Record }; + + assert.equal(packageJson.scripts.test, "node --import tsx --test test/*.test.ts"); + assert.equal( + packageJson.scripts["test:live:rfq"], + "node --import tsx --test test/live/canton-live-rfq.test.ts", + ); + assert.ok( + existsSync(join(ROOT, "services/operator-backend/test/live/canton-live-rfq.test.ts")), + ); + assert.ok(!existsSync(join(ROOT, "services/operator-backend/test/canton-live-rfq.test.ts"))); + }); + + it("does not restore the misleading legacy E2E aliases", () => { + assert.ok(!existsSync(join(ROOT, "scripts/e2e-smoke.sh"))); + assert.ok(!existsSync(join(ROOT, "scripts/localnet-dvp-e2e.ts"))); + + const canonicalFiles = [ + join(ROOT, "README.md"), + ...textFilesBelow(join(ROOT, "docs")), + ...textFilesBelow(join(ROOT, "scripts")), + ...textFilesBelow(join(ROOT, "services")), + ...textFilesBelow(join(ROOT, "app")), + ...textFilesBelow(join(ROOT, "trading-tests")), + ...textFilesBelow(join(ROOT, ".github")), + ]; + const stale = canonicalFiles + .filter( + (file) => + file !== join(ROOT, "services/operator-backend/test/test-taxonomy-boundaries.test.ts"), + ) + .flatMap((file) => { + const match = readFileSync(file, "utf8").match( + /CANTON_E2E|e2e-smoke|localnet-dvp-e2e|localnet:dvp-e2e/, + ); + return match ? [`${file}: ${match[0]}`] : []; + }); + assert.deepEqual(stale, []); + }); + + it("keeps the mock-registry workflow proofs split into readable modules", () => { + const testsDir = join(ROOT, "trading-tests/CantonDex/Tests"); + assert.ok(!existsSync(join(testsDir, "WorkflowIntegrationTests.daml"))); + + const modules = readdirSync(testsDir) + .filter((name) => /WorkflowTests\.daml$/.test(name)) + .sort(); + assert.deepEqual(modules, [ + "ChoiceContextWorkflowTests.daml", + "OrderWorkflowTests.daml", + "PoolWorkflowTests.daml", + "TradeWorkflowTests.daml", + ]); + + let declarations = 0; + for (const module of modules) { + const source = readFileSync(join(testsDir, module), "utf8"); + const lines = source.split("\n").length; + assert.ok(lines <= 600, `${module} has ${lines} lines; split it again`); + declarations += [...source.matchAll(/^test[A-Z]\w*\s*:\s*Script\b/gm)].length; + } + assert.equal(declarations, 19); + + const fixtures = readFileSync(join(testsDir, "WorkflowTestFixtures.daml"), "utf8"); + assert.ok(fixtures.split("\n").length <= 300, "workflow fixtures became a new monolith"); + }); + + it("documents the live gate and the container runtime boundary", () => { + const testing = readFileSync(join(ROOT, "docs/reference/testing.md"), "utf8"); + assert.match(testing, /CANTON_LIVE_RFQ=1 npm run test:live:rfq/); + + const ci = readFileSync(join(ROOT, ".github/workflows/ci.yml"), "utf8"); + assert.match(ci, /name: Container build \+ backend runtime smoke/); + }); + + it("keeps one canonical newcomer curriculum in both entry points", () => { + const expected = [ + "concepts/canton-daml-primer.md", + "concepts/overview.md", + "getting-started.md", + "tutorials/amm-first-walkthrough.md", + "concepts/design-tour.md", + "concepts/architecture.md", + "concepts/workflows.md", + "tutorials/make-your-first-amm-change.md", + "guides/builder-guide.md", + ]; + + const index = readFileSync(join(ROOT, "docs/README.md"), "utf8"); + const indexSection = index + .split("## Canonical newcomer learning path", 2)[1]! + .split("\n## ", 1)[0]!; + const indexPaths = [...indexSection.matchAll(/^\|\s*\d+\s*\|\s*\[[^\]]+\]\(([^)]+)\)/gm)] + .map((match) => match[1]!); + assert.deepEqual(indexPaths, expected); + + const readme = readFileSync(join(ROOT, "README.md"), "utf8"); + const readmeSection = readme + .split("## New To Canton Or Daml?", 2)[1]! + .split("\n## ", 1)[0]!; + const readmePaths = [...readmeSection.matchAll(/^\d+\.\s*\[[^\]]+\]\(([^)]+)\)/gm)] + .map((match) => match[1]!.replace(/^docs\//, "")); + assert.deepEqual(readmePaths, expected); + + const website = readFileSync(join(ROOT, "website/astro.config.mjs"), "utf8"); + const websiteSection = website + .split("label: 'Newcomer learning path'", 2)[1]! + .split("label: 'Concepts'", 1)[0]!; + const websitePaths = [...websiteSection.matchAll(/slug:\s*'([^']+)'/g)] + .map((match) => `${match[1]}.md`); + assert.deepEqual(websitePaths, expected); + }); + + it("uses the live official archive for SDK 3.5 learning links", () => { + const docs = [ + readFileSync(join(ROOT, "README.md"), "utf8"), + readFileSync(join(ROOT, "docs/getting-started.md"), "utf8"), + readFileSync(join(ROOT, "docs/concepts/canton-daml-primer.md"), "utf8"), + ].join("\n"); + assert.doesNotMatch(docs, /https:\/\/docs\.digitalasset\.com\/build\/3\.5/); + assert.match( + docs, + /https:\/\/archived\.docs\.digitalasset\.com\/build\/3\.5\/dpm\/manual-install\.html/, + ); + }); + + it("keeps registry documentation operation-specific and fail-closed", () => { + const guide = readFileSync(join(ROOT, "docs/guides/choice-context.md"), "utf8"); + const obsolete = [ + ["get", "Factories"], + ["get", "ChoiceContext"], + ["choiceContext", "TtlMs"], + ].map((parts) => parts.join("")); + for (const name of obsolete) { + assert.ok(!guide.includes(name), `choice-context guide restored obsolete ${name}`); + } + assert.match( + guide, + /POST \/registry\/allocation-instruction\/v2\/allocation-factory/, + ); + assert.match(guide, /POST \/registry\/allocation\/v2\/settlement-factory/); + assert.match(guide, /RegistryError\("unsupported", \.\.\.\)/); + assert.match(guide, /do not exist before that transaction/); + }); +}); diff --git a/services/operator-backend/test/validation.test.ts b/services/operator-backend/test/validation.test.ts index 118520eb..56af968a 100644 --- a/services/operator-backend/test/validation.test.ts +++ b/services/operator-backend/test/validation.test.ts @@ -8,24 +8,7 @@ import assert from "node:assert/strict"; import { InMemoryLedger } from "../src/ledger/in-memory.js"; import { OperatorBackend } from "../src/index.js"; import { startHttpServer } from "../src/http/index.js"; -import { RegistryClient } from "@canton-dex/registry-client"; -import type { ChoiceContextRef, ContractId } from "@canton-dex/registry-client"; - -class StubRegistry extends RegistryClient { - constructor() { - super({ baseUrl: "http://stub" }); - } - override async getFactories() { - return { - allocationFactoryCid: "#alloc:0" as ContractId<"AllocationFactory">, - settlementFactoryCid: "#settle:0" as ContractId<"SettlementFactory">, - disclosure: [] as never[], - }; - } - override async getChoiceContext(): Promise { - return { context: { values: {} }, disclosure: [] }; - } -} +import { StubRegistry } from "./stub-registry.js"; let baseUrl: string; let close: () => Promise; @@ -46,10 +29,6 @@ before(async () => { operator: "op" as never, lpRegistrar: "lp" as never, admin: "ad" as never, - allocationFactoryCid: "#alloc:0", - settlementFactoryCid: "#settle:0", - allocationFactoryExtraArgs: { context: { values: {} }, meta: { values: {} } }, - allocationFactoryDisclosure: [], network: "canton:test", }, // Dev-open so the operator-auth gate does not 401 the write @@ -113,7 +92,7 @@ describe("HTTP input validation", () => { const body = r.body as { network: string; slot: number; synced: boolean }; assert.equal(typeof body.network, "string"); assert.equal(typeof body.slot, "number"); - assert.equal(typeof body.synced, "boolean"); + assert.equal(body.synced, true); }); it("GET /v1/context returns shaped context", async () => { diff --git a/services/operator-backend/tsconfig.live-scripts.json b/services/operator-backend/tsconfig.live-scripts.json new file mode 100644 index 00000000..3ea1125f --- /dev/null +++ b/services/operator-backend/tsconfig.live-scripts.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "declaration": false, + "noEmit": true, + "typeRoots": ["./node_modules/@types"] + }, + "include": [ + "../../scripts/bootstrap-registry.ts", + "../../scripts/live-amm-roundtrip.ts", + "../../scripts/seed-testnet-pool.ts", + "../../scripts/testnet-v2registry-trade.ts" + ] +} diff --git a/services/registry-client/src/index.ts b/services/registry-client/src/index.ts index 558c8c7e..346641b4 100644 --- a/services/registry-client/src/index.ts +++ b/services/registry-client/src/index.ts @@ -1,104 +1,122 @@ -// Registry client. Single integration point between the operator -// backend and an asset registrar's HTTP endpoints. -// -// Endpoints (matching docs/guides/choice-context.md): -// GET /registry/factories/:admin -// GET /registry/choice-context/:admin -// -// The client owns its caches. Operator modules use this boundary rather than -// calling registry endpoints directly, keeping validation and invalidation in -// one place. - -import { TtlCache } from "./cache.js"; +// Token Standard V2 registry client. Every lookup is operation-specific and +// carries the exact Daml JSON choice argument, as required by the upstream +// allocation/allocation-instruction OpenAPI. Choice contexts are deliberately +// not cached: the standard permits them to be specific to one exercise. + import { + ChoiceArguments, ChoiceContextRef, - FactoryRefs, + FactoryChoiceContextRef, Party, + RegistryDiscovery, RegistryError, + FactoryRefs, } from "./types.js"; import { validateChoiceContextRef, - validateFactoryRefs, + validateFactoryChoiceContextRef, } from "./validate.js"; export * from "./types.js"; export interface RegistryClientConfig { - baseUrl: string; + /** One registry URL, or a resolver for deployments listing several admins. */ + baseUrl: string | ((admin: Party) => string); authToken?: string; - choiceContextTtlMs?: number; /** Override fetch for tests. */ fetchImpl?: typeof fetch; } -export class RegistryClient { - private readonly factoryCache = new TtlCache( - (a) => `fac:${a}`, - ); - private readonly choiceContextCache = new TtlCache( - (a) => `ctx:${a}`, - ); +export class RegistryClient implements RegistryDiscovery { private readonly fetchImpl: typeof fetch; constructor(private readonly config: RegistryClientConfig) { this.fetchImpl = config.fetchImpl ?? fetch; } - async getFactories(admin: Party): Promise { - const cached = this.factoryCache.get(admin); - if (cached) return cached; - const refs = await this.fetchJson( - `/registry/factories/${encodeURIComponent(admin)}`, - validateFactoryRefs, + async getAllocationFactory( + admin: Party, + choiceArguments: ChoiceArguments, + ): Promise { + return this.requireJson( + admin, + "/registry/allocation-instruction/v2/allocation-factory", + { choiceArguments }, + validateFactoryChoiceContextRef, ); - if (!refs) { - throw new RegistryError("factory-stale", `admin=${admin}`, true); - } - this.factoryCache.set(admin, refs); - return refs; } - /** - * Off-ledger choice context for token-standard factory choices. - * Token-standard registries compute this (disclosed config contracts, - * featured-app rights, …) and the caller threads it into the choice's - * ExtraArgs. Registries that need no context may return 404; callers - * treat that as empty context + no disclosure. - */ - async getChoiceContext(admin: Party): Promise { - const cached = this.choiceContextCache.get(admin); - if (cached) return cached; - const ctx = - (await this.fetchJson( - `/registry/choice-context/${encodeURIComponent(admin)}`, - validateChoiceContextRef, - )) ?? { context: { values: {} }, disclosure: [] }; - this.choiceContextCache.set(admin, ctx, this.config.choiceContextTtlMs); - return ctx; + async getSettlementFactory( + admin: Party, + choiceArguments: ChoiceArguments, + ): Promise { + return this.requireJson( + admin, + "/registry/allocation/v2/settlement-factory", + { choiceArguments }, + validateFactoryChoiceContextRef, + ); + } + + async getAllocationCancelContext( + admin: Party, + allocationId: string, + meta: Record = {}, + ): Promise { + return this.requireJson( + admin, + `/registry/allocations/v2/${encodeURIComponent(allocationId)}/choice-contexts/cancel`, + { meta }, + validateChoiceContextRef, + ); } - invalidateAll(): void { - this.factoryCache.invalidateAll(); - this.choiceContextCache.invalidateAll(); + async getAllocationWithdrawContext( + admin: Party, + allocationId: string, + meta: Record = {}, + ): Promise { + return this.requireJson( + admin, + `/registry/allocations/v2/${encodeURIComponent(allocationId)}/choice-contexts/withdraw`, + { meta }, + validateChoiceContextRef, + ); } /** * Fetch + validate a registry response. `validate` turns the parsed JSON * into a checked `T`, throwing RegistryError("malformed", ...) on a shape * mismatch. Registry output is never trusted via a bare `as T` cast. - * Returns null on 404 (callers treat absent as empty/not-found). + * A missing canonical endpoint is an integration error, not permission to + * silently submit empty context. */ - private async fetchJson( + private async requireJson( + admin: Party, path: string, + body: Record, validate: (raw: unknown) => T, - ): Promise { - const url = new URL(path, this.config.baseUrl); - const headers: Record = { Accept: "application/json" }; + ): Promise { + const baseUrl = + typeof this.config.baseUrl === "function" + ? this.config.baseUrl(admin) + : this.config.baseUrl; + const url = new URL(path, baseUrl); + const headers: Record = { + Accept: "application/json", + "Content-Type": "application/json", + }; if (this.config.authToken) { headers.Authorization = `Bearer ${this.config.authToken}`; } - const res = await this.fetchImpl(url.toString(), { headers }); - if (res.status === 404) return null; + const res = await this.fetchImpl(url.toString(), { + method: "POST", + headers, + body: JSON.stringify(body), + }); + if (res.status === 404) { + throw new RegistryError("not-found", `${path}: admin=${admin}`, false); + } if (res.status === 401 || res.status === 403) { throw new RegistryError("auth", `status=${res.status}`, false); } @@ -122,3 +140,55 @@ export class RegistryClient { return validate(raw); } } + +/** + * Adapter for the repository's self-registry, whose factory CIDs are deployed + * and configured together with the operator. It implements the same + * operation-specific interface without exposing made-up HTTP endpoints. + */ +export class FixedRegistryClient implements RegistryDiscovery { + constructor( + private readonly factoriesForAdmin: (admin: Party) => FactoryRefs, + ) {} + + async getAllocationFactory( + admin: Party, + _choiceArguments: ChoiceArguments, + ): Promise { + const refs = this.factoriesForAdmin(admin); + return { + factoryCid: refs.allocationFactoryCid, + context: { values: {} }, + disclosure: refs.disclosure, + }; + } + + async getSettlementFactory( + admin: Party, + _choiceArguments: ChoiceArguments, + ): Promise { + const refs = this.factoriesForAdmin(admin); + return { + factoryCid: refs.settlementFactoryCid, + context: { values: {} }, + disclosure: refs.disclosure, + }; + } + + async getAllocationCancelContext( + _admin: Party, + _allocationId: string, + _meta: Record = {}, + ): Promise { + return { context: { values: {} }, disclosure: [] }; + } + + async getAllocationWithdrawContext( + _admin: Party, + _allocationId: string, + _meta: Record = {}, + ): Promise { + return { context: { values: {} }, disclosure: [] }; + } + +} diff --git a/services/registry-client/src/types.ts b/services/registry-client/src/types.ts index 9df1fd50..d696c8d2 100644 --- a/services/registry-client/src/types.ts +++ b/services/registry-client/src/types.ts @@ -1,5 +1,6 @@ -// Registry HTTP response shapes. A registry may use any on-ledger templates as -// long as its API returns these validated integration fields. +// Normalized Token Standard V2 registry HTTP shapes. The wire names follow the +// upstream OpenAPI (`factoryId`, `choiceContextData`, `disclosedContracts`); +// callers use the normalized names below when constructing Ledger API commands. export type Party = string; export type ContractId<_T> = string & { readonly __brand: unique symbol }; @@ -12,6 +13,9 @@ export interface FactoryRefs { disclosure: DisclosedContract[]; } +/** Daml JSON encoding of a choice argument, with empty `extraArgs`. */ +export type ChoiceArguments = Record; + export interface DisclosedContract { contractId: string; templateId: string; @@ -32,10 +36,43 @@ export interface ChoiceContextRef { disclosure: DisclosedContract[]; } +/** One operation-specific factory response from a V2 registry. */ +export interface FactoryChoiceContextRef extends ChoiceContextRef { + factoryCid: ContractId<"TokenStandardFactory">; +} + +/** + * The backend depends on this operation-specific surface rather than on a + * concrete HTTP client. Fixed self-registries can implement the same contract + * without inventing non-standard discovery endpoints. + */ +export interface RegistryDiscovery { + getAllocationFactory( + admin: Party, + choiceArguments: ChoiceArguments, + ): Promise; + getSettlementFactory( + admin: Party, + choiceArguments: ChoiceArguments, + ): Promise; + getAllocationCancelContext( + admin: Party, + allocationId: string, + meta?: Record, + ): Promise; + getAllocationWithdrawContext( + admin: Party, + allocationId: string, + meta?: Record, + ): Promise; +} + export type RegistryErrorKind = | "factory-stale" + | "not-found" | "transport" | "auth" + | "unsupported" // The response did not match the declared integration shape. | "malformed"; diff --git a/services/registry-client/src/validate.ts b/services/registry-client/src/validate.ts index ae251f90..574911e0 100644 --- a/services/registry-client/src/validate.ts +++ b/services/registry-client/src/validate.ts @@ -8,6 +8,7 @@ import { ChoiceContextRef, DisclosedContract, + FactoryChoiceContextRef, FactoryRefs, RegistryError, } from "./types.js"; @@ -70,16 +71,28 @@ export function validateFactoryRefs(v: unknown): FactoryRefs { } export function validateChoiceContextRef(v: unknown): ChoiceContextRef { - const w = "ChoiceContextRef"; + const w = "ChoiceContext"; const o = obj(v, w); - const ctx = obj(o.context, `${w}.context`); + const ctx = obj(o.choiceContextData, `${w}.choiceContextData`); if (typeof ctx.values !== "object" || ctx.values === null || Array.isArray(ctx.values)) { - fail(`${w}.context.values: expected object`); + fail(`${w}.choiceContextData.values: expected object`); } return { context: { values: ctx.values as Record }, - disclosure: arr(o, "disclosure", w).map((x) => - disclosedContract(x, `${w}.disclosure[]`), + disclosure: arr(o, "disclosedContracts", w).map((x) => + disclosedContract(x, `${w}.disclosedContracts[]`), ), }; } + +export function validateFactoryChoiceContextRef( + v: unknown, +): FactoryChoiceContextRef { + const w = "FactoryWithChoiceContext"; + const o = obj(v, w); + const choiceContext = validateChoiceContextRef(o.choiceContext); + return { + factoryCid: str(o, "factoryId", w) as FactoryChoiceContextRef["factoryCid"], + ...choiceContext, + }; +} diff --git a/trading-tests/CantonDex/Tests/ChoiceContextWorkflowTests.daml b/trading-tests/CantonDex/Tests/ChoiceContextWorkflowTests.daml new file mode 100644 index 00000000..5f81f16a --- /dev/null +++ b/trading-tests/CantonDex/Tests/ChoiceContextWorkflowTests.daml @@ -0,0 +1,340 @@ +-- | Registry choice-context forwarding through liquidity workflows. +-- +-- Read the allocation-context pair first, then the three split-admin settlement +-- tests. Context-requiring mock factories make missing or misrouted ExtraArgs +-- observable while keeping the suite focused on choreography. They still have +-- no holdings and therefore do NOT prove value movement. +-- Design context: `docs/concepts/design-tour.md#cross-registry-settlement`. +module CantonDex.Tests.ChoiceContextWorkflowTests where + +import DA.Assert +import DA.List (head, tail) +import DA.TextMap qualified as TextMap + +import Daml.Script + +import Splice.Api.Token.HoldingV2 qualified as V2 +import Splice.Api.Token.AllocationV2 qualified as V2 +import Splice.Api.Token.AllocationInstructionV2 qualified as V2 +import Splice.Api.Token.MetadataV1 +import Splice.Testing.Utils (emptyExtraArgs) + +import CantonDex.Lp.Policy qualified as LP +import CantonDex.Dex.Pool qualified as Pool +import CantonDex.Dex.PoolState qualified as PState +import CantonDex.Dex.PoolModel qualified as PM +import CantonDex.Dex.PoolLiquidityRules qualified as Dvp +import CantonDex.Dex.LiquidityAllocationRequest qualified as LAR +import CantonDex.Testing.MockRegistry qualified as Mock +import CantonDex.Tests.WorkflowTestFixtures + +-- Choice-context fixture ------------------------------------------------ +-- The mock factories can require a marker in their choice context. This makes +-- context forwarding observable without depending on an external registry. +data ContextPoolFixture = ContextPoolFixture with + fixturePoolId : Pool.PoolId + fixturePoolCid : ContractId Pool.Pool + fixtureStateCid : ContractId PState.PoolState + fixtureLiquidityRulesCid : ContractId Dvp.PoolLiquidityRules + fixturePolicyCid : ContractId LP.LPTokenPolicy + fixtureAllocationFactoryCid : ContractId V2.AllocationFactory + fixtureSettlementFactoryCid : ContractId V2.SettlementFactory + +mkPoolFixture : Party -> Party -> Party -> Bool -> Script ContextPoolFixture +mkPoolFixture operator lpRegistrar admin requireContext = do + factory <- submit admin $ createCmd Mock.MockAllocationFactory with + admin; users = [operator, lpRegistrar]; requireContext + settle <- submit admin $ createCmd Mock.MockSettlementFactory with + admin; users = [operator, lpRegistrar]; requireContext + let lpId = V2.InstrumentId with admin = lpRegistrar; id = "BTC-USDC-LP" + poolId = "BTC-USDC" + poolCid <- submit operator $ createCmd Pool.Pool with + poolId; operator; lpRegistrar; admin + baseInstrumentId = "BTC"; quoteInstrumentId = "USDC"; lpInstrumentId = lpId + feeBps = 30 + stateCid <- submit operator $ createCmd PState.PoolState with + poolId; operator; lpRegistrar + status = Pool.PS_Unfunded + reserves = Pool.PoolReserves with baseAmount = 0.0; quoteAmount = 0.0 + totalLpSupply = 0.0; publicReaders = [] + dvpCid <- submit (actAs [operator, lpRegistrar]) $ + createCmd Dvp.PoolLiquidityRules with operator; lpRegistrar + policyCid <- submit lpRegistrar $ createCmd LP.LPTokenPolicy with + lpRegistrar; operator; lpInstrumentId = lpId + totalSupply = 0.0; active = True + pure ContextPoolFixture with + fixturePoolId = poolId + fixturePoolCid = poolCid + fixtureStateCid = stateCid + fixtureLiquidityRulesCid = dvpCid + fixturePolicyCid = policyCid + fixtureAllocationFactoryCid = toInterfaceContractId factory + fixtureSettlementFactoryCid = toInterfaceContractId settle + +-- Marker required by a context-enabled mock factory. +markerContext : ExtraArgs +markerContext = ExtraArgs with + context = ChoiceContext with + values = TextMap.fromList [(Mock.dexChoiceContextKey, AV_Bool True)] + meta = emptyMetadata + +-- A DvP add succeeds when both factory calls receive their required context. +-- | Proves add-liquidity threads supplied context into both allocation calls. +testDvpAddThreadsChoiceContext : Script () +testDvpAddThreadsChoiceContext = do + operator <- allocateParty "operator" + lpRegistrar <- allocateParty "lp-registrar" + admin <- allocateParty "admin" + now <- getTime + fixture <- mkPoolFixture operator lpRegistrar admin True + let poolId = fixture.fixturePoolId + poolCid = fixture.fixturePoolCid + stateCid = fixture.fixtureStateCid + dvpCid = fixture.fixtureLiquidityRulesCid + policyCid = fixture.fixturePolicyCid + factoryCid = fixture.fixtureAllocationFactoryCid + settleCid = fixture.fixtureSettlementFactoryCid + res <- dvpFundPool operator lpRegistrar factoryCid settleCid poolId poolCid stateCid policyCid dvpCid lpRegistrar 10.0 200000.0 now markerContext + Some state <- queryContractId operator res.poolStateCid + state.status === Pool.PS_Active + +-- The same factory rejects an allocation when the context is omitted. +-- | Proves an empty allocation choice context is not silently substituted. +testDvpAddRejectsEmptyContext : Script () +testDvpAddRejectsEmptyContext = do + operator <- allocateParty "operator" + lpRegistrar <- allocateParty "lp-registrar" + admin <- allocateParty "admin" + now <- getTime + fixture <- mkPoolFixture operator lpRegistrar admin True + let poolCid = fixture.fixturePoolCid + dvpCid = fixture.fixtureLiquidityRulesCid + factoryCid = fixture.fixtureAllocationFactoryCid + reqCid <- submit operator $ exerciseCmd dvpCid Dvp.PoolLiquidityRules_RequestAddLiquidity with + poolCid; recipient = lpRegistrar + baseAmount = 10.0; quoteAmount = 200000.0 + lpAmount = PM.sqrtDecimal 2000000.0 + requestedAt = now; settleAt = None + Some req <- queryContractId operator reqCid + submitMustFail lpRegistrar $ + exerciseCmd factoryCid V2.AllocationFactory_Allocate with + settlement = req.settlement + allocation = head req.allocations + requestedAt = now + inputHoldingCids = [] + actors = [lpRegistrar] + extraArgs = emptyExtraArgs + +-- Each registry admin receives its own choice context ------------------- +-- +-- The DvP settle runs two per-admin batches in one transaction: +-- base/quote under pool.admin, LP mint/burn under pool.lpRegistrar. Each +-- batch must carry its own registry choice context. Driven through the +-- context-requiring MockRegistry, these prove poolAdminExtraArgs and +-- lpRegistrarExtraArgs are wired to distinct batches: blanking either one +-- aborts only that batch's allocate/settle. + +-- Author the three add allocations (base + quote deposit, LP mint receipt) +-- the LP signs, returning the request + allocation cids so a test can drive +-- SettleAddLiquidity with explicit per-admin choice contexts. +authorAddAllocations + : Party -> ContractId V2.AllocationFactory -> ContractId Pool.Pool + -> ContractId Dvp.PoolLiquidityRules -> Party -> Decimal -> Decimal -> Time -> ExtraArgs + -> Script ( ContractId LAR.LiquidityAllocationRequest + , ContractId V2.Allocation, ContractId V2.Allocation, ContractId V2.Allocation ) +authorAddAllocations operator factoryCid poolCid dvpCid recipient baseAmount quoteAmount now authorCtx = do + reqCid <- submit operator $ exerciseCmd dvpCid Dvp.PoolLiquidityRules_RequestAddLiquidity with + poolCid; recipient; baseAmount; quoteAmount + lpAmount = PM.sqrtDecimal (baseAmount * quoteAmount) + requestedAt = now; settleAt = None + Some req <- queryContractId operator reqCid + let mkOne spec = do + res <- submit recipient $ exerciseCmd factoryCid V2.AllocationFactory_Allocate with + settlement = req.settlement; allocation = spec; requestedAt = now + inputHoldingCids = []; actors = [recipient]; extraArgs = authorCtx + case res.output of + V2.AllocationInstructionResult_Completed cid -> pure cid + _ -> abort "author add alloc must complete" + baseCid <- mkOne (head req.allocations) + quoteCid <- mkOne (head (tail req.allocations)) + receiptCid <- mkOne (head (tail (tail req.allocations))) + pure (reqCid, baseCid, quoteCid, receiptCid) + +addPreparation + : ContextPoolFixture + -> ContractId LAR.LiquidityAllocationRequest + -> Party + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> Dvp.AddLiquidityPreparationArgs +addPreparation fixture reqCid recipient baseCid quoteCid receiptCid = + Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fixture.fixturePoolId + poolCid = fixture.fixturePoolCid + poolStateCid = fixture.fixtureStateCid + lpPolicyCid = fixture.fixturePolicyCid + requestCid = Some reqCid + acceptanceCid = None + recipient + lpBaseDepositCid = baseCid + lpQuoteDepositCid = quoteCid + lpReceiptCid = receiptCid + baseAmount = 10.0 + quoteAmount = 200000.0 + minLpTokens = 0.0 + knownTotalLpSupply = 0.0 + +stageAddAllocations + : Party + -> Party + -> ContextPoolFixture + -> Dvp.AddLiquidityPreparationArgs + -> Time + -> ExtraArgs + -> Script (ContractId V2.Allocation, ContractId V2.Allocation, ContractId V2.Allocation) +stageAddAllocations operator lpRegistrar fixture preparation requestedAt allocationContext = do + plan <- submit (actAs [operator, lpRegistrar]) $ + exerciseCmd fixture.fixtureLiquidityRulesCid + Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation + requestedAt + let allocate actor arg = do + result <- submit actor $ exerciseCmd fixture.fixtureAllocationFactoryCid + (arg with extraArgs = allocationContext) + case result.output of + V2.AllocationInstructionResult_Completed cid -> pure cid + _ -> abort "context fixture allocation must complete" + operatorBaseReceiverCid <- allocate operator plan.baseReceiver + operatorQuoteReceiverCid <- allocate operator plan.quoteReceiver + registrarMintCid <- allocate lpRegistrar plan.lpMintSender + pure (operatorBaseReceiverCid, operatorQuoteReceiverCid, registrarMintCid) + +settleAddCommand + : ContextPoolFixture + -> Dvp.AddLiquidityPreparationArgs + -> Time + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ExtraArgs + -> ExtraArgs + -> Commands Dvp.PoolLiquidityRules_SettleAddResult +settleAddCommand fixture preparation requestedAt + operatorBaseReceiverCid operatorQuoteReceiverCid registrarMintCid + poolAdminExtraArgs lpRegistrarExtraArgs = + exerciseCmd fixture.fixtureLiquidityRulesCid + Dvp.PoolLiquidityRules_SettleAddLiquidity with + expectedPoolId = preparation.expectedPoolId + poolCid = preparation.poolCid + poolStateCid = preparation.poolStateCid + lpPolicyCid = preparation.lpPolicyCid + requestCid = preparation.requestCid + acceptanceCid = preparation.acceptanceCid + recipient = preparation.recipient + lpBaseDepositCid = preparation.lpBaseDepositCid + lpQuoteDepositCid = preparation.lpQuoteDepositCid + lpReceiptCid = preparation.lpReceiptCid + baseFactoryCid = fixture.fixtureAllocationFactoryCid + quoteFactoryCid = fixture.fixtureAllocationFactoryCid + lpFactoryCid = fixture.fixtureAllocationFactoryCid + baseQuoteSettleCid = fixture.fixtureSettlementFactoryCid + lpSettleCid = fixture.fixtureSettlementFactoryCid + baseAmount = preparation.baseAmount + quoteAmount = preparation.quoteAmount + minLpTokens = preparation.minLpTokens + knownTotalLpSupply = preparation.knownTotalLpSupply + requestedAt + poolAdminExtraArgs + lpRegistrarExtraArgs + operatorBaseReceiverCid = Some operatorBaseReceiverCid + operatorQuoteReceiverCid = Some operatorQuoteReceiverCid + registrarMintCid = Some registrarMintCid + +-- Both per-admin contexts supplied: the context-requiring base/quote and LP +-- batches both settle, pool goes Active. +-- | Proves split-admin settle sends each registry its own choice context. +testDvpSettleThreadsBothAdminContexts : Script () +testDvpSettleThreadsBothAdminContexts = do + operator <- allocateParty "operator" + lpRegistrar <- allocateParty "lp-registrar" + admin <- allocateParty "admin" + now <- getTime + fixture <- mkPoolFixture operator lpRegistrar admin True + let poolId = fixture.fixturePoolId + poolCid = fixture.fixturePoolCid + stateCid = fixture.fixtureStateCid + dvpCid = fixture.fixtureLiquidityRulesCid + policyCid = fixture.fixturePolicyCid + factoryCid = fixture.fixtureAllocationFactoryCid + settleCid = fixture.fixtureSettlementFactoryCid + (reqCid, baseCid, quoteCid, receiptCid) <- + authorAddAllocations operator factoryCid poolCid dvpCid lpRegistrar 10.0 200000.0 now markerContext + let preparation = addPreparation fixture reqCid lpRegistrar baseCid quoteCid receiptCid + (operatorBaseReceiverCid, operatorQuoteReceiverCid, registrarMintCid) <- + stageAddAllocations operator lpRegistrar fixture preparation now markerContext + _settlementPlan <- submit (actAs [operator, lpRegistrar]) $ + exerciseCmd dvpCid Dvp.PoolLiquidityRules_PreviewAddSettlement with + preparation + operatorBaseReceiverCid + operatorQuoteReceiverCid + registrarMintCid + let settleAdd = settleAddCommand fixture preparation now + operatorBaseReceiverCid operatorQuoteReceiverCid registrarMintCid + res <- submit (actAs [operator, lpRegistrar]) $ settleAdd markerContext markerContext + Some state <- queryContractId operator res.poolStateCid + state.status === Pool.PS_Active + +-- Blanking poolAdminExtraArgs aborts the base/quote batch (its factory +-- requires context), proving that field feeds the pool.admin batch. +-- | Proves omitting only pool-admin context aborts the pool-admin batch. +testDvpSettleRequiresPoolAdminContext : Script () +testDvpSettleRequiresPoolAdminContext = do + operator <- allocateParty "operator" + lpRegistrar <- allocateParty "lp-registrar" + admin <- allocateParty "admin" + now <- getTime + fixture <- mkPoolFixture operator lpRegistrar admin True + let poolId = fixture.fixturePoolId + poolCid = fixture.fixturePoolCid + stateCid = fixture.fixtureStateCid + dvpCid = fixture.fixtureLiquidityRulesCid + policyCid = fixture.fixturePolicyCid + factoryCid = fixture.fixtureAllocationFactoryCid + settleCid = fixture.fixtureSettlementFactoryCid + (reqCid, baseCid, quoteCid, receiptCid) <- + authorAddAllocations operator factoryCid poolCid dvpCid lpRegistrar 10.0 200000.0 now markerContext + let preparation = addPreparation fixture reqCid lpRegistrar baseCid quoteCid receiptCid + (operatorBaseReceiverCid, operatorQuoteReceiverCid, registrarMintCid) <- + stageAddAllocations operator lpRegistrar fixture preparation now markerContext + submitMustFail (actAs [operator, lpRegistrar]) $ + settleAddCommand fixture preparation now + operatorBaseReceiverCid operatorQuoteReceiverCid registrarMintCid + emptyExtraArgs markerContext + +-- Blanking lpRegistrarExtraArgs aborts the LP mint batch, proving that +-- field feeds the pool.lpRegistrar batch. +-- | Proves omitting only LP-registrar context aborts the LP batch. +testDvpSettleRequiresLpRegistrarContext : Script () +testDvpSettleRequiresLpRegistrarContext = do + operator <- allocateParty "operator" + lpRegistrar <- allocateParty "lp-registrar" + admin <- allocateParty "admin" + now <- getTime + fixture <- mkPoolFixture operator lpRegistrar admin True + let poolId = fixture.fixturePoolId + poolCid = fixture.fixturePoolCid + stateCid = fixture.fixtureStateCid + dvpCid = fixture.fixtureLiquidityRulesCid + policyCid = fixture.fixturePolicyCid + factoryCid = fixture.fixtureAllocationFactoryCid + settleCid = fixture.fixtureSettlementFactoryCid + (reqCid, baseCid, quoteCid, receiptCid) <- + authorAddAllocations operator factoryCid poolCid dvpCid lpRegistrar 10.0 200000.0 now markerContext + let preparation = addPreparation fixture reqCid lpRegistrar baseCid quoteCid receiptCid + (operatorBaseReceiverCid, operatorQuoteReceiverCid, registrarMintCid) <- + stageAddAllocations operator lpRegistrar fixture preparation now markerContext + submitMustFail (actAs [operator, lpRegistrar]) $ + settleAddCommand fixture preparation now + operatorBaseReceiverCid operatorQuoteReceiverCid registrarMintCid + markerContext emptyExtraArgs diff --git a/trading-tests/CantonDex/Tests/DexPairTests.daml b/trading-tests/CantonDex/Tests/DexPairTests.daml new file mode 100644 index 00000000..d26d85d1 --- /dev/null +++ b/trading-tests/CantonDex/Tests/DexPairTests.daml @@ -0,0 +1,112 @@ +-- | Focused executable documentation for the DexPair listing record. +-- +-- DexPair is operator-owned venue metadata. Its update choices recreate the +-- listing, but `active` and `tradingMode` are not on-ledger gates for PoolRules +-- or OrderMatchExecution. These tests therefore prove listing state, +-- authorization, visibility, and fee-accounting only. +-- Design context: `docs/concepts/design-tour.md#pair-and-governance-state`. +module CantonDex.Tests.DexPairTests where + +import DA.Assert +import DA.Optional (fromSome) +import DA.TextMap qualified as TextMap +import Daml.Script + +import CantonDex.Dex.DexPair + +initialFeeModel : FeeModel +initialFeeModel = FeeModel with + makerFeeBps = 10 + takerFeeBps = 20 + poolFeeBps = 30 + +createPair : Party -> Party -> Script (ContractId DexPair) +createPair operator admin = + submit operator $ createCmd DexPair with + operator + admin + baseInstrumentId = "BTC" + quoteInstrumentId = "USDC" + tradingMode = TM_Both + feeModel = initialFeeModel + active = True + publicReaders = None + accumulatedMakerFees = None + accumulatedTakerFees = None + +-- Each listing update is consuming and should preserve unrelated fields. +-- | Proves operator-controlled DexPair updates recreate and preserve the listing. +testDexPairLifecycleUpdates : Script () +testDexPairLifecycleUpdates = do + operator <- allocateParty "pair-operator" + admin <- allocateParty "pair-admin" + reader <- allocateParty "pair-reader" + pair0 <- createPair operator admin + + let replacementFees = FeeModel with + makerFeeBps = 5 + takerFeeBps = 15 + poolFeeBps = 25 + + -- Fee rates are basis-point fractions and can never be negative or reach + -- 100%; a failed update leaves the original pair active. + submitMustFail operator $ exerciseCmd pair0 DexPair_UpdateFeeModel with + newFeeModel = replacementFees with takerFeeBps = 10000 + pair1 <- submit operator $ exerciseCmd pair0 DexPair_UpdateFeeModel with + newFeeModel = replacementFees + pair2 <- submit operator $ exerciseCmd pair1 DexPair_SetActive with + newActive = False + pair3 <- submit operator $ exerciseCmd pair2 DexPair_UpdateTradingMode with + newTradingMode = TM_Pool + pair4 <- submit operator $ exerciseCmd pair3 DexPair_UpdatePublicReaders with + newReaders = [reader] + + Some pair <- queryContractId reader pair4 + pair.baseInstrumentId === "BTC" + pair.quoteInstrumentId === "USDC" + pair.feeModel === replacementFees + pair.active === False + pair.tradingMode === TM_Pool + pair.publicReaders === Some [reader] + + -- Every update is consuming: only the newest listing remains active. + None <- queryContractId operator pair0 + None <- queryContractId operator pair1 + None <- queryContractId operator pair2 + None <- queryContractId operator pair3 + pure () + +-- | Proves the registry admin may observe a listing but cannot mutate it. +testDexPairUpdatesRequireOperator : Script () +testDexPairUpdatesRequireOperator = do + operator <- allocateParty "pair-auth-operator" + admin <- allocateParty "pair-auth-admin" + pairCid <- createPair operator admin + + submitMustFail admin $ exerciseCmd pairCid DexPair_SetActive with + newActive = False + Some pair <- queryContractId operator pairCid + pair.active === True + +-- The counters are accounting records, not holdings or a fee-collection path. +-- | Proves DexPair fee counters accumulate without pretending to move assets. +testDexPairRecordsMatchedTradeFees : Script () +testDexPairRecordsMatchedTradeFees = do + operator <- allocateParty "pair-fee-operator" + admin <- allocateParty "pair-fee-admin" + pair0 <- createPair operator admin + + submitMustFail operator $ exerciseCmd pair0 DexPair_RecordMatchedTrade with + legNotionals = TextMap.fromList [("BTC", -1.0)] + pair1 <- submit operator $ exerciseCmd pair0 DexPair_RecordMatchedTrade with + legNotionals = TextMap.fromList [("BTC", 2.0), ("USDC", 100.0)] + pair2 <- submit operator $ exerciseCmd pair1 DexPair_RecordMatchedTrade with + legNotionals = TextMap.fromList [("BTC", 3.0)] + + Some pair <- queryContractId operator pair2 + let maker = fromSome pair.accumulatedMakerFees + taker = fromSome pair.accumulatedTakerFees + TextMap.lookup "BTC" maker === Some 0.005 + TextMap.lookup "USDC" maker === Some 0.1 + TextMap.lookup "BTC" taker === Some 0.01 + TextMap.lookup "USDC" taker === Some 0.2 diff --git a/trading-tests/CantonDex/Tests/EndToEndTests.daml b/trading-tests/CantonDex/Tests/EndToEndTests.daml deleted file mode 100644 index 09f92057..00000000 --- a/trading-tests/CantonDex/Tests/EndToEndTests.daml +++ /dev/null @@ -1,1401 +0,0 @@ --- | Workflow-integration tests against MockRegistry. --- --- This suite proves DEX contract choreography: which party may take each step, --- which contracts are consumed or created, and how pool, order, RFQ, and OTC --- state advances. MockRegistry does not model holdings, so this file does NOT --- prove value conservation. Tests backed by real holdings live in --- PoolLiquidityRulesTests, RegistryConservationTests, RfqSettlementTests, and --- RealRegistryDvpTests. --- --- Suggested reading order: --- 1. testPoolFullLifecycle --- 2. testOrderFundingFlow --- 3. testRfqAcceptProducesMatchedTradeWithReceipt --- 4. testPoolSwapViaRequestSwap --- 5. testMatchedTradeFullSettle --- Design context: `docs/concepts/design-tour.md`. -module CantonDex.Tests.EndToEndTests where - -import DA.Assert -import DA.List (head, sort, tail) -import DA.Map qualified as Map -import DA.Optional (isSome, isNone, fromSome) -import DA.TextMap qualified as TextMap -import DA.Time - -import Daml.Script - -import Splice.Api.Token.HoldingV2 qualified as V2 -import Splice.Api.Token.AllocationV2 qualified as V2 -import Splice.Api.Token.AllocationInstructionV2 qualified as V2 -import Splice.Api.Token.AllocationInstructionV2 qualified as AllocationInstructionV2 -import Splice.Api.Token.AllocationRequestV2 qualified as V2 -import Splice.Api.Token.MetadataV1 -import Splice.Testing.Utils (emptyExtraArgs) - -import CantonDex.Lp.Policy qualified as LP -import CantonDex.Dex.MatchedTrade qualified as MT -import CantonDex.Dex.Order qualified as Order -import CantonDex.Dex.OrderFundingRequest qualified as OFR -import CantonDex.Dex.OrderMatchExecution qualified as OME -import CantonDex.Dex.Pool qualified as Pool -import CantonDex.Dex.PoolState qualified as PState -import CantonDex.Dex.PoolRules qualified as PRules -import CantonDex.Dex.PoolModel qualified as PM -import CantonDex.Dex.PoolLiquidityRules qualified as Dvp -import CantonDex.Dex.LiquidityAllocationRequest qualified as LAR -import CantonDex.Dex.PolicyReceipt qualified as PR -import CantonDex.Dex.Rfq qualified as Rfq -import CantonDex.Trading.Utils qualified as Utils -import CantonDex.Trading.WorkflowConstructors qualified as WC -import CantonDex.Testing.MockRegistry qualified as Mock - --- Create the split pool contracts the operator uses for swap + DvP --- liquidity, plus the LP policy. -data PoolSetup = PoolSetup with - setupPoolId : Pool.PoolId - setupPoolCid : ContractId Pool.Pool - setupStateCid : ContractId PState.PoolState - setupRulesCid : ContractId PRules.PoolRules - setupLiquidityRulesCid : ContractId Dvp.PoolLiquidityRules - setupPolicyCid : ContractId LP.LPTokenPolicy - -setupPool : Party -> Party -> Party -> Script PoolSetup -setupPool operator lpRegistrar admin = do - -- One BTC/USDC pool. The pool mints its own "BTC-USDC-LP" share token to - -- liquidity providers, issued by the LP registrar. Fee is 0.30% (30 bps). - let poolId = "BTC-USDC" - lpInstrumentId = V2.InstrumentId with admin = lpRegistrar; id = "BTC-USDC-LP" - -- Pool: the static config (the two sides, who runs it, the fee). - poolCid <- submit operator $ createCmd Pool.Pool with - poolId - operator - lpRegistrar - admin - baseInstrumentId = "BTC" - quoteInstrumentId = "USDC" - lpInstrumentId - feeBps = 30 - -- PoolState: the live balances. Starts empty and Unfunded (no reserves, - -- no LP shares issued yet); the first deposit flips it to Active. - stateCid <- submit operator $ createCmd PState.PoolState with - poolId - operator - lpRegistrar - status = Pool.PS_Unfunded - reserves = Pool.PoolReserves with baseAmount = 0.0; quoteAmount = 0.0 - totalLpSupply = 0.0 - publicReaders = [] - -- PoolRules: the swap choices (trade against the pool). - rulesCid <- submit operator $ createCmd PRules.PoolRules with operator - -- PoolLiquidityRules: the add/remove-liquidity choices, run jointly by the - -- operator and the LP registrar (both signatures are required). - dvpCid <- submit (actAs [operator, lpRegistrar]) $ - createCmd Dvp.PoolLiquidityRules with operator; lpRegistrar - -- LPTokenPolicy: tracks how many LP shares exist across all providers. - policyCid <- submit lpRegistrar $ createCmd LP.LPTokenPolicy with - lpRegistrar - operator - lpInstrumentId - totalSupply = 0.0 - active = True - pure PoolSetup with - setupPoolId = poolId - setupPoolCid = poolCid - setupStateCid = stateCid - setupRulesCid = rulesCid - setupLiquidityRulesCid = dvpCid - setupPolicyCid = policyCid - --- Stand up the stand-in token registry: an allocation factory (which turns a --- request to set aside funds into a locked allocation) and a settlement --- factory (which atomically swaps the locked allocations). These replace a --- real token issuer so the trading flows can be exercised in isolation. -setupRegistries : Party -> [Party] -> Script ( ContractId V2.AllocationFactory - , ContractId V2.SettlementFactory - ) -setupRegistries admin users = do - factoryCid <- submit admin $ createCmd Mock.MockAllocationFactory with admin; users; requireContext = False - settleCid <- submit admin $ createCmd Mock.MockSettlementFactory with admin; users; requireContext = False - pure (toInterfaceContractId factoryCid, toInterfaceContractId settleCid) - --- Deposit liquidity into a pool the honest way: delivery-versus-payment (DvP), --- meaning the base and quote go in and the LP shares come out as one --- all-or-nothing swap. Steps: the operator opens the request, the depositor --- sets aside the two deposits plus the mint receipt as locked allocations, --- then operator and LP registrar jointly settle all three at once. The pool's --- LP shares are the square root of base*quote, the standard constant-product --- rule. -dvpFundPool - : Party - -> Party - -> ContractId V2.AllocationFactory - -> ContractId V2.SettlementFactory - -> Pool.PoolId - -> ContractId Pool.Pool - -> ContractId PState.PoolState - -> ContractId LP.LPTokenPolicy - -> ContractId Dvp.PoolLiquidityRules - -> Party - -> Decimal - -> Decimal - -> Time - -> ExtraArgs - -> Script Dvp.PoolLiquidityRules_SettleAddResult -dvpFundPool operator lpRegistrar factoryCid settleCid poolId poolCid stateCid policyCid dvpCid recipient baseAmount quoteAmount now extraArgs = do - let lpAmount = PM.sqrtDecimal (baseAmount * quoteAmount) - -- Operator opens the add-liquidity request; it lists the three legs the - -- depositor must fund: base deposit, quote deposit, and the LP-mint receipt. - reqCid <- submit operator $ exerciseCmd dvpCid Dvp.PoolLiquidityRules_RequestAddLiquidity with - poolCid; recipient; baseAmount; quoteAmount; lpAmount; requestedAt = now; settleAt = None - Some req <- queryContractId operator reqCid - -- The three legs, in order, and a helper that locks up each one via the - -- allocation factory (the mock completes instantly instead of pending). - let baseSpec = head req.allocations - quoteSpec = head (tail req.allocations) - receiptSpec = head (tail (tail req.allocations)) - mkOne spec = do - res <- submit recipient $ exerciseCmd factoryCid V2.AllocationFactory_Allocate with - settlement = req.settlement - allocation = spec - requestedAt = now - inputHoldingCids = [] - actors = [recipient] - extraArgs - case res.output of - V2.AllocationInstructionResult_Completed cid -> pure cid - _ -> abort "Mock factory should complete immediately" - baseAllocCid <- mkOne baseSpec - quoteAllocCid <- mkOne quoteSpec - receiptAllocCid <- mkOne receiptSpec - submit (actAs [operator, lpRegistrar]) $ - exerciseCmd dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = poolId - poolCid - poolStateCid = stateCid - lpPolicyCid = policyCid - requestCid = Some reqCid; acceptanceCid = None - recipient - lpBaseDepositCid = baseAllocCid - lpQuoteDepositCid = quoteAllocCid - lpReceiptCid = receiptAllocCid - baseFactoryCid = factoryCid - quoteFactoryCid = factoryCid - lpFactoryCid = factoryCid - baseQuoteSettleCid = settleCid - lpSettleCid = settleCid - baseAmount - quoteAmount - minLpTokens = 0.0 - knownTotalLpSupply = 0.0 - requestedAt = now - poolAdminExtraArgs = extraArgs - lpRegistrarExtraArgs = extraArgs - --- DvP add activates an unfunded pool ------------------------------------ --- --- Depositing 10 BTC and 200,000 USDC activates the pool, records those exact --- reserves, and mints sqrt(base*quote) LP shares. - --- | Proves pool creation, first funding, pause, and resume state transitions. -testPoolFullLifecycle : Script () -testPoolFullLifecycle = do - -- Roles: the venue operator, the LP-share registrar, the token admin, and - -- Alice, who supplies the liquidity. - operator <- allocateParty "operator" - lpRegistrar <- allocateParty "lp-registrar" - admin <- allocateParty "admin" - alice <- allocateParty "alice" - now <- getTime - - -- Stand up the mock registry and an empty BTC/USDC pool, then have Alice - -- deposit through the honest DvP path. - (factoryCid, _settleCid) <- setupRegistries admin [operator, lpRegistrar, alice] - setup <- setupPool operator lpRegistrar admin - let poolId = setup.setupPoolId - poolCid = setup.setupPoolCid - stateCid = setup.setupStateCid - dvpCid = setup.setupLiquidityRulesCid - policyCid = setup.setupPolicyCid - initRes <- dvpFundPool - operator - lpRegistrar - factoryCid - _settleCid - poolId - poolCid - stateCid - policyCid - dvpCid - alice - 10.0 - 200000.0 - now - emptyExtraArgs - - -- Check the outcome: shares minted match the formula, the pool is now - -- Active, and its reserves and total shares equal exactly what went in. - Some state <- queryContractId operator initRes.poolStateCid - Some _baseSlice <- queryContractId operator initRes.baseSliceCid - Some _quoteSlice <- queryContractId operator initRes.quoteSliceCid - let expectedLp = PM.sqrtDecimal 2000000.0 - initRes.lpTokensMinted === expectedLp - state.status === Pool.PS_Active - state.reserves.baseAmount === 10.0 - state.reserves.quoteAmount === 200000.0 - state.totalLpSupply === expectedLp - - pure () - --- Accepting a trade allocation request consumes it ---------------------- --- --- A TradeAllocationRequest implements the standard V2 AllocationRequest --- interface. Accept is the holder's acknowledgement of the allocation --- specification; it consumes the request. The wallet composes Accept with the --- corresponding AllocationFactory_Allocate command in the real trade flow. - --- | Proves accepting a trade allocation request consumes that one-shot request. -testTradeAllocationRequestAcceptArchivesRequest : Script () -testTradeAllocationRequestAcceptArchivesRequest = do - operator <- allocateParty "operator" - admin <- allocateParty "admin" - alice <- allocateParty "alice" - bob <- allocateParty "bob" - now <- getTime - - -- Arrange: one requested leg moves 10 BTC from Alice to Bob, with the - -- operator as settlement executor. - let aliceAccount = Utils.basicAccount alice - bobAccount = Utils.basicAccount bob - leg = V2.TransferLeg with - transferLegId = "leg-1" - sender = aliceAccount - receiver = bobAccount - amount = 10.0 - instrumentId = "BTC" - meta = emptyMetadata - settlement = V2.SettlementInfo with - executors = [operator] - id = "test-trade" - cid = None - meta = emptyMetadata - - reqCid <- submit operator $ createCmd MT.TradeAllocationRequest with - authorizer = aliceAccount - admin - settlement - settlementDeadline = Some (addRelTime now (hours 1)) - transferLegs = [leg] - requestedAt = now - - -- Act: Alice accepts through the standard interface. - let reqIfaceCid : ContractId V2.AllocationRequest = toInterfaceContractId reqCid - acceptRes <- submit alice $ exerciseCmd reqIfaceCid V2.AllocationRequest_Accept with - actors = [alice] - extraArgs = emptyExtraArgs - -- Assert: Accept returns the request metadata and consumes the request. - acceptRes.meta === emptyMetadata - None <- queryContractId alice reqCid - - pure () - --- The trader funds a pending order -------------------------------------- --- --- Contract transitions under test: --- OrderFundingRequest --Bind--> Pending Order + OrderAllocationRequest --- trader --Allocate--> committed V2.Allocation --- Pending Order --Fund--> Funded Order (request consumed) --- The trader, not the operator, locks the funds. The operator can only attach --- that allocation to the pending order. - --- | Proves trader funding binds the exact requested allocation to an order. -testOrderFundingFlow : Script () -testOrderFundingFlow = do - operator <- allocateParty "operator" - admin <- allocateParty "admin" - alice <- allocateParty "alice" - now <- getTime - - (factoryCid, _) <- setupRegistries admin [operator, alice] - - -- Arrange: Alice asks to place a bid for 0.5 BTC at 60,000 USDC. - reqCid <- submit alice $ createCmd OFR.OrderFundingRequest with - operator - trader = alice - admin - baseInstrumentId = "BTC" - quoteInstrumentId = "USDC" - side = Order.Bid - limitPrice = 60000.0 - quantity = 0.5 - expiry = Some (addRelTime now (hours 1)) - - -- Act 1: the operator turns the intent into a pending market object and a - -- standard allocation request describing the required funding. - bindRes <- submit operator $ exerciseCmd reqCid OFR.OrderFundingRequest_Bind with - settlementRef = "ord-test-1" - - -- Assert the intermediate state before any funds are locked. - Some order <- queryContractId operator bindRes.orderCid - order.status === Order.OS_Pending - Some allocReq <- queryContractId operator bindRes.allocationRequestCid - - -- Act 2: Alice's wallet authors the committed funding allocation under her - -- own authority. The request remains live as the correlation record until - -- the operator binds the resulting allocation. - let aliceAccount = Utils.basicAccount alice - settlement = fromSome allocReq.allocationSettlement - specification = Order.orderFundingSpecification - admin alice allocReq.lockInstrumentId allocReq.lockAmount - allocReq.expiry - allocateArg = V2.AllocationFactory_Allocate with - settlement - allocation = specification - requestedAt = now - inputHoldingCids = [] - actors = [alice] - extraArgs = emptyExtraArgs - - instrResult <- submit alice $ exerciseCmd factoryCid allocateArg - case instrResult.output of - V2.AllocationInstructionResult_Completed allocCid -> do - -- Funding without the request correlation is rejected; otherwise the - -- request would remain live after the order becomes funded. - submitMustFail operator $ exerciseCmd bindRes.orderCid Order.Order_Fund with - allocationCid = allocCid - allocationRequestCid = None - -- Act 3: the operator binds Alice's allocation. Order_Fund consumes both - -- the pending order and its allocation request, then creates the funded - -- successor. - fundRes <- submit operator $ exerciseCmd bindRes.orderCid Order.Order_Fund with - allocationCid = allocCid - allocationRequestCid = Some bindRes.allocationRequestCid - Some funded <- queryContractId operator fundRes.orderCid - funded.status === Order.OS_Funded - funded.allocationCid === Some allocCid - pendingGone <- queryContractId operator bindRes.orderCid - assertMsg "pending Order consumed by Order_Fund" (isNone pendingGone) - reqGone <- queryContractId operator bindRes.allocationRequestCid - assertMsg "OrderAllocationRequest consumed by Order_Fund" (isNone reqGone) - _ -> abort "Mock factory should complete immediately" - - pure () - --- RFQ acceptance records the ranking decision --------------------------- --- --- RFQ = request-for-quote: the trader asks dealers to bid, then accepts one. --- Asserts that when three dealers quote and the trader accepts, the winner is --- ranked by the stated policy (trusted tier, later expiry, earlier posting, --- dealer id), a trade is created, and it carries a signed receipt proving that --- ranking. Price is displayed in the receipt but is not a policy sort key. - --- | Proves RFQ acceptance selects a quote and records its policy receipt. -testRfqAcceptProducesMatchedTradeWithReceipt : Script () -testRfqAcceptProducesMatchedTradeWithReceipt = do - operator <- allocateParty "operator" - admin <- allocateParty "admin" - alice <- allocateParty "alice" - orca <- allocateParty "orca-mm" - jump <- allocateParty "jump-tr" - galaxy <- allocateParty "galaxy-otc" - now <- getTime - - let expiresAt = addRelTime now (hours 1) - rfqId = "rfq-test-001" - - -- Alice creates an RFQ. - rfqCid <- submit alice $ createCmd Rfq.Rfq with - trader = alice - operator - rfqId - pair = "BTC/USDC" - side = Rfq.RFQ_Buy - size = 5.0 - expiresAt - whitelist = [orca, jump, galaxy] - createdAt = now - - -- Three dealers post quotes. Trusted dealers rank above whitelisted dealers; - -- Jump and Orca have equal expiry, so Jump's earlier posting ranks first. - quoteOrca <- submit orca $ createCmd Rfq.RfqQuote with - dealer = orca - trader = alice - operator - rfqId - price = 60530.00 - expiresAt = addRelTime now (seconds 30) - postedAt = now - tier = Rfq.TierTrusted - - quoteJump <- submit jump $ createCmd Rfq.RfqQuote with - dealer = jump - trader = alice - operator - rfqId - price = 60510.00 - expiresAt = addRelTime now (seconds 30) - postedAt = addRelTime now (seconds (-3)) - tier = Rfq.TierTrusted - - quoteGalaxy <- submit galaxy $ createCmd Rfq.RfqQuote with - dealer = galaxy - trader = alice - operator - rfqId - price = 60509.50 - expiresAt = addRelTime now (seconds 30) - postedAt = addRelTime now (seconds 8) - tier = Rfq.TierWhitelist - - -- Alice + operator jointly exercise Rfq_Accept (operator's authority - -- is needed because the resulting MatchedTrade is operator-signed). - -- Accept Jump's quote. It ranks first by tier and posting time even though - -- Galaxy has a marginally lower price. - acceptRes <- submit (actAs [alice, operator]) $ - exerciseCmd rfqCid Rfq.Rfq_Accept with - acceptedQuoteCid = quoteJump - consideredQuoteCids = [quoteOrca, quoteJump, quoteGalaxy] - admin - currentTime = now - signature = "0xtest-signature" - - -- Verify the receipt: - let r = acceptRes.receipt - r.acceptedDealer === jump - r.acceptedRank === 1 - r.consideredCount === 3 - PR.isWellFormed r === True - - -- Verify the MatchedTrade exists and carries the receipt. - Some trade <- queryContractId operator acceptRes.tradeCid - case trade.policyReceipt of - Some embedded -> embedded === r - None -> abort "Trade should carry a policy receipt" - - pure () - --- The operator expires stale RFQs --------------------------------------- --- --- Only the operator can sweep an RFQ, and only after its deadline. --- | Proves RFQ expiry cleanup is operator-only and deadline-gated. -testRfqExpireOperatorCleanup : Script () -testRfqExpireOperatorCleanup = do - operator <- allocateParty "operator-exp" - alice <- allocateParty "alice-exp" - orca <- allocateParty "orca-exp" - now <- getTime - - let expiresAt = addRelTime now (hours 1) - - rfqCid <- submit alice $ createCmd Rfq.Rfq with - trader = alice - operator - rfqId = "rfq-expire-001" - pair = "BTC/USDC" - side = Rfq.RFQ_Buy - size = 1.0 - expiresAt - whitelist = [orca] - createdAt = now - - -- Before the deadline the operator cannot expire it. - submitMustFail operator $ exerciseCmd rfqCid Rfq.Rfq_Expire with - currentTime = now - - -- The trader is not the controller of Rfq_Expire. - submitMustFail alice $ exerciseCmd rfqCid Rfq.Rfq_Expire with - currentTime = addRelTime now (hours 2) - - -- After the deadline the operator sweeps it. - submit operator $ exerciseCmd rfqCid Rfq.Rfq_Expire with - currentTime = addRelTime now (hours 2) - - remaining <- queryContractId operator rfqCid - remaining === None - --- PoolRules_Swap updates pool inventory --------------------------------- --- --- Trader authorizes the exact two-sided allocation via the mock factory; --- operator drives PoolRules_Swap which adjusts both --- pool allocations + the trader allocation and batch-settles them. --- --- Verifies: --- - the swap completes without error --- - reserves update correctly --- - the head pool allocation is replaced by its next-iteration CID --- - other pool allocations on the same side stay untouched - --- | Proves a pool swap settles and rewrites only the consumed reserve slices. -testPoolSwapEndToEnd : Script () -testPoolSwapEndToEnd = do - operator <- allocateParty "operator-swap" - lpRegistrar <- allocateParty "lp-registrar-swap" - admin <- allocateParty "admin-swap" - alice <- allocateParty "alice-swap" -- LP - bob <- allocateParty "bob-swap" -- swapper - now <- getTime - - (factoryCid, settleCid) <- setupRegistries admin [operator, lpRegistrar, alice, bob] - - -- Fund the pool through the live DvP add path. - setup <- setupPool operator lpRegistrar admin - let poolId = setup.setupPoolId - poolCid = setup.setupPoolCid - stateCid = setup.setupStateCid - rulesCid = setup.setupRulesCid - dvpCid = setup.setupLiquidityRulesCid - policyCid = setup.setupPolicyCid - initRes <- dvpFundPool - operator - lpRegistrar - factoryCid - settleCid - poolId - poolCid - stateCid - policyCid - dvpCid - alice - 10.0 - 200000.0 - now - emptyExtraArgs - - let bobAccount = Utils.basicAccount bob - quoteBinding = Some (PRules.SwapQuoteBinding with - expectedPoolId = poolId - poolStateCid = initRes.poolStateCid - inputSliceCid = initRes.quoteSliceCid - outputSliceCids = [initRes.baseSliceCid] - minOutputAmount = 0.0) - reqRes <- submit operator $ exerciseCmd rulesCid PRules.PoolRules_RequestSwap with - poolCid - swapper = bob - inputInstrumentId = "USDC" - inputAmount = 100.0 - quoteBinding - let bobAllocateArg = WC.mkAllocationFactoryAllocate - reqRes.settlement reqRes.allocationSpec now [] [bob] emptyExtraArgs - - bobInstr <- submit bob $ exerciseCmd factoryCid bobAllocateArg - bobAllocationCid <- case bobInstr.output of - V2.AllocationInstructionResult_Completed cid -> pure cid - _ -> abort "swap allocation must complete" - - -- Operator drives the swap: input USDC into the quote slice, source - -- BTC out from the base slice. - swapRes <- submit operator $ exerciseCmd rulesCid PRules.PoolRules_Swap with - expectedPoolId = poolId - poolCid - poolStateCid = initRes.poolStateCid - swapperAccount = bobAccount - inputInstrumentId = "USDC" - inputAmount = 100.0 - minOutputAmount = 0.0 - swapperAllocationCid = bobAllocationCid - inputSliceCid = initRes.quoteSliceCid - outputSliceCids = [initRes.baseSliceCid] - factoryCid = settleCid - extraArgs = emptyExtraArgs - quoteBinding = reqRes.quoteBinding - - -- Verify reserves moved. - Some state <- queryContractId operator swapRes.poolStateCid - assertMsg "base reserve decreased" (state.reserves.baseAmount < 10.0) - assertMsg "quote reserve increased" (state.reserves.quoteAmount > 200000.0) - -- Input slice (quote) grew; output slice (base) re-allocated as boundary. - Some _newQuote <- queryContractId operator swapRes.inputSliceCid - assertMsg "boundary output slice produced" (isSome swapRes.boundaryOutputSliceCid) - -- The output amount is positive. - assertMsg "swap produced output" (swapRes.amountOut > 0.0) - pure () - --- PoolRules_RequestSwap produces a settleable allocation spec ------------ --- --- The operator builds the allocation specification, the trader authors that --- exact allocation, and PoolRules_Swap settles it against the reserves. - --- | Proves the trader-signed request specification settles without mutation. -testPoolSwapViaRequestSwap : Script () -testPoolSwapViaRequestSwap = do - operator <- allocateParty "operator-rswap" - lpRegistrar <- allocateParty "lp-registrar-rswap" - admin <- allocateParty "admin-rswap" - alice <- allocateParty "alice-rswap" -- LP - bob <- allocateParty "bob-rswap" -- swapper - now <- getTime - - (factoryCid, settleCid) <- setupRegistries admin [operator, lpRegistrar, alice, bob] - - setup <- setupPool operator lpRegistrar admin - let poolId = setup.setupPoolId - poolCid = setup.setupPoolCid - stateCid = setup.setupStateCid - rulesCid = setup.setupRulesCid - dvpCid = setup.setupLiquidityRulesCid - policyCid = setup.setupPolicyCid - initRes <- dvpFundPool - operator - lpRegistrar - factoryCid - settleCid - poolId - poolCid - stateCid - policyCid - dvpCid - alice - 10.0 - 200000.0 - now - emptyExtraArgs - - -- Operator builds the swapper's allocation specification, mirroring the - -- dApp's POST /v1/pools/swap/request call. - reqRes <- submit operator $ exerciseCmd rulesCid PRules.PoolRules_RequestSwap with - poolCid - swapper = bob - inputInstrumentId = "USDC" - inputAmount = 100.0 - quoteBinding = Some (PRules.SwapQuoteBinding with - expectedPoolId = poolId - poolStateCid = initRes.poolStateCid - inputSliceCid = initRes.quoteSliceCid - outputSliceCids = [initRes.baseSliceCid] - minOutputAmount = 0.0) - - -- Bob (the wallet) authors that exact spec via the allocation factory. - let bobAccount = Utils.basicAccount bob - bobAllocateArg = WC.mkAllocationFactoryAllocate - reqRes.settlement reqRes.allocationSpec now [] [bob] emptyExtraArgs - bobInstr <- submit bob $ exerciseCmd factoryCid bobAllocateArg - bobAllocationCid <- case bobInstr.output of - V2.AllocationInstructionResult_Completed cid -> pure cid - _ -> abort "swap allocation must complete" - - -- Operator settles the swap against the authored allocation. - swapRes <- submit operator $ exerciseCmd rulesCid PRules.PoolRules_Swap with - expectedPoolId = poolId - poolCid - poolStateCid = initRes.poolStateCid - swapperAccount = bobAccount - inputInstrumentId = "USDC" - inputAmount = 100.0 - minOutputAmount = 0.0 - swapperAllocationCid = bobAllocationCid - inputSliceCid = initRes.quoteSliceCid - outputSliceCids = [initRes.baseSliceCid] - factoryCid = settleCid - extraArgs = emptyExtraArgs - quoteBinding = reqRes.quoteBinding - - Some state <- queryContractId operator swapRes.poolStateCid - assertMsg "base reserve decreased" (state.reserves.baseAmount < 10.0) - assertMsg "quote reserve increased" (state.reserves.quoteAmount > 200000.0) - assertMsg "swap via RequestSwap produced output" (swapRes.amountOut > 0.0) - pure () - --- A bilateral MatchedTrade settles atomically --------------------------- --- --- Verifies the OTC (over-the-counter, dealt directly not via the order book) --- settlement path end-to-end: the operator requests allocations, each side --- accepts and locks its own funds, and the operator settles both in one batch. - --- | Proves both matched-trade allocations settle atomically into final holdings. -testMatchedTradeFullSettle : Script () -testMatchedTradeFullSettle = do - operator <- allocateParty "venue" - admin <- allocateParty "admin-mt" - alice <- allocateParty "alice-mt" - bob <- allocateParty "bob-mt" - now <- getTime - - (factoryCid, settleCid) <- setupRegistries admin [operator, alice, bob] - - -- Build a MatchedTrade with two-way legs (alice <-> bob in BTC). - let aliceAccount = Utils.basicAccount alice - bobAccount = Utils.basicAccount bob - legs = - [ V2.TransferLeg with - transferLegId = "leg-1" - sender = aliceAccount - receiver = bobAccount - amount = 1.0 - instrumentId = "BTC" - meta = emptyMetadata - ] - tradeCid <- submit operator $ createCmd MT.MatchedTrade with - venue = operator - admin - transferLegs = legs - settlementDeadline = None - policyReceipt = None - - -- Operator requests allocations. - reqCids <- submit operator $ exerciseCmd tradeCid MT.MatchedTrade_RequestAllocations - length reqCids === 2 -- one per authorizer - - -- Each authorizer accepts via the V2 interface, then creates their - -- allocation under their own authority. Alice is sender of "leg-1"; - -- Bob is receiver. Both need an allocation. - let mkAlloc party cid = do - -- The accept choice consumes the request; we then create the - -- allocation via the factory. - let reqIface : ContractId V2.AllocationRequest = toInterfaceContractId cid - _ <- submit party $ exerciseCmd reqIface V2.AllocationRequest_Accept with - actors = [party] - extraArgs = emptyExtraArgs - -- Build allocation for this party using the trade's settlement. - let acct = Utils.basicAccount party - settlement = MT.mkTradeSettlementInfo tradeCid - (MT.MatchedTrade with - venue = operator - admin - transferLegs = legs - settlementDeadline = None - policyReceipt = None) - spec = V2.AllocationSpecification with - admin - authorizer = acct - transferLegSides = Utils.legsToSides acct legs - settlementDeadline = None - nextIterationFunding = None - committed = False - meta = emptyMetadata - allocateArg = AllocationInstructionV2.AllocationFactory_Allocate with - settlement - allocation = spec - requestedAt = now - inputHoldingCids = [] - actors = [party] - extraArgs = emptyExtraArgs - instr <- submit party $ exerciseCmd factoryCid allocateArg - case instr.output of - V2.AllocationInstructionResult_Completed allocCid -> pure allocCid - _ -> abort "allocation must complete" - - -- We need to know which request belongs to whom. Query and split. - reqs <- query @MT.TradeAllocationRequest operator - let reqsByOwner = [ (Utils.accountOwner req.authorizer, cid) | (cid, req) <- reqs ] - -- The active req cids may differ from the freshly-returned reqCids - -- because authorizers haven't accepted yet. Find the per-party CIDs. - let aliceReqCid = case [ c | (p, c) <- reqsByOwner, p == alice ] of - (c :: _) -> c - [] -> error "alice request missing" - bobReqCid = case [ c | (p, c) <- reqsByOwner, p == bob ] of - (c :: _) -> c - [] -> error "bob request missing" - - let aliceReqIface : ContractId V2.AllocationRequest = - toInterfaceContractId aliceReqCid - submitMustFail (actAs [bob] <> readAs [operator]) $ - exerciseCmd aliceReqIface V2.AllocationRequest_Accept with - actors = [bob] - extraArgs = emptyExtraArgs - - aliceAllocCid <- mkAlloc alice aliceReqCid - bobAllocCid <- mkAlloc bob bobReqCid - - -- Operator settles. Both allocations belong to admin's batch. - settleResult <- submit operator $ exerciseCmd tradeCid MT.MatchedTrade_Settle with - batchesByAdmin = Map.fromList - [ (admin, MT.SettlementBatchV2 with - transferLegs = Some legs - allocations = - [ Utils.finalAllocation aliceAllocCid - , Utils.finalAllocation bobAllocCid - ] - factoryCid = settleCid - extraArgs = emptyExtraArgs) - ] - allocationRequests = [] -- already consumed via Accept - dexPairCid = None - - -- Verify the result has one per-admin entry. - Map.size settleResult.resultsByAdmin === 1 - pure () - --- Partial fills carry only unspent funding ------------------------------ --- --- The matcher carries only the unspent portion of each allocation into a --- partial-fill remainder. This focused helper test checks both a partial and a --- full fill; RegistryConservationTests proves the corresponding SettleBatch --- behavior against real holdings. --- | Proves order remainder quantities and reserved funding use exact arithmetic. -testOrderRemainderFundingArithmetic : Script () -testOrderRemainderFundingArithmetic = do - -- Build accounts from real allocatable parties so the legs route correctly. - alice <- allocateParty "ome-buyer" - bob <- allocateParty "ome-seller" - let buyerAcct = Utils.basicAccount alice - sellerAcct = Utils.basicAccount bob - -- A bid for 10 base @ 5 quote locks 50 quote; a partial fill of 4 base - -- spends 4*5 = 20 quote, leaving 30. The ask locks 10 base; the fill - -- spends 4 base, leaving 6. - partialMatch = OME.MatchedOrderPair with - buyerAccount = buyerAcct - sellerAccount = sellerAcct - baseInstrumentId = "BTC" - quoteInstrumentId = "USDC" - fillQty = 4.0 - fillPrice = 5.0 - partialLegs = OME.mkMatchTransferLegs partialMatch - buyerBudget = TextMap.fromList [("USDC", 50.0)] - sellerBudget = TextMap.fromList [("BTC", 10.0)] - - -- Partial fill: residual = committed - spent on the locked instrument. - OME.remainderFunding buyerAcct buyerBudget partialLegs - === Some (TextMap.fromList [("USDC", 30.0)]) - OME.remainderFunding sellerAcct sellerBudget partialLegs - === Some (TextMap.fromList [("BTC", 6.0)]) - - -- Exact full fill: 10 base @ 5 spends the whole 50-quote / 10-base budget, - -- so the residual is empty -> None (the allocation fully settles). - let fullMatch = partialMatch with fillQty = 10.0 - fullLegs = OME.mkMatchTransferLegs fullMatch - OME.remainderFunding buyerAcct buyerBudget fullLegs === None - OME.remainderFunding sellerAcct sellerBudget fullLegs === None - pure () - --- Order matching enforces both limit prices ----------------------------- --- --- A resting bid @ 100 and ask @ 90 cross; any cleared price must sit in --- [90, 100]. OrderMatchExecution_Execute fetches both orders and refuses a --- fill outside that band (or with mismatched instruments / quantities / --- accounts), so a buggy or malicious matcher cannot fill a resting order on --- terms its owner never agreed to. --- Prefunded (no legs, next-iteration funding) allocation, as a trader authors --- it before the operator binds it onto a resting order. -omeAlloc - : Party -> Party -> ContractId V2.AllocationFactory -> Party -> V2.Account - -> TextMap.TextMap Decimal -> Time -> Script (ContractId V2.Allocation) -omeAlloc operator admin factoryCid party acct funding now = do - let settlement = V2.SettlementInfo with - executors = [operator]; id = "OrderMatch-m1"; cid = None - meta = emptyMetadata - arg = WC.mkPrefundedAllocationFactoryAllocate - admin acct settlement None funding now [] [party] emptyExtraArgs - r <- submit party $ exerciseCmd factoryCid arg - case r.output of - V2.AllocationInstructionResult_Completed cid -> pure cid - _ -> abort "alloc must complete" - --- A resting BTC/USDC order for 10 base at the given limit price, side, and --- status, optionally bound to a funding allocation. -omeOrder - : Party -> Party -> Party -> Order.Side -> Decimal -> Order.OrderStatus - -> Optional (ContractId V2.Allocation) -> Text -> Script (ContractId Order.Order) -omeOrder operator admin trader side limitPrice status allocationCid ref = - submit operator $ createCmd Order.Order with - operator; trader; admin - baseInstrumentId = "BTC"; quoteInstrumentId = "USDC" - side; limitPrice; remainingQty = 10.0 - expiry = None; status; allocationCid - settlementRef = Order.makeOrderRefFromText ref - --- A proposed fill of 4 base at the given price, naming the two orders and the --- two allocations it intends to spend. Exercising its _Execute choice is what --- the matching tests accept or reject. -omeExec - : Party -> V2.Account -> V2.Account -> ContractId Order.Order - -> ContractId Order.Order -> ContractId V2.Allocation -> ContractId V2.Allocation - -> Decimal -> OME.OrderMatchExecution -omeExec operator buyerAcct sellerAcct buyOrderCid sellOrderCid - buyerAllocationCid sellerAllocationCid fillPrice = - OME.OrderMatchExecution with - operator; matchId = "m1" - match = OME.MatchedOrderPair with - buyerAccount = buyerAcct; sellerAccount = sellerAcct - baseInstrumentId = "BTC"; quoteInstrumentId = "USDC" - fillQty = 4.0; fillPrice - buyOrderCid; sellOrderCid - buyerAllocationCid; sellerAllocationCid - buyerCommittedFunding = TextMap.empty - sellerCommittedFunding = TextMap.empty - --- | Proves a match outside either resting order's limit price is rejected. -testOrderMatchEnforcesLimitPrice : Script () -testOrderMatchEnforcesLimitPrice = do - operator <- allocateParty "ome-op" - admin <- allocateParty "ome-admin" - buyer <- allocateParty "ome-bid" - seller <- allocateParty "ome-ask" - now <- getTime - - (factoryCid, settleCid) <- setupRegistries admin [operator, buyer, seller] - - let buyerAcct = Utils.basicAccount buyer - sellerAcct = Utils.basicAccount seller - - -- Each side prefunds an allocation (bid locks quote, ask locks base). - buyerAllocationCid <- omeAlloc operator admin factoryCid buyer buyerAcct - (TextMap.fromList [("USDC", 400.0)]) now - sellerAllocationCid <- omeAlloc operator admin factoryCid seller sellerAcct - (TextMap.fromList [("BTC", 10.0)]) now - - -- Resting bid: willing to pay up to 100 quote/base for 10 base. - buyOrderCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Funded - (Some buyerAllocationCid) "bid" - -- Resting ask: willing to sell down to 90 quote/base for 10 base. - sellOrderCid <- omeOrder operator admin seller Order.Ask 90.0 Order.OS_Funded - (Some sellerAllocationCid) "ask" - - let mkExec = omeExec operator buyerAcct sellerAcct buyOrderCid sellOrderCid - buyerAllocationCid sellerAllocationCid - - -- Above the bid limit: the buyer would overpay -> rejected. - tooHigh <- submit operator $ createCmd (mkExec 101.0) - submitMustFail operator $ exerciseCmd tooHigh OME.OrderMatchExecution_Execute with - factoryCid = settleCid; extraArgs = emptyExtraArgs - - -- Below the ask limit: the seller would undersell -> rejected. - tooLow <- submit operator $ createCmd (mkExec 89.0) - submitMustFail operator $ exerciseCmd tooLow OME.OrderMatchExecution_Execute with - factoryCid = settleCid; extraArgs = emptyExtraArgs - - -- Inside the band [90, 100]: accepted, settles both allocations. - ok <- submit operator $ createCmd (mkExec 95.0) - res <- submit operator $ exerciseCmd ok OME.OrderMatchExecution_Execute with - factoryCid = settleCid; extraArgs = emptyExtraArgs - length res.settleResult.allocationSettleResults === 2 - pure () - --- A match can spend only the allocations bound to its orders ------------ --- --- The buyer rests two funded bids. Filling the first while spending the --- second's collateral conserves per-instrument totals inside the batch, so --- nothing downstream rejects it; only the order/allocation binding does. --- | Proves a match cannot substitute another order's funding allocation. -testOrderMatchRejectsAnotherOrdersAllocation : Script () -testOrderMatchRejectsAnotherOrdersAllocation = do - operator <- allocateParty "ome2-op" - admin <- allocateParty "ome2-admin" - buyer <- allocateParty "ome2-bid" - seller <- allocateParty "ome2-ask" - now <- getTime - - (factoryCid, settleCid) <- setupRegistries admin [operator, buyer, seller] - - let buyerAcct = Utils.basicAccount buyer - sellerAcct = Utils.basicAccount seller - quoteFunding = TextMap.fromList [("USDC", 400.0)] - - firstAllocCid <- omeAlloc operator admin factoryCid buyer buyerAcct quoteFunding now - secondAllocCid <- omeAlloc operator admin factoryCid buyer buyerAcct quoteFunding now - sellerAllocationCid <- omeAlloc operator admin factoryCid seller sellerAcct - (TextMap.fromList [("BTC", 10.0)]) now - - buyOrderCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Funded - (Some firstAllocCid) "bid-1" - _secondBidCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Funded - (Some secondAllocCid) "bid-2" - sellOrderCid <- omeOrder operator admin seller Order.Ask 90.0 Order.OS_Funded - (Some sellerAllocationCid) "ask" - - let mkExec buyerAllocationCid = omeExec operator buyerAcct sellerAcct - buyOrderCid sellOrderCid buyerAllocationCid sellerAllocationCid 95.0 - - foreign_ <- submit operator $ createCmd (mkExec secondAllocCid) - submitMustFail operator $ exerciseCmd foreign_ OME.OrderMatchExecution_Execute with - factoryCid = settleCid; extraArgs = emptyExtraArgs - - ok <- submit operator $ createCmd (mkExec firstAllocCid) - res <- submit operator $ exerciseCmd ok OME.OrderMatchExecution_Execute with - factoryCid = settleCid; extraArgs = emptyExtraArgs - length res.settleResult.allocationSettleResults === 2 - pure () - --- Pending orders cannot be matched -------------------------------------- --- --- The order carries the right allocation cid, so only the status gate can --- reject it. --- | Proves an unfunded pending order cannot enter settlement. -testOrderMatchRejectsPendingOrder : Script () -testOrderMatchRejectsPendingOrder = do - operator <- allocateParty "ome3-op" - admin <- allocateParty "ome3-admin" - buyer <- allocateParty "ome3-bid" - seller <- allocateParty "ome3-ask" - now <- getTime - - (factoryCid, settleCid) <- setupRegistries admin [operator, buyer, seller] - - let buyerAcct = Utils.basicAccount buyer - sellerAcct = Utils.basicAccount seller - - buyerAllocationCid <- omeAlloc operator admin factoryCid buyer buyerAcct - (TextMap.fromList [("USDC", 400.0)]) now - sellerAllocationCid <- omeAlloc operator admin factoryCid seller sellerAcct - (TextMap.fromList [("BTC", 10.0)]) now - - pendingBidCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Pending - (Some buyerAllocationCid) "bid" - sellOrderCid <- omeOrder operator admin seller Order.Ask 90.0 Order.OS_Funded - (Some sellerAllocationCid) "ask" - - pending <- submit operator $ createCmd (omeExec operator buyerAcct sellerAcct - pendingBidCid sellOrderCid buyerAllocationCid sellerAllocationCid 95.0) - submitMustFail operator $ exerciseCmd pending OME.OrderMatchExecution_Execute with - factoryCid = settleCid; extraArgs = emptyExtraArgs - - fundedBidCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Funded - (Some buyerAllocationCid) "bid" - ok <- submit operator $ createCmd (omeExec operator buyerAcct sellerAcct - fundedBidCid sellOrderCid buyerAllocationCid sellerAllocationCid 95.0) - res <- submit operator $ exerciseCmd ok OME.OrderMatchExecution_Execute with - factoryCid = settleCid; extraArgs = emptyExtraArgs - length res.settleResult.allocationSettleResults === 2 - pure () - --- Settlement and order roll-forward are atomic ------------------------- --- --- The settle archives both funding allocations, so an order left behind --- pointing at one is uncancellable (Order_Cancel exercises the archived cid) --- and unfillable. Nothing outside this choice may observe that state, so the --- choice itself archives the filled orders, rolls each remainder onto the --- allocation the settle minted, and records the trade. --- | Proves one transaction settles a partial fill and rolls both orders forward. -testOrderMatchRollsOrdersForwardAtomically : Script () -testOrderMatchRollsOrdersForwardAtomically = do - operator <- allocateParty "ome5-op" - admin <- allocateParty "ome5-admin" - buyer <- allocateParty "ome5-bid" - seller <- allocateParty "ome5-ask" - now <- getTime - - (factoryCid, settleCid) <- setupRegistries admin [operator, buyer, seller] - - let buyerAcct = Utils.basicAccount buyer - sellerAcct = Utils.basicAccount seller - - buyerAllocationCid <- omeAlloc operator admin factoryCid buyer buyerAcct - (TextMap.fromList [("USDC", 400.0)]) now - sellerAllocationCid <- omeAlloc operator admin factoryCid seller sellerAcct - (TextMap.fromList [("BTC", 10.0)]) now - - buyOrderCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Funded - (Some buyerAllocationCid) "bid" - sellOrderCid <- omeOrder operator admin seller Order.Ask 90.0 Order.OS_Funded - (Some sellerAllocationCid) "ask" - - execCid <- submit operator $ createCmd (omeExec operator buyerAcct sellerAcct - buyOrderCid sellOrderCid buyerAllocationCid sellerAllocationCid 95.0) - res <- submit operator $ exerciseCmd execCid OME.OrderMatchExecution_Execute with - factoryCid = settleCid; extraArgs = emptyExtraArgs - - -- Both orders filled 4 of 10, so both roll forward onto the allocation the - -- same transaction minted for them. The cids the orders were bound to are - -- gone with the settle. - isSome res.buyRemainderCid === True - isSome res.sellRemainderCid === True - liveOrders <- query @Order.Order operator - -- The ACS comes back ordered by contract id, so compare as a set. - sort (map fst liveOrders) - === sort [fromSome res.buyRemainderCid, fromSome res.sellRemainderCid] - Some buyRemainder <- queryContractId operator (fromSome res.buyRemainderCid) - Some sellRemainder <- queryContractId operator (fromSome res.sellRemainderCid) - buyRemainder.allocationCid === res.buyerNextAllocationCid - sellRemainder.allocationCid === res.sellerNextAllocationCid - [buyRemainder.remainingQty, sellRemainder.remainingQty] === [6.0, 6.0] - [buyRemainder.status, sellRemainder.status] - === [Order.OS_PartiallyFilled, Order.OS_PartiallyFilled] - - -- The fill is durable trade history: OrderMatchExecution is consumed by the - -- choice, so nothing about the match would survive in the ACS without it. - settled <- query @MT.SettledTrade operator - map (Some . fst) settled === [res.settledTradeCid] - [ t.transferLegs | (_, t) <- settled ] === [OME.mkMatchTransferLegs - (OME.MatchedOrderPair with - buyerAccount = buyerAcct; sellerAccount = sellerAcct - baseInstrumentId = "BTC"; quoteInstrumentId = "USDC" - fillQty = 4.0; fillPrice = 95.0)] - pure () - --- Exhausting the funding budget closes the order ------------------------ --- --- A tiny residual quantity is not recreated when decimal rounding has already --- consumed the entire committed budget. Any remainder contract must be backed --- by a funded next allocation. --- | Proves exhausted backing closes the remainder instead of creating an orphan. -testOrderMatchClosesRemainderWhenBudgetIsExhausted : Script () -testOrderMatchClosesRemainderWhenBudgetIsExhausted = do - operator <- allocateParty "ome6-op" - admin <- allocateParty "ome6-admin" - buyer <- allocateParty "ome6-bid" - seller <- allocateParty "ome6-ask" - now <- getTime - - (factoryCid, settleCid) <- setupRegistries admin [operator, buyer, seller] - - let buyerAcct = Utils.basicAccount buyer - sellerAcct = Utils.basicAccount seller - price = 0.000001 - restingQty = 1000.0 - fillQty = 999.99996 - committedQuote = restingQty * price - - -- The collision: a strictly partial fill whose spend equals the whole budget. - (fillQty < restingQty) === True - fillQty * price === committedQuote - - buyerAllocationCid <- omeAlloc operator admin factoryCid buyer buyerAcct - (TextMap.fromList [("USDC", committedQuote)]) now - sellerAllocationCid <- omeAlloc operator admin factoryCid seller sellerAcct - (TextMap.fromList [("BTC", fillQty)]) now - - let mkOrder trader side limitPrice remainingQty allocationCid ref = - submit operator $ createCmd Order.Order with - operator; trader; admin - baseInstrumentId = "BTC"; quoteInstrumentId = "USDC" - side; limitPrice; remainingQty - expiry = None; status = Order.OS_Funded - allocationCid = Some allocationCid - settlementRef = Order.makeOrderRefFromText ref - buyOrderCid <- mkOrder buyer Order.Bid price restingQty buyerAllocationCid "dust-bid" - sellOrderCid <- mkOrder seller Order.Ask price fillQty sellerAllocationCid "dust-ask" - - execCid <- submit operator $ createCmd OME.OrderMatchExecution with - operator; matchId = "dust" - match = OME.MatchedOrderPair with - buyerAccount = buyerAcct; sellerAccount = sellerAcct - baseInstrumentId = "BTC"; quoteInstrumentId = "USDC" - fillQty; fillPrice = price - buyOrderCid; sellOrderCid - buyerAllocationCid; sellerAllocationCid - buyerCommittedFunding = TextMap.empty - sellerCommittedFunding = TextMap.empty - res <- submit operator $ exerciseCmd execCid OME.OrderMatchExecution_Execute with - factoryCid = settleCid; extraArgs = emptyExtraArgs - - -- 0.00004 base is unfilled and the quote behind it is gone, so nothing rolls - -- forward on the bid. - restingQty - fillQty === 0.00004 - res.buyerNextAllocationCid === None - res.buyRemainderCid === None - res.sellRemainderCid === None - liveOrders <- query @Order.Order operator - [ cid | (cid, o) <- liveOrders, isNone o.allocationCid ] === [] - liveOrders === [] - pure () - --- Choice-context fixture ------------------------------------------------ --- The mock factories can require a marker in their choice context. This makes --- context forwarding observable without depending on an external registry. -data ContextPoolFixture = ContextPoolFixture with - fixturePoolId : Pool.PoolId - fixturePoolCid : ContractId Pool.Pool - fixtureStateCid : ContractId PState.PoolState - fixtureLiquidityRulesCid : ContractId Dvp.PoolLiquidityRules - fixturePolicyCid : ContractId LP.LPTokenPolicy - fixtureAllocationFactoryCid : ContractId V2.AllocationFactory - fixtureSettlementFactoryCid : ContractId V2.SettlementFactory - -mkPoolFixture : Party -> Party -> Party -> Bool -> Script ContextPoolFixture -mkPoolFixture operator lpRegistrar admin requireContext = do - factory <- submit admin $ createCmd Mock.MockAllocationFactory with - admin; users = [operator, lpRegistrar]; requireContext - settle <- submit admin $ createCmd Mock.MockSettlementFactory with - admin; users = [operator, lpRegistrar]; requireContext - let lpId = V2.InstrumentId with admin = lpRegistrar; id = "BTC-USDC-LP" - poolId = "BTC-USDC" - poolCid <- submit operator $ createCmd Pool.Pool with - poolId; operator; lpRegistrar; admin - baseInstrumentId = "BTC"; quoteInstrumentId = "USDC"; lpInstrumentId = lpId - feeBps = 30 - stateCid <- submit operator $ createCmd PState.PoolState with - poolId; operator; lpRegistrar - status = Pool.PS_Unfunded - reserves = Pool.PoolReserves with baseAmount = 0.0; quoteAmount = 0.0 - totalLpSupply = 0.0; publicReaders = [] - dvpCid <- submit (actAs [operator, lpRegistrar]) $ - createCmd Dvp.PoolLiquidityRules with operator; lpRegistrar - policyCid <- submit lpRegistrar $ createCmd LP.LPTokenPolicy with - lpRegistrar; operator; lpInstrumentId = lpId - totalSupply = 0.0; active = True - pure ContextPoolFixture with - fixturePoolId = poolId - fixturePoolCid = poolCid - fixtureStateCid = stateCid - fixtureLiquidityRulesCid = dvpCid - fixturePolicyCid = policyCid - fixtureAllocationFactoryCid = toInterfaceContractId factory - fixtureSettlementFactoryCid = toInterfaceContractId settle - --- Marker required by a context-enabled mock factory. -markerContext : ExtraArgs -markerContext = ExtraArgs with - context = ChoiceContext with - values = TextMap.fromList [(Mock.dexChoiceContextKey, AV_Bool True)] - meta = emptyMetadata - --- A DvP add succeeds when both factory calls receive their required context. --- | Proves add-liquidity threads supplied context into both allocation calls. -testDvpAddThreadsChoiceContext : Script () -testDvpAddThreadsChoiceContext = do - operator <- allocateParty "operator" - lpRegistrar <- allocateParty "lp-registrar" - admin <- allocateParty "admin" - now <- getTime - fixture <- mkPoolFixture operator lpRegistrar admin True - let poolId = fixture.fixturePoolId - poolCid = fixture.fixturePoolCid - stateCid = fixture.fixtureStateCid - dvpCid = fixture.fixtureLiquidityRulesCid - policyCid = fixture.fixturePolicyCid - factoryCid = fixture.fixtureAllocationFactoryCid - settleCid = fixture.fixtureSettlementFactoryCid - res <- dvpFundPool operator lpRegistrar factoryCid settleCid poolId poolCid stateCid policyCid dvpCid lpRegistrar 10.0 200000.0 now markerContext - Some state <- queryContractId operator res.poolStateCid - state.status === Pool.PS_Active - --- The same factory rejects an allocation when the context is omitted. --- | Proves an empty allocation choice context is not silently substituted. -testDvpAddRejectsEmptyContext : Script () -testDvpAddRejectsEmptyContext = do - operator <- allocateParty "operator" - lpRegistrar <- allocateParty "lp-registrar" - admin <- allocateParty "admin" - now <- getTime - fixture <- mkPoolFixture operator lpRegistrar admin True - let poolCid = fixture.fixturePoolCid - dvpCid = fixture.fixtureLiquidityRulesCid - factoryCid = fixture.fixtureAllocationFactoryCid - reqCid <- submit operator $ exerciseCmd dvpCid Dvp.PoolLiquidityRules_RequestAddLiquidity with - poolCid; recipient = lpRegistrar - baseAmount = 10.0; quoteAmount = 200000.0 - lpAmount = PM.sqrtDecimal 2000000.0 - requestedAt = now; settleAt = None - Some req <- queryContractId operator reqCid - submitMustFail lpRegistrar $ - exerciseCmd factoryCid V2.AllocationFactory_Allocate with - settlement = req.settlement - allocation = head req.allocations - requestedAt = now - inputHoldingCids = [] - actors = [lpRegistrar] - extraArgs = emptyExtraArgs - --- Each registry admin receives its own choice context ------------------- --- --- The DvP settle runs two per-admin batches in one transaction: --- base/quote under pool.admin, LP mint/burn under pool.lpRegistrar. Each --- batch must carry its own registry choice context. Driven through the --- context-requiring MockRegistry, these prove poolAdminExtraArgs and --- lpRegistrarExtraArgs are wired to distinct batches: blanking either one --- aborts only that batch's allocate/settle. - --- Author the three add allocations (base + quote deposit, LP mint receipt) --- the LP signs, returning the request + allocation cids so a test can drive --- SettleAddLiquidity with explicit per-admin choice contexts. -authorAddAllocations - : Party -> ContractId V2.AllocationFactory -> ContractId Pool.Pool - -> ContractId Dvp.PoolLiquidityRules -> Party -> Decimal -> Decimal -> Time -> ExtraArgs - -> Script ( ContractId LAR.LiquidityAllocationRequest - , ContractId V2.Allocation, ContractId V2.Allocation, ContractId V2.Allocation ) -authorAddAllocations operator factoryCid poolCid dvpCid recipient baseAmount quoteAmount now authorCtx = do - reqCid <- submit operator $ exerciseCmd dvpCid Dvp.PoolLiquidityRules_RequestAddLiquidity with - poolCid; recipient; baseAmount; quoteAmount - lpAmount = PM.sqrtDecimal (baseAmount * quoteAmount) - requestedAt = now; settleAt = None - Some req <- queryContractId operator reqCid - let mkOne spec = do - res <- submit recipient $ exerciseCmd factoryCid V2.AllocationFactory_Allocate with - settlement = req.settlement; allocation = spec; requestedAt = now - inputHoldingCids = []; actors = [recipient]; extraArgs = authorCtx - case res.output of - V2.AllocationInstructionResult_Completed cid -> pure cid - _ -> abort "author add alloc must complete" - baseCid <- mkOne (head req.allocations) - quoteCid <- mkOne (head (tail req.allocations)) - receiptCid <- mkOne (head (tail (tail req.allocations))) - pure (reqCid, baseCid, quoteCid, receiptCid) - --- Both per-admin contexts supplied: the context-requiring base/quote and LP --- batches both settle, pool goes Active. --- | Proves split-admin settle sends each registry its own choice context. -testDvpSettleThreadsBothAdminContexts : Script () -testDvpSettleThreadsBothAdminContexts = do - operator <- allocateParty "operator" - lpRegistrar <- allocateParty "lp-registrar" - admin <- allocateParty "admin" - now <- getTime - fixture <- mkPoolFixture operator lpRegistrar admin True - let poolId = fixture.fixturePoolId - poolCid = fixture.fixturePoolCid - stateCid = fixture.fixtureStateCid - dvpCid = fixture.fixtureLiquidityRulesCid - policyCid = fixture.fixturePolicyCid - factoryCid = fixture.fixtureAllocationFactoryCid - settleCid = fixture.fixtureSettlementFactoryCid - (reqCid, baseCid, quoteCid, receiptCid) <- - authorAddAllocations operator factoryCid poolCid dvpCid lpRegistrar 10.0 200000.0 now markerContext - let settleAdd poolAdminCtx lpRegistrarCtx = - exerciseCmd dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = poolId; poolCid; poolStateCid = stateCid; lpPolicyCid = policyCid - requestCid = Some reqCid; acceptanceCid = None; recipient = lpRegistrar - lpBaseDepositCid = baseCid; lpQuoteDepositCid = quoteCid; lpReceiptCid = receiptCid - baseFactoryCid = factoryCid; quoteFactoryCid = factoryCid; lpFactoryCid = factoryCid - baseQuoteSettleCid = settleCid; lpSettleCid = settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0; knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = poolAdminCtx; lpRegistrarExtraArgs = lpRegistrarCtx - res <- submit (actAs [operator, lpRegistrar]) $ settleAdd markerContext markerContext - Some state <- queryContractId operator res.poolStateCid - state.status === Pool.PS_Active - --- Blanking poolAdminExtraArgs aborts the base/quote batch (its factory --- requires context), proving that field feeds the pool.admin batch. --- | Proves omitting only pool-admin context aborts the pool-admin batch. -testDvpSettleRequiresPoolAdminContext : Script () -testDvpSettleRequiresPoolAdminContext = do - operator <- allocateParty "operator" - lpRegistrar <- allocateParty "lp-registrar" - admin <- allocateParty "admin" - now <- getTime - fixture <- mkPoolFixture operator lpRegistrar admin True - let poolId = fixture.fixturePoolId - poolCid = fixture.fixturePoolCid - stateCid = fixture.fixtureStateCid - dvpCid = fixture.fixtureLiquidityRulesCid - policyCid = fixture.fixturePolicyCid - factoryCid = fixture.fixtureAllocationFactoryCid - settleCid = fixture.fixtureSettlementFactoryCid - (reqCid, baseCid, quoteCid, receiptCid) <- - authorAddAllocations operator factoryCid poolCid dvpCid lpRegistrar 10.0 200000.0 now markerContext - submitMustFail (actAs [operator, lpRegistrar]) $ - exerciseCmd dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = poolId; poolCid; poolStateCid = stateCid; lpPolicyCid = policyCid - requestCid = Some reqCid; acceptanceCid = None; recipient = lpRegistrar - lpBaseDepositCid = baseCid; lpQuoteDepositCid = quoteCid; lpReceiptCid = receiptCid - baseFactoryCid = factoryCid; quoteFactoryCid = factoryCid; lpFactoryCid = factoryCid - baseQuoteSettleCid = settleCid; lpSettleCid = settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0; knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = markerContext - --- Blanking lpRegistrarExtraArgs aborts the LP mint batch, proving that --- field feeds the pool.lpRegistrar batch. --- | Proves omitting only LP-registrar context aborts the LP batch. -testDvpSettleRequiresLpRegistrarContext : Script () -testDvpSettleRequiresLpRegistrarContext = do - operator <- allocateParty "operator" - lpRegistrar <- allocateParty "lp-registrar" - admin <- allocateParty "admin" - now <- getTime - fixture <- mkPoolFixture operator lpRegistrar admin True - let poolId = fixture.fixturePoolId - poolCid = fixture.fixturePoolCid - stateCid = fixture.fixtureStateCid - dvpCid = fixture.fixtureLiquidityRulesCid - policyCid = fixture.fixturePolicyCid - factoryCid = fixture.fixtureAllocationFactoryCid - settleCid = fixture.fixtureSettlementFactoryCid - (reqCid, baseCid, quoteCid, receiptCid) <- - authorAddAllocations operator factoryCid poolCid dvpCid lpRegistrar 10.0 200000.0 now markerContext - submitMustFail (actAs [operator, lpRegistrar]) $ - exerciseCmd dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = poolId; poolCid; poolStateCid = stateCid; lpPolicyCid = policyCid - requestCid = Some reqCid; acceptanceCid = None; recipient = lpRegistrar - lpBaseDepositCid = baseCid; lpQuoteDepositCid = quoteCid; lpReceiptCid = receiptCid - baseFactoryCid = factoryCid; quoteFactoryCid = factoryCid; lpFactoryCid = factoryCid - baseQuoteSettleCid = settleCid; lpSettleCid = settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0; knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = markerContext; lpRegistrarExtraArgs = emptyExtraArgs diff --git a/trading-tests/CantonDex/Tests/LifecycleChoiceTests.daml b/trading-tests/CantonDex/Tests/LifecycleChoiceTests.daml new file mode 100644 index 00000000..81ddaccf --- /dev/null +++ b/trading-tests/CantonDex/Tests/LifecycleChoiceTests.daml @@ -0,0 +1,295 @@ +-- | Focused lifecycle and recovery-choice tests. +-- +-- Happy-path settlement suites prove how value moves. This module proves the +-- explicit exits around those paths: who may cancel/reject/withdraw a market +-- object, which contracts disappear, and whether locked holdings are returned. +-- Design context: `docs/concepts/workflows.md#active-workflow-map`. +module CantonDex.Tests.LifecycleChoiceTests where + +import DA.Assert +import DA.Foldable (forA_) +import DA.Optional (fromSome, isNone) +import DA.TextMap qualified as TextMap +import DA.Time (addRelTime, hours) +import Daml.Script + +import Splice.Api.Token.AllocationV2 qualified as V2 +import Splice.Api.Token.AllocationInstructionV2 qualified as V2 +import Splice.Api.Token.HoldingV2 qualified as V2 +import Splice.Api.Token.MetadataV1 +import Splice.Testing.Utils (emptyExtraArgs) + +import CantonDex.Dex.MatchedTrade qualified as MT +import CantonDex.Dex.Order qualified as Order +import CantonDex.Dex.OrderFundingRequest qualified as OFR +import CantonDex.Dex.OrderMatchExecution qualified as OME +import CantonDex.Dex.Rfq qualified as Rfq +import CantonDex.Registry.V2 qualified as RegV2 +import CantonDex.Trading.Utils qualified as Utils + +mintHolding + : Party -> Party -> Text -> Decimal -> Script (ContractId RegV2.Holding) +mintHolding admin owner instrumentId amount = + submit (actAs [admin, owner]) $ createCmd RegV2.Holding with + admin + owner + instrumentId + amount + locked = False + +allocate + : ContractId RegV2.Registry + -> Party + -> V2.SettlementInfo + -> V2.AllocationSpecification + -> [ContractId V2.Holding] + -> Time + -> Script (ContractId V2.Allocation) +allocate registryCid authorizer settlement allocation inputHoldingCids requestedAt = do + result <- submit authorizer $ + exerciseCmd (toInterfaceContractId registryCid : ContractId V2.AllocationFactory) + V2.AllocationFactory_Allocate with + settlement + allocation + requestedAt + inputHoldingCids + actors = [authorizer] + extraArgs = emptyExtraArgs + case result.output of + V2.AllocationInstructionResult_Completed cid -> pure cid + _ -> abort "allocation must complete" + +createOrderRequest + : Party -> Party -> Party -> Text -> Script (ContractId OFR.OrderFundingRequest) +createOrderRequest operator trader admin label = + submit trader $ createCmd OFR.OrderFundingRequest with + operator + trader + admin + baseInstrumentId = "BTC" + quoteInstrumentId = "USDC" + side = Order.Bid + limitPrice = 10.0 + quantity = 2.0 + expiry = None + +unlockedAmounts : Party -> Text -> Script [Decimal] +unlockedAmounts owner instrumentId = do + holdings <- query @RegV2.Holding owner + pure + [ h.amount + | (_, h) <- holdings + , h.owner == owner + , h.instrumentId == instrumentId + , not h.locked + ] + +lockedAmounts : Party -> Text -> Script [Decimal] +lockedAmounts owner instrumentId = do + holdings <- query @RegV2.Holding owner + pure + [ h.amount + | (_, h) <- holdings + , h.owner == owner + , h.instrumentId == instrumentId + , h.locked + ] + +-- Before the request is bound, trader cancellation and operator rejection are +-- deliberately separate exits. +-- | Proves trader cancel and operator reject authority on an unfunded order request. +testOrderFundingRequestCancelAndRejectAuthority : Script () +testOrderFundingRequestCancelAndRejectAuthority = do + operator <- allocateParty "request-operator" + trader <- allocateParty "request-trader" + admin <- allocateParty "request-admin" + + cancelCid <- createOrderRequest operator trader admin "cancel" + submitMustFail operator $ + exerciseCmd cancelCid OFR.OrderFundingRequest_Cancel + submit trader $ exerciseCmd cancelCid OFR.OrderFundingRequest_Cancel + None <- queryContractId trader cancelCid + + rejectCid <- createOrderRequest operator trader admin "reject" + submitMustFail trader $ + exerciseCmd rejectCid OFR.OrderFundingRequest_Reject with reason = "unsupported pair" + submit operator $ + exerciseCmd rejectCid OFR.OrderFundingRequest_Reject with reason = "unsupported pair" + None <- queryContractId operator rejectCid + pure () + +-- A funded cancellation must close both records and unlock actual backing. +-- | Proves funded-order cancellation releases the trader's real locked holding. +testOrderCancelReleasesRealFunding : Script () +testOrderCancelReleasesRealFunding = do + operator <- allocateParty "cancel-order-operator" + trader <- allocateParty "cancel-order-trader" + admin <- allocateParty "cancel-order-admin" + registryCid <- submit admin $ createCmd RegV2.Registry with + admin + users = [operator, trader] + now <- getTime + + requestCid <- createOrderRequest operator trader admin "fund-and-cancel" + bound <- submit operator $ + exerciseCmd requestCid OFR.OrderFundingRequest_Bind with + settlementRef = "fund-and-cancel" + Some allocationRequest <- queryContractId operator bound.allocationRequestCid + holdingCid <- mintHolding admin trader "USDC" allocationRequest.lockAmount + let settlement = fromSome allocationRequest.allocationSettlement + specification = Order.orderFundingSpecification + admin trader allocationRequest.lockInstrumentId allocationRequest.lockAmount + allocationRequest.expiry + allocationCid <- allocate registryCid trader settlement specification + [toInterfaceContractId holdingCid] now + funded <- submit operator $ exerciseCmd bound.orderCid Order.Order_Fund with + allocationCid + allocationRequestCid = Some bound.allocationRequestCid + + result <- submit (actAs [operator] <> readAs [admin]) $ + exerciseCmd funded.orderCid Order.Order_Cancel with extraArgs = emptyExtraArgs + assertMsg "cancel returns the released holding ids" + (not (TextMap.null result.releasedHoldings)) + None <- queryContractId operator funded.orderCid + allocationAfter <- queryInterfaceContractId @V2.Allocation operator allocationCid + assertMsg "cancel consumes the funding allocation" (isNone allocationAfter) + unlocked <- unlockedAmounts trader "USDC" + locked <- lockedAmounts trader "USDC" + unlocked === [20.0] + locked === [] + +-- Cancelling the proposed trade must clean up its requests/allocations without +-- executing its value-moving leg. +-- | Proves matched-trade cancellation cleans up requests and releases real backing. +testMatchedTradeCancelReleasesRealFunding : Script () +testMatchedTradeCancelReleasesRealFunding = do + operator <- allocateParty "trade-cancel-operator" + admin <- allocateParty "trade-cancel-admin" + alice <- allocateParty "trade-cancel-alice" + bob <- allocateParty "trade-cancel-bob" + registryCid <- submit admin $ createCmd RegV2.Registry with + admin + users = [operator, alice, bob] + now <- getTime + aliceHolding <- mintHolding admin alice "BTC" 1.0 + let aliceAccount = Utils.basicAccount alice + bobAccount = Utils.basicAccount bob + leg = V2.TransferLeg with + transferLegId = "cancelled-leg" + sender = aliceAccount + receiver = bobAccount + amount = 1.0 + instrumentId = "BTC" + meta = emptyMetadata + tradeCid <- submit operator $ createCmd MT.MatchedTrade with + venue = operator + admin + transferLegs = [leg] + settlementDeadline = None + policyReceipt = None + requestCids <- submit operator $ + exerciseCmd tradeCid MT.MatchedTrade_RequestAllocations + + allocationCids <- forA requestCids $ \requestCid -> do + Some request <- queryContractId operator requestCid + let party = fromSome request.authorizer.owner + specification = V2.AllocationSpecification with + admin + authorizer = request.authorizer + transferLegSides = Utils.legsToSides request.authorizer request.transferLegs + settlementDeadline = request.settlementDeadline + nextIterationFunding = None + committed = False + meta = emptyMetadata + inputs = if party == alice then [toInterfaceContractId aliceHolding] else [] + allocate registryCid party request.settlement specification inputs now + + cancelResult <- submit (actAs [operator] <> readAs [admin]) $ + exerciseCmd tradeCid MT.MatchedTrade_Cancel with + allocationsToCancel = [(cid, emptyExtraArgs) | cid <- allocationCids] + allocationRequestCids = requestCids + length cancelResult.cancelResults === 2 + None <- queryContractId operator tradeCid + forA_ requestCids $ \cid -> do + None <- queryContractId operator cid + pure () + forA_ allocationCids $ \cid -> do + allocationAfter <- queryInterfaceContractId @V2.Allocation operator cid + assertMsg "cancel consumes each trade allocation" (isNone allocationAfter) + unlocked <- unlockedAmounts alice "BTC" + locked <- lockedAmounts alice "BTC" + unlocked === [1.0] + locked === [] + +-- | Proves RFQ and quote exits are controlled by their own signatories. +testRfqCancelAndQuoteWithdrawAuthority : Script () +testRfqCancelAndQuoteWithdrawAuthority = do + trader <- allocateParty "rfq-cancel-trader" + operator <- allocateParty "rfq-cancel-operator" + dealer <- allocateParty "rfq-cancel-dealer" + now <- getTime + let expiresAt = addRelTime now (hours 1) + + rfqCid <- submit trader $ createCmd Rfq.Rfq with + trader + operator + rfqId = "rfq-cancel" + pair = "BTC/USDC" + side = Rfq.RFQ_Buy + size = 1.0 + expiresAt + whitelist = [dealer] + createdAt = now + submitMustFail operator $ exerciseCmd rfqCid Rfq.Rfq_Cancel + submit trader $ exerciseCmd rfqCid Rfq.Rfq_Cancel + None <- queryContractId trader rfqCid + + quoteCid <- submit dealer $ createCmd Rfq.RfqQuote with + dealer + trader + operator + rfqId = "rfq-cancel" + price = 100.0 + expiresAt + postedAt = now + tier = Rfq.TierTrusted + submitMustFail trader $ exerciseCmd quoteCid Rfq.RfqQuote_Withdraw + submit dealer $ exerciseCmd quoteCid Rfq.RfqQuote_Withdraw + None <- queryContractId dealer quoteCid + pure () + +-- Abort discards a stale proposal without fetching or mutating the referenced +-- orders and allocations. +-- | Proves match abort consumes only the proposal and requires operator authority. +testOrderMatchExecutionAbort : Script () +testOrderMatchExecutionAbort = do + operator <- allocateParty "abort-operator" + admin <- allocateParty "abort-admin" + buyer <- allocateParty "abort-buyer" + seller <- allocateParty "abort-seller" + placeholderCid <- createOrderRequest operator buyer admin "abort-placeholder" + let placeholderOrderCid : ContractId Order.Order = coerceContractId placeholderCid + placeholderAllocationCid : ContractId V2.Allocation = coerceContractId placeholderCid + executionCid <- submit operator $ createCmd OME.OrderMatchExecution with + operator + matchId = "aborted-match" + match = OME.MatchedOrderPair with + buyerAccount = Utils.basicAccount buyer + sellerAccount = Utils.basicAccount seller + baseInstrumentId = "BTC" + quoteInstrumentId = "USDC" + fillQty = 1.0 + fillPrice = 100.0 + buyOrderCid = placeholderOrderCid + sellOrderCid = placeholderOrderCid + buyerAllocationCid = placeholderAllocationCid + sellerAllocationCid = placeholderAllocationCid + buyerCommittedFunding = TextMap.empty + sellerCommittedFunding = TextMap.empty + + submitMustFail buyer $ exerciseCmd executionCid OME.OrderMatchExecution_Abort + submit operator $ exerciseCmd executionCid OME.OrderMatchExecution_Abort + None <- queryContractId operator executionCid + -- Abort deliberately does not fetch its referenced market state. + Some _ <- queryContractId buyer placeholderCid + pure () diff --git a/trading-tests/CantonDex/Tests/OrderWorkflowTests.daml b/trading-tests/CantonDex/Tests/OrderWorkflowTests.daml new file mode 100644 index 00000000..8b88fc8c --- /dev/null +++ b/trading-tests/CantonDex/Tests/OrderWorkflowTests.daml @@ -0,0 +1,478 @@ +-- | Prefunded-order choreography and matching rules against MockRegistry. +-- +-- Read testOrderFundingFlow first, then the arithmetic and rejection tests, and +-- finish with testOrderMatchRollsOrdersForwardAtomically. The suite proves +-- trader/operator authority, allocation binding, price limits, and atomic +-- remainder creation. Its holding-less fixture does NOT prove locked backing; +-- RegistryConservationTests supplies those value-conservation proofs. +-- Design context: `docs/concepts/design-tour.md#prefunded-orders`. +module CantonDex.Tests.OrderWorkflowTests where + +import DA.Assert +import DA.List (sort) +import DA.Optional (isSome, isNone, fromSome) +import DA.TextMap qualified as TextMap +import DA.Time + +import Daml.Script + +import Splice.Api.Token.HoldingV2 qualified as V2 +import Splice.Api.Token.AllocationV2 qualified as V2 +import Splice.Api.Token.AllocationInstructionV2 qualified as V2 +import Splice.Api.Token.MetadataV1 +import Splice.Testing.Utils (emptyExtraArgs) + +import CantonDex.Dex.MatchedTrade qualified as MT +import CantonDex.Dex.Order qualified as Order +import CantonDex.Dex.OrderFundingRequest qualified as OFR +import CantonDex.Dex.OrderMatchExecution qualified as OME +import CantonDex.Trading.Utils qualified as Utils +import CantonDex.Trading.WorkflowConstructors qualified as WC +import CantonDex.Tests.WorkflowTestFixtures + +-- The trader authorizes a pending order allocation --------------------- +-- +-- Contract transitions under test: +-- OrderFundingRequest --Bind--> Pending Order + OrderAllocationRequest +-- trader --Allocate--> committed V2.Allocation +-- Pending Order --Fund--> Funded Order (request consumed) +-- The trader, not the operator, authorizes the allocation. The operator can +-- only attach it to the pending order. This holding-less fixture does not lock +-- funds; real-backing coverage lives in RegistryConservationTests. + +-- | Proves trader funding binds the exact requested allocation to an order. +testOrderFundingFlow : Script () +testOrderFundingFlow = do + operator <- allocateParty "operator" + admin <- allocateParty "admin" + alice <- allocateParty "alice" + now <- getTime + + (factoryCid, _) <- setupRegistries admin [operator, alice] + + -- Arrange: Alice asks to place a bid for 0.5 BTC at 60,000 USDC. + reqCid <- submit alice $ createCmd OFR.OrderFundingRequest with + operator + trader = alice + admin + baseInstrumentId = "BTC" + quoteInstrumentId = "USDC" + side = Order.Bid + limitPrice = 60000.0 + quantity = 0.5 + expiry = Some (addRelTime now (hours 1)) + + -- Act 1: the operator turns the intent into a pending market object and a + -- standard allocation request describing the required funding. + bindRes <- submit operator $ exerciseCmd reqCid OFR.OrderFundingRequest_Bind with + settlementRef = "ord-test-1" + + -- Assert the intermediate state before any mock allocation is attached. + Some order <- queryContractId operator bindRes.orderCid + order.status === Order.OS_Pending + Some allocReq <- queryContractId operator bindRes.allocationRequestCid + + -- Act 2: Alice's wallet authors the committed funding allocation under her + -- own authority. The request remains live as the correlation record until + -- the operator binds the resulting allocation. + let aliceAccount = Utils.basicAccount alice + settlement = fromSome allocReq.allocationSettlement + specification = Order.orderFundingSpecification + admin alice allocReq.lockInstrumentId allocReq.lockAmount + allocReq.expiry + allocateArg = V2.AllocationFactory_Allocate with + settlement + allocation = specification + requestedAt = now + inputHoldingCids = [] + actors = [alice] + extraArgs = emptyExtraArgs + + instrResult <- submit alice $ exerciseCmd factoryCid allocateArg + case instrResult.output of + V2.AllocationInstructionResult_Completed allocCid -> do + -- Funding without the request correlation is rejected; otherwise the + -- request would remain live after the order becomes funded. + submitMustFail operator $ exerciseCmd bindRes.orderCid Order.Order_Fund with + allocationCid = allocCid + allocationRequestCid = None + -- Act 3: the operator binds Alice's allocation. Order_Fund consumes both + -- the pending order and its allocation request, then creates the funded + -- successor. + fundRes <- submit operator $ exerciseCmd bindRes.orderCid Order.Order_Fund with + allocationCid = allocCid + allocationRequestCid = Some bindRes.allocationRequestCid + Some funded <- queryContractId operator fundRes.orderCid + funded.status === Order.OS_Funded + funded.allocationCid === Some allocCid + pendingGone <- queryContractId operator bindRes.orderCid + assertMsg "pending Order consumed by Order_Fund" (isNone pendingGone) + reqGone <- queryContractId operator bindRes.allocationRequestCid + assertMsg "OrderAllocationRequest consumed by Order_Fund" (isNone reqGone) + _ -> abort "Mock factory should complete immediately" + + pure () + + +-- Partial fills carry only unspent funding ------------------------------ +-- +-- The matcher carries only the unspent portion of each allocation into a +-- partial-fill remainder. This focused helper test checks both a partial and a +-- full fill; RegistryConservationTests proves the corresponding SettleBatch +-- behavior against real holdings. +-- | Proves order remainder quantities and reserved funding use exact arithmetic. +testOrderRemainderFundingArithmetic : Script () +testOrderRemainderFundingArithmetic = do + -- Build accounts from real allocatable parties so the legs route correctly. + alice <- allocateParty "ome-buyer" + bob <- allocateParty "ome-seller" + let buyerAcct = Utils.basicAccount alice + sellerAcct = Utils.basicAccount bob + -- A bid for 10 base @ 5 quote locks 50 quote; a partial fill of 4 base + -- spends 4*5 = 20 quote, leaving 30. The ask locks 10 base; the fill + -- spends 4 base, leaving 6. + partialMatch = OME.MatchedOrderPair with + buyerAccount = buyerAcct + sellerAccount = sellerAcct + baseInstrumentId = "BTC" + quoteInstrumentId = "USDC" + fillQty = 4.0 + fillPrice = 5.0 + partialLegs = OME.mkMatchTransferLegs partialMatch + buyerBudget = TextMap.fromList [("USDC", 50.0)] + sellerBudget = TextMap.fromList [("BTC", 10.0)] + + -- Partial fill: residual = committed - spent on the locked instrument. + OME.remainderFunding buyerAcct buyerBudget partialLegs + === Some (TextMap.fromList [("USDC", 30.0)]) + OME.remainderFunding sellerAcct sellerBudget partialLegs + === Some (TextMap.fromList [("BTC", 6.0)]) + + -- Exact full fill: 10 base @ 5 spends the whole 50-quote / 10-base budget, + -- so the residual is empty -> None (the allocation fully settles). + let fullMatch = partialMatch with fillQty = 10.0 + fullLegs = OME.mkMatchTransferLegs fullMatch + OME.remainderFunding buyerAcct buyerBudget fullLegs === None + OME.remainderFunding sellerAcct sellerBudget fullLegs === None + pure () + +-- Order matching enforces both limit prices ----------------------------- +-- +-- A resting bid @ 100 and ask @ 90 cross; any cleared price must sit in +-- [90, 100]. OrderMatchExecution_Execute fetches both orders and refuses a +-- fill outside that band (or with mismatched instruments / quantities / +-- accounts), so a buggy or malicious matcher cannot fill a resting order on +-- terms its owner never agreed to. +-- Prefunded (no legs, next-iteration funding) allocation, as a trader authors +-- it before the operator binds it onto a resting order. +omeAlloc + : Party -> Party -> ContractId V2.AllocationFactory -> Party -> V2.Account + -> TextMap.TextMap Decimal -> Time -> Script (ContractId V2.Allocation) +omeAlloc operator admin factoryCid party acct funding now = do + let settlement = V2.SettlementInfo with + executors = [operator]; id = "OrderMatch-m1"; cid = None + meta = emptyMetadata + arg = WC.mkPrefundedAllocationFactoryAllocate + admin acct settlement None funding now [] [party] emptyExtraArgs + r <- submit party $ exerciseCmd factoryCid arg + case r.output of + V2.AllocationInstructionResult_Completed cid -> pure cid + _ -> abort "alloc must complete" + +-- A resting BTC/USDC order for 10 base at the given limit price, side, and +-- status, optionally bound to a funding allocation. +omeOrder + : Party -> Party -> Party -> Order.Side -> Decimal -> Order.OrderStatus + -> Optional (ContractId V2.Allocation) -> Text -> Script (ContractId Order.Order) +omeOrder operator admin trader side limitPrice status allocationCid ref = + submit operator $ createCmd Order.Order with + operator; trader; admin + baseInstrumentId = "BTC"; quoteInstrumentId = "USDC" + side; limitPrice; remainingQty = 10.0 + expiry = None; status; allocationCid + settlementRef = Order.makeOrderRefFromText ref + +-- A proposed fill of 4 base at the given price, naming the two orders and the +-- two allocations it intends to spend. Exercising its _Execute choice is what +-- the matching tests accept or reject. +omeExec + : Party -> V2.Account -> V2.Account -> ContractId Order.Order + -> ContractId Order.Order -> ContractId V2.Allocation -> ContractId V2.Allocation + -> Decimal -> OME.OrderMatchExecution +omeExec operator buyerAcct sellerAcct buyOrderCid sellOrderCid + buyerAllocationCid sellerAllocationCid fillPrice = + OME.OrderMatchExecution with + operator; matchId = "m1" + match = OME.MatchedOrderPair with + buyerAccount = buyerAcct; sellerAccount = sellerAcct + baseInstrumentId = "BTC"; quoteInstrumentId = "USDC" + fillQty = 4.0; fillPrice + buyOrderCid; sellOrderCid + buyerAllocationCid; sellerAllocationCid + buyerCommittedFunding = TextMap.empty + sellerCommittedFunding = TextMap.empty + +-- | Proves a match outside either resting order's limit price is rejected. +testOrderMatchEnforcesLimitPrice : Script () +testOrderMatchEnforcesLimitPrice = do + operator <- allocateParty "ome-op" + admin <- allocateParty "ome-admin" + buyer <- allocateParty "ome-bid" + seller <- allocateParty "ome-ask" + now <- getTime + + (factoryCid, settleCid) <- setupRegistries admin [operator, buyer, seller] + + let buyerAcct = Utils.basicAccount buyer + sellerAcct = Utils.basicAccount seller + + -- Each side creates a mock allocation carrying the declared quote/base budget. + buyerAllocationCid <- omeAlloc operator admin factoryCid buyer buyerAcct + (TextMap.fromList [("USDC", 400.0)]) now + sellerAllocationCid <- omeAlloc operator admin factoryCid seller sellerAcct + (TextMap.fromList [("BTC", 10.0)]) now + + -- Resting bid: willing to pay up to 100 quote/base for 10 base. + buyOrderCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Funded + (Some buyerAllocationCid) "bid" + -- Resting ask: willing to sell down to 90 quote/base for 10 base. + sellOrderCid <- omeOrder operator admin seller Order.Ask 90.0 Order.OS_Funded + (Some sellerAllocationCid) "ask" + + let mkExec = omeExec operator buyerAcct sellerAcct buyOrderCid sellOrderCid + buyerAllocationCid sellerAllocationCid + + -- Above the bid limit: the buyer would overpay -> rejected. + tooHigh <- submit operator $ createCmd (mkExec 101.0) + submitMustFail operator $ exerciseCmd tooHigh OME.OrderMatchExecution_Execute with + factoryCid = settleCid; extraArgs = emptyExtraArgs + + -- Below the ask limit: the seller would undersell -> rejected. + tooLow <- submit operator $ createCmd (mkExec 89.0) + submitMustFail operator $ exerciseCmd tooLow OME.OrderMatchExecution_Execute with + factoryCid = settleCid; extraArgs = emptyExtraArgs + + -- Inside the band [90, 100]: accepted, settles both allocations. + ok <- submit operator $ createCmd (mkExec 95.0) + res <- submit operator $ exerciseCmd ok OME.OrderMatchExecution_Execute with + factoryCid = settleCid; extraArgs = emptyExtraArgs + length res.settleResult.allocationSettleResults === 2 + pure () + +-- A match can spend only the allocations bound to its orders ------------ +-- +-- The buyer rests two orders with different mock allocation ids. Filling the +-- first while substituting the second's allocation keeps the declared +-- per-instrument batch totals balanced, so the binding check must reject it. +-- | Proves a match cannot substitute another order's funding allocation. +testOrderMatchRejectsAnotherOrdersAllocation : Script () +testOrderMatchRejectsAnotherOrdersAllocation = do + operator <- allocateParty "ome2-op" + admin <- allocateParty "ome2-admin" + buyer <- allocateParty "ome2-bid" + seller <- allocateParty "ome2-ask" + now <- getTime + + (factoryCid, settleCid) <- setupRegistries admin [operator, buyer, seller] + + let buyerAcct = Utils.basicAccount buyer + sellerAcct = Utils.basicAccount seller + quoteFunding = TextMap.fromList [("USDC", 400.0)] + + firstAllocCid <- omeAlloc operator admin factoryCid buyer buyerAcct quoteFunding now + secondAllocCid <- omeAlloc operator admin factoryCid buyer buyerAcct quoteFunding now + sellerAllocationCid <- omeAlloc operator admin factoryCid seller sellerAcct + (TextMap.fromList [("BTC", 10.0)]) now + + buyOrderCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Funded + (Some firstAllocCid) "bid-1" + _secondBidCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Funded + (Some secondAllocCid) "bid-2" + sellOrderCid <- omeOrder operator admin seller Order.Ask 90.0 Order.OS_Funded + (Some sellerAllocationCid) "ask" + + let mkExec buyerAllocationCid = omeExec operator buyerAcct sellerAcct + buyOrderCid sellOrderCid buyerAllocationCid sellerAllocationCid 95.0 + + foreign_ <- submit operator $ createCmd (mkExec secondAllocCid) + submitMustFail operator $ exerciseCmd foreign_ OME.OrderMatchExecution_Execute with + factoryCid = settleCid; extraArgs = emptyExtraArgs + + ok <- submit operator $ createCmd (mkExec firstAllocCid) + res <- submit operator $ exerciseCmd ok OME.OrderMatchExecution_Execute with + factoryCid = settleCid; extraArgs = emptyExtraArgs + length res.settleResult.allocationSettleResults === 2 + pure () + +-- Pending orders cannot be matched -------------------------------------- +-- +-- The execution proposal names an allocation, but the pending order itself is +-- correctly unfunded. The status gate must reject it before settlement. +-- | Proves an unfunded pending order cannot enter settlement. +testOrderMatchRejectsPendingOrder : Script () +testOrderMatchRejectsPendingOrder = do + operator <- allocateParty "ome3-op" + admin <- allocateParty "ome3-admin" + buyer <- allocateParty "ome3-bid" + seller <- allocateParty "ome3-ask" + now <- getTime + + (factoryCid, settleCid) <- setupRegistries admin [operator, buyer, seller] + + let buyerAcct = Utils.basicAccount buyer + sellerAcct = Utils.basicAccount seller + + buyerAllocationCid <- omeAlloc operator admin factoryCid buyer buyerAcct + (TextMap.fromList [("USDC", 400.0)]) now + sellerAllocationCid <- omeAlloc operator admin factoryCid seller sellerAcct + (TextMap.fromList [("BTC", 10.0)]) now + + pendingBidCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Pending + None "bid" + sellOrderCid <- omeOrder operator admin seller Order.Ask 90.0 Order.OS_Funded + (Some sellerAllocationCid) "ask" + + pending <- submit operator $ createCmd (omeExec operator buyerAcct sellerAcct + pendingBidCid sellOrderCid buyerAllocationCid sellerAllocationCid 95.0) + submitMustFail operator $ exerciseCmd pending OME.OrderMatchExecution_Execute with + factoryCid = settleCid; extraArgs = emptyExtraArgs + + fundedBidCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Funded + (Some buyerAllocationCid) "bid" + ok <- submit operator $ createCmd (omeExec operator buyerAcct sellerAcct + fundedBidCid sellOrderCid buyerAllocationCid sellerAllocationCid 95.0) + res <- submit operator $ exerciseCmd ok OME.OrderMatchExecution_Execute with + factoryCid = settleCid; extraArgs = emptyExtraArgs + length res.settleResult.allocationSettleResults === 2 + pure () + +-- Settlement and order roll-forward are atomic ------------------------- +-- +-- The settle archives both funding allocations, so an order left behind +-- pointing at one is uncancellable (Order_Cancel exercises the archived cid) +-- and unfillable. Nothing outside this choice may observe that state, so the +-- choice itself archives the filled orders, rolls each remainder onto the +-- allocation the settle minted, and records the trade. +-- | Proves one transaction settles a partial fill and rolls both orders forward. +testOrderMatchRollsOrdersForwardAtomically : Script () +testOrderMatchRollsOrdersForwardAtomically = do + operator <- allocateParty "ome5-op" + admin <- allocateParty "ome5-admin" + buyer <- allocateParty "ome5-bid" + seller <- allocateParty "ome5-ask" + now <- getTime + + (factoryCid, settleCid) <- setupRegistries admin [operator, buyer, seller] + + let buyerAcct = Utils.basicAccount buyer + sellerAcct = Utils.basicAccount seller + + buyerAllocationCid <- omeAlloc operator admin factoryCid buyer buyerAcct + (TextMap.fromList [("USDC", 400.0)]) now + sellerAllocationCid <- omeAlloc operator admin factoryCid seller sellerAcct + (TextMap.fromList [("BTC", 10.0)]) now + + buyOrderCid <- omeOrder operator admin buyer Order.Bid 100.0 Order.OS_Funded + (Some buyerAllocationCid) "bid" + sellOrderCid <- omeOrder operator admin seller Order.Ask 90.0 Order.OS_Funded + (Some sellerAllocationCid) "ask" + + execCid <- submit operator $ createCmd (omeExec operator buyerAcct sellerAcct + buyOrderCid sellOrderCid buyerAllocationCid sellerAllocationCid 95.0) + res <- submit operator $ exerciseCmd execCid OME.OrderMatchExecution_Execute with + factoryCid = settleCid; extraArgs = emptyExtraArgs + + -- Both orders filled 4 of 10, so both roll forward onto the allocation the + -- same transaction minted for them. The cids the orders were bound to are + -- gone with the settle. + isSome res.buyRemainderCid === True + isSome res.sellRemainderCid === True + liveOrders <- query @Order.Order operator + -- The ACS comes back ordered by contract id, so compare as a set. + sort (map fst liveOrders) + === sort [fromSome res.buyRemainderCid, fromSome res.sellRemainderCid] + Some buyRemainder <- queryContractId operator (fromSome res.buyRemainderCid) + Some sellRemainder <- queryContractId operator (fromSome res.sellRemainderCid) + buyRemainder.allocationCid === res.buyerNextAllocationCid + sellRemainder.allocationCid === res.sellerNextAllocationCid + [buyRemainder.remainingQty, sellRemainder.remainingQty] === [6.0, 6.0] + [buyRemainder.status, sellRemainder.status] + === [Order.OS_PartiallyFilled, Order.OS_PartiallyFilled] + + -- The fill is durable trade history: OrderMatchExecution is consumed by the + -- choice, so nothing about the match would survive in the ACS without it. + settled <- query @MT.SettledTrade operator + map (Some . fst) settled === [res.settledTradeCid] + [ t.transferLegs | (_, t) <- settled ] === [OME.mkMatchTransferLegs + (OME.MatchedOrderPair with + buyerAccount = buyerAcct; sellerAccount = sellerAcct + baseInstrumentId = "BTC"; quoteInstrumentId = "USDC" + fillQty = 4.0; fillPrice = 95.0)] + pure () + +-- Exhausting the funding budget closes the order ------------------------ +-- +-- A tiny residual quantity is not recreated when decimal rounding has already +-- consumed the entire declared committed budget. Any remainder contract must +-- point to a non-empty next-allocation budget. +-- | Proves an exhausted budget closes the remainder instead of creating an orphan. +testOrderMatchClosesRemainderWhenBudgetIsExhausted : Script () +testOrderMatchClosesRemainderWhenBudgetIsExhausted = do + operator <- allocateParty "ome6-op" + admin <- allocateParty "ome6-admin" + buyer <- allocateParty "ome6-bid" + seller <- allocateParty "ome6-ask" + now <- getTime + + (factoryCid, settleCid) <- setupRegistries admin [operator, buyer, seller] + + let buyerAcct = Utils.basicAccount buyer + sellerAcct = Utils.basicAccount seller + price = 0.000001 + restingQty = 1000.0 + fillQty = 999.99996 + committedQuote = restingQty * price + + -- The collision: a strictly partial fill whose spend equals the whole budget. + (fillQty < restingQty) === True + fillQty * price === committedQuote + + buyerAllocationCid <- omeAlloc operator admin factoryCid buyer buyerAcct + (TextMap.fromList [("USDC", committedQuote)]) now + sellerAllocationCid <- omeAlloc operator admin factoryCid seller sellerAcct + (TextMap.fromList [("BTC", fillQty)]) now + + let mkOrder trader side limitPrice remainingQty allocationCid ref = + submit operator $ createCmd Order.Order with + operator; trader; admin + baseInstrumentId = "BTC"; quoteInstrumentId = "USDC" + side; limitPrice; remainingQty + expiry = None; status = Order.OS_Funded + allocationCid = Some allocationCid + settlementRef = Order.makeOrderRefFromText ref + buyOrderCid <- mkOrder buyer Order.Bid price restingQty buyerAllocationCid "dust-bid" + sellOrderCid <- mkOrder seller Order.Ask price fillQty sellerAllocationCid "dust-ask" + + execCid <- submit operator $ createCmd OME.OrderMatchExecution with + operator; matchId = "dust" + match = OME.MatchedOrderPair with + buyerAccount = buyerAcct; sellerAccount = sellerAcct + baseInstrumentId = "BTC"; quoteInstrumentId = "USDC" + fillQty; fillPrice = price + buyOrderCid; sellOrderCid + buyerAllocationCid; sellerAllocationCid + buyerCommittedFunding = TextMap.empty + sellerCommittedFunding = TextMap.empty + res <- submit operator $ exerciseCmd execCid OME.OrderMatchExecution_Execute with + factoryCid = settleCid; extraArgs = emptyExtraArgs + + -- 0.00004 base is unfilled and the quote behind it is gone, so nothing rolls + -- forward on the bid. + restingQty - fillQty === 0.00004 + res.buyerNextAllocationCid === None + res.buyRemainderCid === None + res.sellRemainderCid === None + liveOrders <- query @Order.Order operator + [ cid | (cid, o) <- liveOrders, isNone o.allocationCid ] === [] + liveOrders === [] + pure () diff --git a/trading-tests/CantonDex/Tests/PolicyReceiptTests.daml b/trading-tests/CantonDex/Tests/PolicyReceiptTests.daml index c321ce5f..63f0e357 100644 --- a/trading-tests/CantonDex/Tests/PolicyReceiptTests.daml +++ b/trading-tests/CantonDex/Tests/PolicyReceiptTests.daml @@ -20,6 +20,8 @@ import DA.TextMap qualified as TextMap import Daml.Script +import Splice.Api.Token.AllocationV2 qualified as V2 +import Splice.Api.Token.HoldingV2 qualified as V2 import Splice.Api.Token.MetadataV1 import CantonDex.Dex.MatchedTrade qualified as MT @@ -45,6 +47,17 @@ mkValidReceipt venue ranked acceptedDealer acceptedRank now = signedAt = now signature = "0xdeadbeef" +-- A minimal valid bilateral leg keeps the receipt tests focused on receipt +-- validation rather than relying on an empty, non-settleable trade. +mkLeg : Party -> Party -> V2.TransferLeg +mkLeg sender receiver = V2.TransferLeg with + transferLegId = "receipt-test-leg" + sender = V2.Account with owner = Some sender; provider = None; id = "" + receiver = V2.Account with owner = Some receiver; provider = None; id = "" + amount = 1.0 + instrumentId = "TEST" + meta = emptyMetadata + -- 1. Encoding test ---------------------------------------------------------- -- | Proves a receipt encodes its header and ranked-dealer rows in metadata. @@ -157,7 +170,7 @@ testMatchedTradeWithNoReceipt = do cid <- submit venue $ createCmd MT.MatchedTrade with venue admin - transferLegs = [] + transferLegs = [mkLeg venue admin] settlementDeadline = None policyReceipt = None Some _ <- queryContractId venue cid @@ -175,7 +188,7 @@ testMatchedTradeWithGoodReceipt = do cid <- submit venue $ createCmd MT.MatchedTrade with venue admin - transferLegs = [] + transferLegs = [mkLeg venue admin] settlementDeadline = None policyReceipt = Some r Some _ <- queryContractId venue cid @@ -195,7 +208,7 @@ testMatchedTradeRejectsWrongSigner = do submitMustFail venue $ createCmd MT.MatchedTrade with venue admin - transferLegs = [] + transferLegs = [mkLeg venue admin] settlementDeadline = None policyReceipt = Some r pure () @@ -213,7 +226,7 @@ testMatchedTradeRejectsMalformed = do submitMustFail venue $ createCmd MT.MatchedTrade with venue admin - transferLegs = [] + transferLegs = [mkLeg venue admin] settlementDeadline = None policyReceipt = Some r pure () @@ -235,7 +248,7 @@ testSettlementInfoCarriesReceipt = do cid <- submit venue $ createCmd MT.MatchedTrade with venue admin - transferLegs = [] + transferLegs = [mkLeg venue admin] settlementDeadline = None policyReceipt = Some r Some trade <- queryContractId venue cid @@ -256,7 +269,7 @@ testSettlementInfoEmptyWithoutReceipt = do cid <- submit venue $ createCmd MT.MatchedTrade with venue admin - transferLegs = [] + transferLegs = [mkLeg venue admin] settlementDeadline = None policyReceipt = None Some trade <- queryContractId venue cid diff --git a/trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml b/trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml index 0e02b363..11807970 100644 --- a/trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml +++ b/trading-tests/CantonDex/Tests/PoolLiquidityRulesTests.daml @@ -30,6 +30,7 @@ import Splice.Testing.Utils (emptyExtraArgs) import Splice.Util.Token.Wallet.BatchingUtilityV2 qualified as BU import CantonDex.Dex.Pool +import CantonDex.Dex.PoolSlice qualified as Slice import CantonDex.Dex.PoolState qualified as PS import CantonDex.Dex.PoolModel qualified as PM import CantonDex.Dex.PoolLiquidityRules qualified as Dvp @@ -162,6 +163,174 @@ requestRemove fx holder baseOuts quoteOuts lpBurnAmount settleAt now = poolCid = fx.poolCid; holder; baseOuts; quoteOuts; lpBurnAmount requestedAt = now; settleAt +-- Complete one allocation returned by a PoolLiquidityRules preview. The +-- fixture uses one Registry.V2 for both asset and LP-token operations. +completePlannedAllocation + : Fixture -> Party -> V2.AllocationFactory_Allocate + -> Script (ContractId V2.Allocation) +completePlannedAllocation fx actor allocationArg = do + result <- submit actor $ exerciseCmd fx.factoryCid allocationArg + case result.output of + V2.AllocationInstructionResult_Completed cid -> pure cid + _ -> abort "planned allocation must complete" + +settlePreparedAdd + : Fixture -> Dvp.AddLiquidityPreparationArgs -> Time + -> Script Dvp.PoolLiquidityRules_SettleAddResult +settlePreparedAdd fx preparation requestedAt = do + allocationPlan <- submit (actAs [fx.operator, fx.registry]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation + requestedAt + operatorBaseReceiverCid <- + completePlannedAllocation fx fx.operator allocationPlan.baseReceiver + operatorQuoteReceiverCid <- + completePlannedAllocation fx fx.operator allocationPlan.quoteReceiver + registrarMintCid <- + completePlannedAllocation fx fx.registry allocationPlan.lpMintSender + _settlementPlan <- submit (actAs [fx.operator, fx.registry]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddSettlement with + preparation + operatorBaseReceiverCid + operatorQuoteReceiverCid + registrarMintCid + submit (actAs [fx.operator, fx.registry]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with + expectedPoolId = preparation.expectedPoolId + poolCid = preparation.poolCid + poolStateCid = preparation.poolStateCid + lpPolicyCid = preparation.lpPolicyCid + requestCid = preparation.requestCid + acceptanceCid = preparation.acceptanceCid + recipient = preparation.recipient + lpBaseDepositCid = preparation.lpBaseDepositCid + lpQuoteDepositCid = preparation.lpQuoteDepositCid + lpReceiptCid = preparation.lpReceiptCid + baseFactoryCid = fx.factoryCid + quoteFactoryCid = fx.factoryCid + lpFactoryCid = fx.factoryCid + baseQuoteSettleCid = fx.settleCid + lpSettleCid = fx.settleCid + baseAmount = preparation.baseAmount + quoteAmount = preparation.quoteAmount + minLpTokens = preparation.minLpTokens + knownTotalLpSupply = preparation.knownTotalLpSupply + requestedAt + poolAdminExtraArgs = emptyExtraArgs + lpRegistrarExtraArgs = emptyExtraArgs + operatorBaseReceiverCid = Some operatorBaseReceiverCid + operatorQuoteReceiverCid = Some operatorQuoteReceiverCid + registrarMintCid = Some registrarMintCid + +-- Exercise the deployed settle shape without the optional staged-allocation +-- cids. The choice must create those allocations through the original factory +-- fields, preserving callers compiled against the previous package version. +settlePreparedAddLegacy + : Fixture -> Dvp.AddLiquidityPreparationArgs -> Time + -> Script Dvp.PoolLiquidityRules_SettleAddResult +settlePreparedAddLegacy fx preparation requestedAt = + submit (actAs [fx.operator, fx.registry]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with + expectedPoolId = preparation.expectedPoolId + poolCid = preparation.poolCid + poolStateCid = preparation.poolStateCid + lpPolicyCid = preparation.lpPolicyCid + requestCid = preparation.requestCid + acceptanceCid = preparation.acceptanceCid + recipient = preparation.recipient + lpBaseDepositCid = preparation.lpBaseDepositCid + lpQuoteDepositCid = preparation.lpQuoteDepositCid + lpReceiptCid = preparation.lpReceiptCid + baseFactoryCid = fx.factoryCid + quoteFactoryCid = fx.factoryCid + lpFactoryCid = fx.factoryCid + baseQuoteSettleCid = fx.settleCid + lpSettleCid = fx.settleCid + baseAmount = preparation.baseAmount + quoteAmount = preparation.quoteAmount + minLpTokens = preparation.minLpTokens + knownTotalLpSupply = preparation.knownTotalLpSupply + requestedAt + poolAdminExtraArgs = emptyExtraArgs + lpRegistrarExtraArgs = emptyExtraArgs + operatorBaseReceiverCid = None + operatorQuoteReceiverCid = None + registrarMintCid = None + +settlePreparedRemove + : Fixture -> Dvp.RemoveLiquidityPreparationArgs -> Time + -> Script Dvp.PoolLiquidityRules_SettleRemoveResult +settlePreparedRemove fx preparation requestedAt = do + allocationPlan <- submit (actAs [fx.operator, fx.registry]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewRemoveAllocations with + preparation + requestedAt + registrarBurnReceiverCid <- + completePlannedAllocation fx fx.registry allocationPlan.lpBurnReceiver + _settlementPlan <- submit (actAs [fx.operator, fx.registry]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewRemoveSettlement with + preparation + registrarBurnReceiverCid + submit (actAs [fx.operator, fx.registry]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleRemoveLiquidity with + expectedPoolId = preparation.expectedPoolId + poolCid = preparation.poolCid + poolStateCid = preparation.poolStateCid + lpPolicyCid = preparation.lpPolicyCid + requestCid = preparation.requestCid + acceptanceCid = preparation.acceptanceCid + holder = preparation.holder + lpTokensToRedeem = preparation.lpTokensToRedeem + knownTotalLpSupply = preparation.knownTotalLpSupply + minBaseOut = preparation.minBaseOut + minQuoteOut = preparation.minQuoteOut + baseSliceCids = preparation.baseSliceCids + quoteSliceCids = preparation.quoteSliceCids + holderBaseReceiptCid = preparation.holderBaseReceiptCid + holderQuoteReceiptCid = preparation.holderQuoteReceiptCid + holderBurnSenderCid = preparation.holderBurnSenderCid + baseFactoryCid = fx.factoryCid + quoteFactoryCid = fx.factoryCid + lpFactoryCid = fx.factoryCid + baseQuoteSettleCid = fx.settleCid + lpSettleCid = fx.settleCid + requestedAt + poolAdminExtraArgs = emptyExtraArgs + lpRegistrarExtraArgs = emptyExtraArgs + registrarBurnReceiverCid = Some registrarBurnReceiverCid + +settlePreparedRemoveLegacy + : Fixture -> Dvp.RemoveLiquidityPreparationArgs -> Time + -> Script Dvp.PoolLiquidityRules_SettleRemoveResult +settlePreparedRemoveLegacy fx preparation requestedAt = + submit (actAs [fx.operator, fx.registry]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleRemoveLiquidity with + expectedPoolId = preparation.expectedPoolId + poolCid = preparation.poolCid + poolStateCid = preparation.poolStateCid + lpPolicyCid = preparation.lpPolicyCid + requestCid = preparation.requestCid + acceptanceCid = preparation.acceptanceCid + holder = preparation.holder + lpTokensToRedeem = preparation.lpTokensToRedeem + knownTotalLpSupply = preparation.knownTotalLpSupply + minBaseOut = preparation.minBaseOut + minQuoteOut = preparation.minQuoteOut + baseSliceCids = preparation.baseSliceCids + quoteSliceCids = preparation.quoteSliceCids + holderBaseReceiptCid = preparation.holderBaseReceiptCid + holderQuoteReceiptCid = preparation.holderQuoteReceiptCid + holderBurnSenderCid = preparation.holderBurnSenderCid + baseFactoryCid = fx.factoryCid + quoteFactoryCid = fx.factoryCid + lpFactoryCid = fx.factoryCid + baseQuoteSettleCid = fx.settleCid + lpSettleCid = fx.settleCid + requestedAt + poolAdminExtraArgs = emptyExtraArgs + lpRegistrarExtraArgs = emptyExtraArgs + registrarBurnReceiverCid = None + -- Drive a DvP add of `baseAmount` BTC + `quoteAmount` USDC by `lp`, -- returning the settle result. Builds the LP's deposit + receipt -- allocations matching the legs PoolLiquidityRules settles. @@ -194,17 +363,15 @@ dvpAdd fx stateCid policyCid lp baseAmount quoteAmount knownSupply reserveBase r lpBaseDeposit <- mkAlloc fx.factoryCid lp settlement baseSpec [toInterfaceContractId baseHold] now lpQuoteDeposit <- mkAlloc fx.factoryCid lp settlement quoteSpec [toInterfaceContractId quoteHold] now lpReceipt <- mkAlloc fx.factoryCid lp settlement receiptSpec [] now - submit (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = stateCid - lpPolicyCid = policyCid; requestCid = Some reqCid; acceptanceCid = None; recipient = lp - lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit - lpReceiptCid = lpReceipt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount; quoteAmount; minLpTokens = 0.0; knownTotalLpSupply = knownSupply - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = stateCid + lpPolicyCid = policyCid; requestCid = Some reqCid; acceptanceCid = None + recipient = lp + lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit + lpReceiptCid = lpReceipt + baseAmount; quoteAmount; minLpTokens = 0.0 + knownTotalLpSupply = knownSupply + settlePreparedAddLegacy fx preparation now -- First-funding add with an explicit receipt amount, allowing a test to place -- the requested mint immediately above or below the symmetric dust bound. @@ -232,17 +399,14 @@ dvpAddWithLp fx stateCid policyCid lp baseAmount quoteAmount lpAmount now = do lpBaseDeposit <- mkAlloc fx.factoryCid lp settlement baseSpec [toInterfaceContractId baseHold] now lpQuoteDeposit <- mkAlloc fx.factoryCid lp settlement quoteSpec [toInterfaceContractId quoteHold] now lpReceipt <- mkAlloc fx.factoryCid lp settlement receiptSpec [] now - submit (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = stateCid - lpPolicyCid = policyCid; requestCid = Some reqCid; acceptanceCid = None; recipient = lp - lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit - lpReceiptCid = lpReceipt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount; quoteAmount; minLpTokens = 0.0; knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = stateCid + lpPolicyCid = policyCid; requestCid = Some reqCid; acceptanceCid = None + recipient = lp + lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit + lpReceiptCid = lpReceipt + baseAmount; quoteAmount; minLpTokens = 0.0; knownTotalLpSupply = 0.0 + settlePreparedAdd fx preparation now -- Happy path: a first add funds an empty pool and mints LP tokens to the -- provider, all in one atomic settlement. Asserts the pool turns Active with the @@ -275,6 +439,19 @@ testDvpAddLiquidity = do let lpHoldings = filter (\(_, h) -> h.instrumentId == lpId && h.owner == alice && not h.locked) aliceHoldings length lpHoldings === 1 (snd (head lpHoldings)).amount === expectedLp + + -- Policy supply changes and reserve-slice records must represent real, + -- positive value. Failed submissions leave the live policy untouched. + submitMustFail fx.registry $ exerciseCmd res.lpPolicyCid LP.LPTokenPolicy_RecordMint with + amount = 0.0 + submitMustFail fx.registry $ exerciseCmd res.lpPolicyCid LP.LPTokenPolicy_RecordBurn with + amount = expectedLp + 0.0000000001 + submitMustFail fx.operator $ createCmd Slice.PoolSlice with + poolId = fx.poolId + operator = fx.operator + side = BaseSide + allocationCid = baseSlice.allocationCid + amount = 0.0 pure () -- Off-ratio add: LP tokens are minted against the limiting side, so the @@ -367,20 +544,16 @@ testDvpRemoveDeliversToHolder = do quoteReceipt <- mkAlloc fx.factoryCid alice settlement quoteReceiptSpec [] now burnSender <- mkAlloc fx.factoryCid alice settlement burnSpec [toInterfaceContractId lpHoldCid] now - remRes <- submit (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleRemoveLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid - poolStateCid = addRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid - requestCid = Some reqCid; acceptanceCid = None - holder = alice; lpTokensToRedeem = lpToRedeem; knownTotalLpSupply = lpAmount - minBaseOut = 0.0; minQuoteOut = 0.0 - baseSliceCids = [addRes.baseSliceCid]; quoteSliceCids = [addRes.quoteSliceCid] - holderBaseReceiptCid = baseReceipt; holderQuoteReceiptCid = quoteReceipt - holderBurnSenderCid = burnSender - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + let preparation = Dvp.RemoveLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid + poolStateCid = addRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid + requestCid = Some reqCid; acceptanceCid = None + holder = alice; lpTokensToRedeem = lpToRedeem; knownTotalLpSupply = lpAmount + minBaseOut = 0.0; minQuoteOut = 0.0 + baseSliceCids = [addRes.baseSliceCid]; quoteSliceCids = [addRes.quoteSliceCid] + holderBaseReceiptCid = baseReceipt; holderQuoteReceiptCid = quoteReceipt + holderBurnSenderCid = burnSender + remRes <- settlePreparedRemoveLegacy fx preparation now -- Reserves, slices, and LP supply all retain the same 60% remainder. Some state <- queryContractId fx.operator remRes.poolStateCid @@ -438,17 +611,17 @@ testStaleQuoteRejected = do bBase <- mkAlloc fx.factoryCid bob settlement bBaseSpec [toInterfaceContractId bBtc] now bQuote <- mkAlloc fx.factoryCid bob settlement bQuoteSpec [toInterfaceContractId bUsdc] now bRcpt <- mkAlloc fx.factoryCid bob settlement bRcptSpec [] now + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid + poolStateCid = addRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid + requestCid = Some bReq; acceptanceCid = None; recipient = bob + lpBaseDepositCid = bBase; lpQuoteDepositCid = bQuote; lpReceiptCid = bRcpt + baseAmount = 1.0; quoteAmount = 20000.0; minLpTokens = 0.0 + knownTotalLpSupply = 0.0 -- STALE: pool is funded now submitMustFail (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid - poolStateCid = addRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid - requestCid = Some bReq; acceptanceCid = None; recipient = bob - lpBaseDepositCid = bBase; lpQuoteDepositCid = bQuote; lpReceiptCid = bRcpt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid; lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount = 1.0; quoteAmount = 20000.0; minLpTokens = 0.0 - knownTotalLpSupply = 0.0 -- STALE: pool is funded now - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation + requestedAt = now pure () -- The settle choices are co-controlled (operator, lpRegistrar): the @@ -460,22 +633,43 @@ testSettleRequiresCoControl = do fx <- setup [alice] now <- getTime addRes <- dvpAdd fx fx.stateCid fx.policyCid alice 10.0 200000.0 0.0 0.0 0.0 now + let preparation = Dvp.RemoveLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid + poolStateCid = addRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid + requestCid = Some (coerceContractId addRes.baseSliceCid); acceptanceCid = None + holder = alice; lpTokensToRedeem = PM.sqrtDecimal 2000000.0 + knownTotalLpSupply = PM.sqrtDecimal 2000000.0 + minBaseOut = 0.0; minQuoteOut = 0.0 + baseSliceCids = [addRes.baseSliceCid]; quoteSliceCids = [addRes.quoteSliceCid] + holderBaseReceiptCid = coerceContractId addRes.baseSliceCid + holderQuoteReceiptCid = coerceContractId addRes.baseSliceCid + holderBurnSenderCid = coerceContractId addRes.baseSliceCid -- Operator alone (no lpRegistrar) cannot settle a remove. submitMustFail fx.operator $ exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleRemoveLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid - poolStateCid = addRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid - requestCid = Some (coerceContractId addRes.baseSliceCid); acceptanceCid = None - holder = alice; lpTokensToRedeem = PM.sqrtDecimal 2000000.0 - knownTotalLpSupply = PM.sqrtDecimal 2000000.0 - minBaseOut = 0.0; minQuoteOut = 0.0 - baseSliceCids = [addRes.baseSliceCid]; quoteSliceCids = [addRes.quoteSliceCid] - holderBaseReceiptCid = coerceContractId addRes.baseSliceCid - holderQuoteReceiptCid = coerceContractId addRes.baseSliceCid - holderBurnSenderCid = coerceContractId addRes.baseSliceCid - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid; lpFactoryCid = fx.factoryCid + expectedPoolId = preparation.expectedPoolId + poolCid = preparation.poolCid + poolStateCid = preparation.poolStateCid + lpPolicyCid = preparation.lpPolicyCid + requestCid = preparation.requestCid + acceptanceCid = preparation.acceptanceCid + holder = preparation.holder + lpTokensToRedeem = preparation.lpTokensToRedeem + knownTotalLpSupply = preparation.knownTotalLpSupply + minBaseOut = preparation.minBaseOut + minQuoteOut = preparation.minQuoteOut + baseSliceCids = preparation.baseSliceCids + quoteSliceCids = preparation.quoteSliceCids + holderBaseReceiptCid = preparation.holderBaseReceiptCid + holderQuoteReceiptCid = preparation.holderQuoteReceiptCid + holderBurnSenderCid = preparation.holderBurnSenderCid + baseFactoryCid = fx.factoryCid + quoteFactoryCid = fx.factoryCid + lpFactoryCid = fx.factoryCid baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + requestedAt = now + poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + registrarBurnReceiverCid = Some (coerceContractId addRes.baseSliceCid) pure () -- Multi-slice remove: two adds create two slices per side; a full @@ -523,20 +717,17 @@ testDvpMultiSliceRemove = do quoteReceipt <- mkAlloc fx.factoryCid alice settlement quoteReceiptSpec [] now burnSender <- mkAlloc fx.factoryCid alice settlement burnSpec (map toInterfaceContractId lpCids) now - remRes <- submit (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleRemoveLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid - poolStateCid = add2.poolStateCid; lpPolicyCid = add2.lpPolicyCid - requestCid = Some reqCid; acceptanceCid = None - holder = alice; lpTokensToRedeem = total; knownTotalLpSupply = total - minBaseOut = 0.0; minQuoteOut = 0.0 - baseSliceCids = [add1.baseSliceCid, add2.baseSliceCid] - quoteSliceCids = [add1.quoteSliceCid, add2.quoteSliceCid] - holderBaseReceiptCid = baseReceipt; holderQuoteReceiptCid = quoteReceipt - holderBurnSenderCid = burnSender - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid; lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + let preparation = Dvp.RemoveLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid + poolStateCid = add2.poolStateCid; lpPolicyCid = add2.lpPolicyCid + requestCid = Some reqCid; acceptanceCid = None + holder = alice; lpTokensToRedeem = total; knownTotalLpSupply = total + minBaseOut = 0.0; minQuoteOut = 0.0 + baseSliceCids = [add1.baseSliceCid, add2.baseSliceCid] + quoteSliceCids = [add1.quoteSliceCid, add2.quoteSliceCid] + holderBaseReceiptCid = baseReceipt; holderQuoteReceiptCid = quoteReceipt + holderBurnSenderCid = burnSender + remRes <- settlePreparedRemove fx preparation now -- Both slices per side consumed; pool drained; alice got it all back. remRes.baseReturned === 15.0 @@ -605,20 +796,20 @@ testExpiredAddRejected = do lpBaseDeposit <- mkAlloc fx.factoryCid alice settlement baseSpec [toInterfaceContractId baseHold] now lpQuoteDeposit <- mkAlloc fx.factoryCid alice settlement quoteSpec [toInterfaceContractId quoteHold] now lpReceipt <- mkAlloc fx.factoryCid alice settlement receiptSpec [] now + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid + lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None + recipient = alice + lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit + lpReceiptCid = lpReceipt + baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 + knownTotalLpSupply = 0.0 -- Even with a fresh `requestedAt`, the settle must abort on the expired -- ledger-anchored deadline. submitMustFail (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid - lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None; recipient = alice - lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit - lpReceiptCid = lpReceipt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 - knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation + requestedAt = now pure () -- Symmetric to the add case, on the remove side: a past deadline on a @@ -653,20 +844,19 @@ testExpiredRemoveRejected = do baseReceipt <- mkAlloc fx.factoryCid alice settlement baseReceiptSpec [] now quoteReceipt <- mkAlloc fx.factoryCid alice settlement quoteReceiptSpec [] now burnSender <- mkAlloc fx.factoryCid alice settlement burnSpec [toInterfaceContractId lpHoldCid] now + let preparation = Dvp.RemoveLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid + poolStateCid = addRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid + requestCid = Some reqCid; acceptanceCid = None + holder = alice; lpTokensToRedeem = lpAmount; knownTotalLpSupply = lpAmount + minBaseOut = 0.0; minQuoteOut = 0.0 + baseSliceCids = [addRes.baseSliceCid]; quoteSliceCids = [addRes.quoteSliceCid] + holderBaseReceiptCid = baseReceipt; holderQuoteReceiptCid = quoteReceipt + holderBurnSenderCid = burnSender submitMustFail (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleRemoveLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid - poolStateCid = addRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid - requestCid = Some reqCid; acceptanceCid = None - holder = alice; lpTokensToRedeem = lpAmount; knownTotalLpSupply = lpAmount - minBaseOut = 0.0; minQuoteOut = 0.0 - baseSliceCids = [addRes.baseSliceCid]; quoteSliceCids = [addRes.quoteSliceCid] - holderBaseReceiptCid = baseReceipt; holderQuoteReceiptCid = quoteReceipt - holderBurnSenderCid = burnSender - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewRemoveAllocations with + preparation + requestedAt = now pure () -- A supplied allocation whose spec is not one @@ -699,18 +889,18 @@ testSettleRejectsForeignAllocation = do lpBaseDeposit <- mkAlloc fx.factoryCid alice settlement forgedSpec [toInterfaceContractId baseHold] now lpQuoteDeposit <- mkAlloc fx.factoryCid alice settlement quoteSpec [toInterfaceContractId quoteHold] now lpReceipt <- mkAlloc fx.factoryCid alice settlement receiptSpec [] now + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid + lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None + recipient = alice + lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit + lpReceiptCid = lpReceipt + baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 + knownTotalLpSupply = 0.0 submitMustFail (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid - lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None; recipient = alice - lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit - lpReceiptCid = lpReceipt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 - knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation + requestedAt = now pure () -- The request's `settleAt` is the single @@ -742,18 +932,18 @@ testSettleRejectsExpiredRequest = do lpBaseDeposit <- mkAlloc fx.factoryCid alice settlement baseSpec [toInterfaceContractId baseHold] now lpQuoteDeposit <- mkAlloc fx.factoryCid alice settlement quoteSpec [toInterfaceContractId quoteHold] now lpReceipt <- mkAlloc fx.factoryCid alice settlement receiptSpec [] now + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid + lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None + recipient = alice + lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit + lpReceiptCid = lpReceipt + baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 + knownTotalLpSupply = 0.0 submitMustFail (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid - lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None; recipient = alice - lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit - lpReceiptCid = lpReceipt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 - knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation + requestedAt = now pure () -- The minted amount is read from the LP receipt but bounded: a receipt claiming @@ -785,18 +975,18 @@ testAddRejectsOverMint = do lpBaseDeposit <- mkAlloc fx.factoryCid alice settlement baseSpec [toInterfaceContractId baseHold] now lpQuoteDeposit <- mkAlloc fx.factoryCid alice settlement quoteSpec [toInterfaceContractId quoteHold] now lpReceipt <- mkAlloc fx.factoryCid alice settlement receiptSpec [] now + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid + lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None + recipient = alice + lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit + lpReceiptCid = lpReceipt + baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 + knownTotalLpSupply = 0.0 submitMustFail (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid - lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None; recipient = alice - lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit - lpReceiptCid = lpReceipt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 - knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation + requestedAt = now pure () -- The over-mint bound is symmetric with the dust tolerance. @@ -870,6 +1060,14 @@ testAcceptCreatesEvidenceThenSettleSucceeds = do lpQuoteDeposit <- mkAlloc fx.factoryCid alice settlement quoteSpec [toInterfaceContractId quoteHold] now lpReceipt <- mkAlloc fx.factoryCid alice settlement receiptSpec [] now + let preparation requestCid acceptanceCid = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid + lpPolicyCid = fx.policyCid; requestCid; acceptanceCid; recipient = alice + lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit + lpReceiptCid = lpReceipt + baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 + knownTotalLpSupply = 0.0 + -- alice (the request observer) exercises the standard accept → consumes the -- request and leaves the acceptance evidence. _ <- submit alice $ exerciseCmd (toInterfaceContractId @V2.AllocationRequest reqCid) @@ -877,53 +1075,23 @@ testAcceptCreatesEvidenceThenSettleSucceeds = do -- Settlement must reject a consumed request without acceptance evidence. submitMustFail (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid - lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None - recipient = alice - lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit - lpReceiptCid = lpReceipt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 - knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation = preparation (Some reqCid) None + requestedAt = now -- Settling with neither request nor evidence fails (no binding to validate -- against): proves the evidence is load-bearing, not optional. submitMustFail (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid - lpPolicyCid = fx.policyCid; requestCid = None; acceptanceCid = None - recipient = alice - lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit - lpReceiptCid = lpReceipt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 - knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation = preparation None None + requestedAt = now -- The operator discovers the acceptance evidence and settles against it. accs <- query @LAR.LiquidityAllocationAcceptance fx.operator accCid <- case accs of ((cid, _) :: _) -> pure cid [] -> abort "accept must leave acceptance evidence" - res <- submit (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid - lpPolicyCid = fx.policyCid; requestCid = None; acceptanceCid = Some accCid - recipient = alice - lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit - lpReceiptCid = lpReceipt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 - knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + res <- settlePreparedAdd fx (preparation None (Some accCid)) now res.lpTokensMinted === lpAmount -- The evidence is consumed by settle (cleanup); none left behind. remaining <- query @LAR.LiquidityAllocationAcceptance fx.operator @@ -1008,19 +1176,15 @@ testBatchedUtilityAddLiquidity = do ((cid, _) :: _) -> pure cid [] -> abort "batched accept must leave acceptance evidence" - res2 <- submit (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid - lpPolicyCid = fx.policyCid; requestCid = None; acceptanceCid = Some accCid - recipient = alice - lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit - lpReceiptCid = lpReceipt - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 - knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid + lpPolicyCid = fx.policyCid; requestCid = None; acceptanceCid = Some accCid + recipient = alice + lpBaseDepositCid = lpBaseDeposit; lpQuoteDepositCid = lpQuoteDeposit + lpReceiptCid = lpReceipt + baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0 + knownTotalLpSupply = 0.0 + res2 <- settlePreparedAdd fx preparation now res2.lpTokensMinted === lpAmount remaining <- query @LAR.LiquidityAllocationAcceptance fx.operator remaining === [] diff --git a/trading-tests/CantonDex/Tests/PoolRoundingTests.daml b/trading-tests/CantonDex/Tests/PoolRoundingTests.daml index 3b93fcfa..e6b8a28d 100644 --- a/trading-tests/CantonDex/Tests/PoolRoundingTests.daml +++ b/trading-tests/CantonDex/Tests/PoolRoundingTests.daml @@ -93,8 +93,9 @@ testFullRedemptionShareIsExact = do PM.floorMul 993.0486593843 share === 993.0486593843 pure () --- The end-to-end proof: a real swap run through the full settlement machinery --- must leave base*quote (the pool's invariant `k`) no lower than it started. +-- The settlement-backed rounding proof: a real-holding swap run through the +-- Daml settlement machinery must leave base*quote (the pool's invariant `k`) +-- no lower than it started. -- -- A zero-fee pool has no fee slack to absorb a rounding error. 7 USDC into a -- 1000/1000 pool prices at 7000/1007, whose rounded quotient sits above the diff --git a/trading-tests/CantonDex/Tests/PoolStateInvariantTests.daml b/trading-tests/CantonDex/Tests/PoolStateInvariantTests.daml index 8d27030a..ebe84298 100644 --- a/trading-tests/CantonDex/Tests/PoolStateInvariantTests.daml +++ b/trading-tests/CantonDex/Tests/PoolStateInvariantTests.daml @@ -142,20 +142,16 @@ testReconcileAfterAddSwapRemove = do (PLT.mkSpec fx.registry aliceAcct [quoteOutLeg] False) [] now burnSender <- PLT.mkAlloc fx.factoryCid alice settlement (PLT.mkSpec fx.registry aliceAcct [burnLeg] True) [toInterfaceContractId lpHoldCid] now - remRes <- submit (actAs [fx.operator, fx.registry]) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleRemoveLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid - poolStateCid = swapRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid - requestCid = Some reqCid; acceptanceCid = None - holder = alice; lpTokensToRedeem = lpAmount; knownTotalLpSupply = lpAmount - minBaseOut = 0.0; minQuoteOut = 0.0 - baseSliceCids = [newBaseSliceCid]; quoteSliceCids = [swapRes.inputSliceCid] - holderBaseReceiptCid = baseReceipt; holderQuoteReceiptCid = quoteReceipt - holderBurnSenderCid = burnSender - baseFactoryCid = fx.factoryCid; quoteFactoryCid = fx.factoryCid - lpFactoryCid = fx.factoryCid - baseQuoteSettleCid = fx.settleCid; lpSettleCid = fx.settleCid - requestedAt = now; poolAdminExtraArgs = emptyExtraArgs; lpRegistrarExtraArgs = emptyExtraArgs + let preparation = Dvp.RemoveLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid + poolStateCid = swapRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid + requestCid = Some reqCid; acceptanceCid = None + holder = alice; lpTokensToRedeem = lpAmount; knownTotalLpSupply = lpAmount + minBaseOut = 0.0; minQuoteOut = 0.0 + baseSliceCids = [newBaseSliceCid]; quoteSliceCids = [swapRes.inputSliceCid] + holderBaseReceiptCid = baseReceipt; holderQuoteReceiptCid = quoteReceipt + holderBurnSenderCid = burnSender + remRes <- PLT.settlePreparedRemove fx preparation now remRes.baseReturned === baseOut remRes.quoteReturned === quoteOut -- Drained pool reconciles against the empty slice set. diff --git a/trading-tests/CantonDex/Tests/PoolWorkflowTests.daml b/trading-tests/CantonDex/Tests/PoolWorkflowTests.daml new file mode 100644 index 00000000..f709c39a --- /dev/null +++ b/trading-tests/CantonDex/Tests/PoolWorkflowTests.daml @@ -0,0 +1,303 @@ +-- | Pool lifecycle and swap choreography against MockRegistry. +-- +-- Read in this order: +-- 1. testPoolFullLifecycle +-- 2. testPoolSwapWorkflow +-- 3. testPoolSwapViaRequestSwap +-- +-- This suite proves controllers, quote/state binding, contract replacement, and +-- settlement-choice assembly. MockRegistry has no holdings, so it does NOT prove +-- balance movement or conservation; use the real-holding pool suites for that. +-- Design context: `docs/concepts/design-tour.md#signed-pool-swaps`. +module CantonDex.Tests.PoolWorkflowTests where + +import DA.Assert +import DA.Optional (isSome) + +import Daml.Script + +import Splice.Api.Token.AllocationInstructionV2 qualified as V2 +import Splice.Testing.Utils (emptyExtraArgs) + +import CantonDex.Dex.Pool qualified as Pool +import CantonDex.Dex.PoolRules qualified as PRules +import CantonDex.Dex.PoolModel qualified as PM +import CantonDex.Trading.Utils qualified as Utils +import CantonDex.Trading.WorkflowConstructors qualified as WC +import CantonDex.Tests.WorkflowTestFixtures + +-- Mock DvP choreography activates an unfunded pool ---------------------- +-- +-- A requested 10 BTC / 200,000 USDC add activates the pool and records those +-- reserve and LP-supply accounting values. No Holding changes in this fixture. + +-- | Proves pool creation, initialization, pause, and resume state transitions. +testPoolFullLifecycle : Script () +testPoolFullLifecycle = do + -- Roles: the venue operator, the LP-share registrar, the token admin, and + -- Alice, who supplies the liquidity. + operator <- allocateParty "operator" + lpRegistrar <- allocateParty "lp-registrar" + admin <- allocateParty "admin" + alice <- allocateParty "alice" + now <- getTime + + -- Stand up the mock registry and an empty BTC/USDC pool, then have Alice + -- authorize the mock DvP allocation specifications. + (factoryCid, _settleCid) <- setupRegistries admin [operator, lpRegistrar, alice] + setup <- setupPool operator lpRegistrar admin + let poolId = setup.setupPoolId + poolCid = setup.setupPoolCid + stateCid = setup.setupStateCid + rulesCid = setup.setupRulesCid + dvpCid = setup.setupLiquidityRulesCid + policyCid = setup.setupPolicyCid + initRes <- dvpFundPool + operator + lpRegistrar + factoryCid + _settleCid + poolId + poolCid + stateCid + policyCid + dvpCid + alice + 10.0 + 200000.0 + now + emptyExtraArgs + + -- Check the outcome: shares minted match the formula, the pool is now + -- Active, and its reserves and total shares equal exactly what went in. + Some state <- queryContractId operator initRes.poolStateCid + Some _baseSlice <- queryContractId operator initRes.baseSliceCid + Some _quoteSlice <- queryContractId operator initRes.quoteSliceCid + let expectedLp = PM.sqrtDecimal 2000000.0 + initRes.lpTokensMinted === expectedLp + state.status === Pool.PS_Active + state.reserves.baseAmount === 10.0 + state.reserves.quoteAmount === 200000.0 + state.totalLpSupply === expectedLp + + -- The same operator-controlled lifecycle rules pause the funded pool and + -- recreate its hot state. A paused pool cannot swap; resume restores Active. + pausedCid <- submit operator $ exerciseCmd rulesCid PRules.PoolRules_Pause with + expectedPoolId = poolId + poolCid + poolStateCid = initRes.poolStateCid + Some paused <- queryContractId operator pausedCid + paused.status === Pool.PS_Paused + None <- queryContractId operator initRes.poolStateCid + + -- The lifecycle flag is an on-ledger gate, not display metadata. The swap + -- fails at the status check before the placeholder trader allocation can be + -- inspected or settlement attempted. + let pausedBinding = PRules.SwapQuoteBinding with + expectedPoolId = poolId + poolStateCid = pausedCid + inputSliceCid = initRes.quoteSliceCid + outputSliceCids = [initRes.baseSliceCid] + minOutputAmount = 0.0 + submitMustFail operator $ exerciseCmd rulesCid PRules.PoolRules_Swap with + expectedPoolId = poolId; poolCid; poolStateCid = pausedCid + swapperAccount = Utils.basicAccount alice + inputInstrumentId = "USDC"; inputAmount = 1.0; minOutputAmount = 0.0 + swapperAllocationCid = _baseSlice.allocationCid + inputSliceCid = initRes.quoteSliceCid + outputSliceCids = [initRes.baseSliceCid] + factoryCid = _settleCid; extraArgs = emptyExtraArgs + quoteBinding = Some pausedBinding + + resumedCid <- submit operator $ exerciseCmd rulesCid PRules.PoolRules_Resume with + expectedPoolId = poolId + poolCid + poolStateCid = pausedCid + Some resumed <- queryContractId operator resumedCid + resumed.status === Pool.PS_Active + resumed.reserves === state.reserves + resumed.totalLpSupply === state.totalLpSupply + + pure () + +-- PoolRules_Swap updates pool inventory --------------------------------- +-- +-- Trader authorizes the exact two-sided allocation via the mock factory; +-- operator drives PoolRules_Swap which adjusts both +-- pool allocations + the trader allocation and batch-settles them. +-- +-- Verifies: +-- - the swap completes without error +-- - reserves update correctly +-- - the head pool allocation is replaced by its next-iteration CID +-- - other pool allocations on the same side stay untouched + +-- | Proves the mock swap batch rewrites only the consumed reserve slices. +testPoolSwapWorkflow : Script () +testPoolSwapWorkflow = do + operator <- allocateParty "operator-swap" + lpRegistrar <- allocateParty "lp-registrar-swap" + admin <- allocateParty "admin-swap" + alice <- allocateParty "alice-swap" -- LP + bob <- allocateParty "bob-swap" -- swapper + now <- getTime + + (factoryCid, settleCid) <- setupRegistries admin [operator, lpRegistrar, alice, bob] + + -- Initialize the pool through the mock DvP choreography. + setup <- setupPool operator lpRegistrar admin + let poolId = setup.setupPoolId + poolCid = setup.setupPoolCid + stateCid = setup.setupStateCid + rulesCid = setup.setupRulesCid + dvpCid = setup.setupLiquidityRulesCid + policyCid = setup.setupPolicyCid + initRes <- dvpFundPool + operator + lpRegistrar + factoryCid + settleCid + poolId + poolCid + stateCid + policyCid + dvpCid + alice + 10.0 + 200000.0 + now + emptyExtraArgs + + let bobAccount = Utils.basicAccount bob + quoteBinding = Some (PRules.SwapQuoteBinding with + expectedPoolId = poolId + poolStateCid = initRes.poolStateCid + inputSliceCid = initRes.quoteSliceCid + outputSliceCids = [initRes.baseSliceCid] + minOutputAmount = 0.0) + reqRes <- submit operator $ exerciseCmd rulesCid PRules.PoolRules_RequestSwap with + poolCid + swapper = bob + inputInstrumentId = "USDC" + inputAmount = 100.0 + quoteBinding + let bobAllocateArg = WC.mkAllocationFactoryAllocate + reqRes.settlement reqRes.allocationSpec now [] [bob] emptyExtraArgs + + bobInstr <- submit bob $ exerciseCmd factoryCid bobAllocateArg + bobAllocationCid <- case bobInstr.output of + V2.AllocationInstructionResult_Completed cid -> pure cid + _ -> abort "swap allocation must complete" + + -- Operator drives the swap: input USDC into the quote slice, source + -- BTC out from the base slice. + swapRes <- submit operator $ exerciseCmd rulesCid PRules.PoolRules_Swap with + expectedPoolId = poolId + poolCid + poolStateCid = initRes.poolStateCid + swapperAccount = bobAccount + inputInstrumentId = "USDC" + inputAmount = 100.0 + minOutputAmount = 0.0 + swapperAllocationCid = bobAllocationCid + inputSliceCid = initRes.quoteSliceCid + outputSliceCids = [initRes.baseSliceCid] + factoryCid = settleCid + extraArgs = emptyExtraArgs + quoteBinding = reqRes.quoteBinding + + -- Verify reserves moved. + Some state <- queryContractId operator swapRes.poolStateCid + assertMsg "base reserve decreased" (state.reserves.baseAmount < 10.0) + assertMsg "quote reserve increased" (state.reserves.quoteAmount > 200000.0) + -- Input slice (quote) grew; output slice (base) re-allocated as boundary. + Some _newQuote <- queryContractId operator swapRes.inputSliceCid + assertMsg "boundary output slice produced" (isSome swapRes.boundaryOutputSliceCid) + -- The output amount is positive. + assertMsg "swap produced output" (swapRes.amountOut > 0.0) + pure () + +-- PoolRules_RequestSwap produces a settleable allocation spec ------------ +-- +-- The operator builds the allocation specification, the trader authors that +-- exact allocation, and PoolRules_Swap settles it against the reserves. + +-- | Proves the trader-signed request specification settles without mutation. +testPoolSwapViaRequestSwap : Script () +testPoolSwapViaRequestSwap = do + operator <- allocateParty "operator-rswap" + lpRegistrar <- allocateParty "lp-registrar-rswap" + admin <- allocateParty "admin-rswap" + alice <- allocateParty "alice-rswap" -- LP + bob <- allocateParty "bob-rswap" -- swapper + now <- getTime + + (factoryCid, settleCid) <- setupRegistries admin [operator, lpRegistrar, alice, bob] + + setup <- setupPool operator lpRegistrar admin + let poolId = setup.setupPoolId + poolCid = setup.setupPoolCid + stateCid = setup.setupStateCid + rulesCid = setup.setupRulesCid + dvpCid = setup.setupLiquidityRulesCid + policyCid = setup.setupPolicyCid + initRes <- dvpFundPool + operator + lpRegistrar + factoryCid + settleCid + poolId + poolCid + stateCid + policyCid + dvpCid + alice + 10.0 + 200000.0 + now + emptyExtraArgs + + -- Operator builds the swapper's allocation specification, mirroring the + -- dApp's POST /v1/pools/swap/request call. + reqRes <- submit operator $ exerciseCmd rulesCid PRules.PoolRules_RequestSwap with + poolCid + swapper = bob + inputInstrumentId = "USDC" + inputAmount = 100.0 + quoteBinding = Some (PRules.SwapQuoteBinding with + expectedPoolId = poolId + poolStateCid = initRes.poolStateCid + inputSliceCid = initRes.quoteSliceCid + outputSliceCids = [initRes.baseSliceCid] + minOutputAmount = 0.0) + + -- Bob (the wallet) authors that exact spec via the allocation factory. + let bobAccount = Utils.basicAccount bob + bobAllocateArg = WC.mkAllocationFactoryAllocate + reqRes.settlement reqRes.allocationSpec now [] [bob] emptyExtraArgs + bobInstr <- submit bob $ exerciseCmd factoryCid bobAllocateArg + bobAllocationCid <- case bobInstr.output of + V2.AllocationInstructionResult_Completed cid -> pure cid + _ -> abort "swap allocation must complete" + + -- Operator settles the swap against the authored allocation. + swapRes <- submit operator $ exerciseCmd rulesCid PRules.PoolRules_Swap with + expectedPoolId = poolId + poolCid + poolStateCid = initRes.poolStateCid + swapperAccount = bobAccount + inputInstrumentId = "USDC" + inputAmount = 100.0 + minOutputAmount = 0.0 + swapperAllocationCid = bobAllocationCid + inputSliceCid = initRes.quoteSliceCid + outputSliceCids = [initRes.baseSliceCid] + factoryCid = settleCid + extraArgs = emptyExtraArgs + quoteBinding = reqRes.quoteBinding + + Some state <- queryContractId operator swapRes.poolStateCid + assertMsg "base reserve decreased" (state.reserves.baseAmount < 10.0) + assertMsg "quote reserve increased" (state.reserves.quoteAmount > 200000.0) + assertMsg "swap via RequestSwap produced output" (swapRes.amountOut > 0.0) + pure () diff --git a/trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml b/trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml index c691fd66..d7ebc388 100644 --- a/trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml +++ b/trading-tests/CantonDex/Tests/RealRegistryDvpTests.daml @@ -20,11 +20,10 @@ import Splice.Api.Token.AllocationInstructionV2 qualified as V2 import Splice.Api.Token.HoldingV2 qualified as V2 import Splice.Api.Token.MetadataV1 -import Splice.Testing.Utils (discloseMany', queryDisclosure', Disclosures', emptyExtraArgs) -import Splice.Testing.TokenStandard.RegistryApiV2 (getAllocationFactory) +import Splice.Testing.Utils (discloseMany', Disclosures', emptyExtraArgs) +import Splice.Testing.TokenStandard.RegistryApiV2 + (getAllocationFactory, getSettlementFactory) import Splice.Testing.Registries.TestTokenV2_RegistryV2 qualified as TT2 -import Splice.Testing.Tokens.TestTokenV2 qualified as TestTokenV2 -import Splice.Testing.Tokens.TestTokenV2.Holding qualified as TTHolding import CantonDex.Dex.Pool import CantonDex.Dex.PoolState qualified as PS @@ -83,15 +82,6 @@ ttAlloc reg party settlement spec inputHoldingCids now = do V2.AllocationInstructionResult_Completed cid -> pure cid _ -> abort "ttAlloc: allocation must complete" --- Disclose TokenRules + every TestTokenV2 holding the registry administers, --- so the settle's TT2 SettleBatch can see the locked deposit holdings it --- consumes (mirrors getSettlementFactory's lockedDs enrichment). -ttSettleDisclosures : Fixture -> Script Disclosures' -ttSettleDisclosures fx = do - toks <- query @TTHolding.Token fx.ttAdmin - ds <- forA toks (\(cid, _) -> queryDisclosure' @TTHolding.Token fx.ttAdmin cid) - pure (fx.ttDisclosures <> mconcat ds) - mkSpec : Party -> V2.Account -> [V2.TransferLeg] -> Bool -> V2.AllocationSpecification mkSpec admin authorizer legs committed = V2.AllocationSpecification with admin; authorizer @@ -117,10 +107,6 @@ data Fixture = Fixture with policyCid : ContractId LP.LPTokenPolicy dvpCid : ContractId Dvp.PoolLiquidityRules rulesCid : ContractId PRules.PoolRules - ttFactoryCid : ContractId V2.AllocationFactory - ttSettleCid : ContractId V2.SettlementFactory - ttDisclosures : Disclosures' - ttContext : ChoiceContext -- base = "X", quote = "Y" (TestTokenV2 instruments administered by ttAdmin). setup : Party -> Script Fixture @@ -152,27 +138,9 @@ setup lp = do policyCid <- submit lpRegistrar $ createCmd LP.LPTokenPolicy with lpRegistrar; operator; lpInstrumentId totalSupply = 0.0; active = True - -- The TestTokenV2 factory cid + its uniform choice context/disclosure - -- (basic accounts => one context reused across all TT2 choices). - now <- getTime - let sampleSpec = mkSpec ttAdmin (Utils.basicAccount lp) - [ V2.TransferLeg with - transferLegId = "sample"; sender = Utils.basicAccount lp - receiver = Utils.basicAccount operator; amount = 10.0 - instrumentId = "X"; meta = emptyMetadata ] True - enriched <- getAllocationFactory ttReg V2.AllocationFactory_Allocate with - settlement = PM.poolSettlement poolCid operator - allocation = sampleSpec; requestedAt = now - inputHoldingCids = []; actors = [lp]; extraArgs = emptyExtraArgs - [(tokenRulesCid, _)] <- query @TestTokenV2.TokenRules ttAdmin - ttD <- queryDisclosure' @TestTokenV2.TokenRules ttAdmin tokenRulesCid pure Fixture with operator; lpRegistrar; ttAdmin; ttReg; lpReg = lpRegCid poolId; poolCid; stateCid; policyCid; dvpCid; rulesCid - ttFactoryCid = enriched.factoryCid - ttSettleCid = coerceContractId enriched.factoryCid - ttDisclosures = ttD - ttContext = enriched.arg.extraArgs.context -- Author the LP's three add allocations and return them with the request. authorAdd @@ -208,32 +176,155 @@ authorAdd fx lp baseAmount quoteAmount now = do let V2.AllocationInstructionResult_Completed receiptCid = rres.output pure (reqCid, baseDep, quoteDep, receiptCid) --- Build the DvP-add SettleAddLiquidity command, parameterized on the --- pool.admin (base/quote) choice context. base/quote settle via the real --- TestTokenV2 factory; LP mint via our self-registry. +data StagedAddSettlement = StagedAddSettlement with + requestedAt : Time + baseFactoryCid : ContractId V2.AllocationFactory + quoteFactoryCid : ContractId V2.AllocationFactory + lpFactoryCid : ContractId V2.AllocationFactory + operatorBaseReceiverCid : ContractId V2.Allocation + operatorQuoteReceiverCid : ContractId V2.Allocation + registrarMintCid : ContractId V2.Allocation + baseQuoteSettleCid : ContractId V2.SettlementFactory + poolAdminExtraArgs : ExtraArgs + disclosures : Disclosures' + +data StagedRemoveSettlement = StagedRemoveSettlement with + requestedAt : Time + lpFactoryCid : ContractId V2.AllocationFactory + registrarBurnReceiverCid : ContractId V2.Allocation + baseQuoteSettleCid : ContractId V2.SettlementFactory + poolAdminExtraArgs : ExtraArgs + disclosures : Disclosures' + +completeTtAllocation + : Fixture -> Party -> V2.AllocationFactory_Allocate + -> Script (ContractId V2.AllocationFactory, ContractId V2.Allocation) +completeTtAllocation fx actor allocationArg = do + enriched <- getAllocationFactory fx.ttReg allocationArg + result <- submit (actAs actor <> discloseMany' enriched.disclosures) $ + exerciseCmd enriched.factoryCid enriched.arg + case result.output of + V2.AllocationInstructionResult_Completed cid -> pure (enriched.factoryCid, cid) + _ -> abort "TestTokenV2 planned allocation must complete" + +completeLpAllocation + : Fixture -> Party -> V2.AllocationFactory_Allocate + -> Script (ContractId V2.AllocationFactory, ContractId V2.Allocation) +completeLpAllocation fx actor allocationArg = do + let factoryCid = toInterfaceContractId fx.lpReg : ContractId V2.AllocationFactory + result <- submit (actAs actor <> readAs [fx.lpRegistrar]) $ + exerciseCmd factoryCid allocationArg + case result.output of + V2.AllocationInstructionResult_Completed cid -> pure (factoryCid, cid) + _ -> abort "LP planned allocation must complete" + +stageAddSettlement + : Fixture -> Dvp.AddLiquidityPreparationArgs -> Time + -> Script StagedAddSettlement +stageAddSettlement fx preparation requestedAt = do + allocationPlan <- submit (actAs [fx.operator, fx.lpRegistrar]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation + requestedAt + (baseFactoryCid, operatorBaseReceiverCid) <- + completeTtAllocation fx fx.operator allocationPlan.baseReceiver + (quoteFactoryCid, operatorQuoteReceiverCid) <- + completeTtAllocation fx fx.operator allocationPlan.quoteReceiver + (lpFactoryCid, registrarMintCid) <- + completeLpAllocation fx fx.lpRegistrar allocationPlan.lpMintSender + settlementPlan <- submit (actAs [fx.operator, fx.lpRegistrar]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewAddSettlement with + preparation + operatorBaseReceiverCid + operatorQuoteReceiverCid + registrarMintCid + enriched <- getSettlementFactory fx.ttReg settlementPlan.baseQuoteBatch + pure StagedAddSettlement with + requestedAt + baseFactoryCid + quoteFactoryCid + lpFactoryCid + operatorBaseReceiverCid + operatorQuoteReceiverCid + registrarMintCid + baseQuoteSettleCid = enriched.factoryCid + poolAdminExtraArgs = enriched.arg.extraArgs + disclosures = enriched.disclosures + +stageRemoveSettlement + : Fixture -> Dvp.RemoveLiquidityPreparationArgs -> Time + -> Script StagedRemoveSettlement +stageRemoveSettlement fx preparation requestedAt = do + allocationPlan <- submit (actAs [fx.operator, fx.lpRegistrar]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewRemoveAllocations with + preparation + requestedAt + (lpFactoryCid, registrarBurnReceiverCid) <- + completeLpAllocation fx fx.lpRegistrar allocationPlan.lpBurnReceiver + settlementPlan <- submit (actAs [fx.operator, fx.lpRegistrar]) $ + exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_PreviewRemoveSettlement with + preparation + registrarBurnReceiverCid + enriched <- getSettlementFactory fx.ttReg settlementPlan.baseQuoteBatch + pure StagedRemoveSettlement with + requestedAt + lpFactoryCid + registrarBurnReceiverCid + baseQuoteSettleCid = enriched.factoryCid + poolAdminExtraArgs = enriched.arg.extraArgs + disclosures = enriched.disclosures + +-- Build the DvP-add command after every exact factory choice has been +-- discovered. The context parameter is exposed only for the negative test +-- that proves TestTokenV2 rejects an omitted operation-specific context. mkSettleAdd - : Fixture -> ContractId LAR.LiquidityAllocationRequest -> Party - -> ContractId V2.Allocation -> ContractId V2.Allocation -> ContractId V2.Allocation - -> Time -> ExtraArgs -> Commands Dvp.PoolLiquidityRules_SettleAddResult -mkSettleAdd fx reqCid lp baseDep quoteDep receiptCid now poolAdminCtx = + : Fixture + -> Dvp.AddLiquidityPreparationArgs + -> StagedAddSettlement + -> ExtraArgs + -> Commands Dvp.PoolLiquidityRules_SettleAddResult +mkSettleAdd fx preparation staged poolAdminExtraArgs = exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid - lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None; recipient = lp - lpBaseDepositCid = baseDep; lpQuoteDepositCid = quoteDep; lpReceiptCid = receiptCid - baseFactoryCid = fx.ttFactoryCid; quoteFactoryCid = fx.ttFactoryCid - lpFactoryCid = toInterfaceContractId fx.lpReg - baseQuoteSettleCid = fx.ttSettleCid; lpSettleCid = coerceContractId fx.lpReg - baseAmount = 10.0; quoteAmount = 200000.0; minLpTokens = 0.0; knownTotalLpSupply = 0.0 - requestedAt = now; poolAdminExtraArgs = poolAdminCtx; lpRegistrarExtraArgs = emptyExtraArgs + expectedPoolId = preparation.expectedPoolId + poolCid = preparation.poolCid + poolStateCid = preparation.poolStateCid + lpPolicyCid = preparation.lpPolicyCid + requestCid = preparation.requestCid + acceptanceCid = preparation.acceptanceCid + recipient = preparation.recipient + lpBaseDepositCid = preparation.lpBaseDepositCid + lpQuoteDepositCid = preparation.lpQuoteDepositCid + lpReceiptCid = preparation.lpReceiptCid + baseFactoryCid = staged.baseFactoryCid + quoteFactoryCid = staged.quoteFactoryCid + lpFactoryCid = staged.lpFactoryCid + baseQuoteSettleCid = staged.baseQuoteSettleCid + lpSettleCid = coerceContractId fx.lpReg + baseAmount = preparation.baseAmount + quoteAmount = preparation.quoteAmount + minLpTokens = preparation.minLpTokens + knownTotalLpSupply = preparation.knownTotalLpSupply + requestedAt = staged.requestedAt + poolAdminExtraArgs + lpRegistrarExtraArgs = emptyExtraArgs + operatorBaseReceiverCid = Some staged.operatorBaseReceiverCid + operatorQuoteReceiverCid = Some staged.operatorQuoteReceiverCid + registrarMintCid = Some staged.registrarMintCid -- Run a full DvP add and return its result for tests that need a funded pool. addSettle : Fixture -> Party -> Time -> Script Dvp.PoolLiquidityRules_SettleAddResult addSettle fx lp now = do (reqCid, baseDep, quoteDep, receiptCid) <- authorAdd fx lp 10.0 200000.0 now - settleDisc <- ttSettleDisclosures fx - let poolAdminCtx = ExtraArgs with context = fx.ttContext; meta = emptyMetadata - submit (foldMap actAs [fx.operator, fx.lpRegistrar] <> discloseMany' settleDisc) $ - mkSettleAdd fx reqCid lp baseDep quoteDep receiptCid now poolAdminCtx + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid + lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None + recipient = lp + lpBaseDepositCid = baseDep; lpQuoteDepositCid = quoteDep; lpReceiptCid = receiptCid + baseAmount = 10.0; quoteAmount = 200000.0 + minLpTokens = 0.0; knownTotalLpSupply = 0.0 + staged <- stageAddSettlement fx preparation now + submit (foldMap actAs [fx.operator, fx.lpRegistrar] <> discloseMany' staged.disclosures) $ + mkSettleAdd fx preparation staged staged.poolAdminExtraArgs -- A DvP add settles across both registries when the base/quote batch receives -- the TestTokenV2 context. The pool becomes active, its reserves are backed by @@ -264,9 +355,17 @@ testRealRegistryDvpRejectsMissingContext = do fx <- setup lp now <- getTime (reqCid, baseDep, quoteDep, receiptCid) <- authorAdd fx lp 10.0 200000.0 now - settleDisc <- ttSettleDisclosures fx - result <- trySubmit (foldMap actAs [fx.operator, fx.lpRegistrar] <> discloseMany' settleDisc) $ - mkSettleAdd fx reqCid lp baseDep quoteDep receiptCid now emptyExtraArgs + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = fx.stateCid + lpPolicyCid = fx.policyCid; requestCid = Some reqCid; acceptanceCid = None + recipient = lp + lpBaseDepositCid = baseDep; lpQuoteDepositCid = quoteDep; lpReceiptCid = receiptCid + baseAmount = 10.0; quoteAmount = 200000.0 + minLpTokens = 0.0; knownTotalLpSupply = 0.0 + staged <- stageAddSettlement fx preparation now + result <- trySubmit + (foldMap actAs [fx.operator, fx.lpRegistrar] <> discloseMany' staged.disclosures) $ + mkSettleAdd fx preparation staged emptyExtraArgs case result of Right _ -> abort "expected a missing-context failure" Left e -> @@ -312,20 +411,16 @@ testRealRegistryDvpAddRefundsOffRatioExcess = do settlement; allocation = mkSpec fx.lpRegistrar lpAcct [lpMintLeg] False; requestedAt = now inputHoldingCids = []; actors = [lp]; extraArgs = emptyExtraArgs let V2.AllocationInstructionResult_Completed receiptCid = rres.output - settleDisc <- ttSettleDisclosures fx - res <- submit (foldMap actAs [fx.operator, fx.lpRegistrar] <> discloseMany' settleDisc) $ - exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = addRes.poolStateCid - lpPolicyCid = addRes.lpPolicyCid; requestCid = Some reqCid; acceptanceCid = None - recipient = lp - lpBaseDepositCid = baseDep; lpQuoteDepositCid = quoteDep; lpReceiptCid = receiptCid - baseFactoryCid = fx.ttFactoryCid; quoteFactoryCid = fx.ttFactoryCid - lpFactoryCid = toInterfaceContractId fx.lpReg - baseQuoteSettleCid = fx.ttSettleCid; lpSettleCid = coerceContractId fx.lpReg - baseAmount; quoteAmount; minLpTokens = 0.0; knownTotalLpSupply = supply - requestedAt = now - poolAdminExtraArgs = ExtraArgs with context = fx.ttContext; meta = emptyMetadata - lpRegistrarExtraArgs = emptyExtraArgs + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid; poolStateCid = addRes.poolStateCid + lpPolicyCid = addRes.lpPolicyCid; requestCid = Some reqCid; acceptanceCid = None + recipient = lp + lpBaseDepositCid = baseDep; lpQuoteDepositCid = quoteDep; lpReceiptCid = receiptCid + baseAmount; quoteAmount; minLpTokens = 0.0; knownTotalLpSupply = supply + staged <- stageAddSettlement fx preparation now + res <- submit + (foldMap actAs [fx.operator, fx.lpRegistrar] <> discloseMany' staged.disclosures) $ + mkSettleAdd fx preparation staged staged.poolAdminExtraArgs res.baseAdded === Some 5.0 res.quoteAdded === Some 100000.0 Some state <- queryContractId fx.operator res.poolStateCid @@ -371,22 +466,44 @@ testRealRegistryDvpRemoveSettles = do settlement; allocation = mkSpec fx.lpRegistrar aliceAcct [burnLeg] True; requestedAt = now inputHoldingCids = [toInterfaceContractId lpHoldCid]; actors = [alice]; extraArgs = emptyExtraArgs let V2.AllocationInstructionResult_Completed burnSender = brres.output - settleDisc <- ttSettleDisclosures fx - let poolAdminCtx = ExtraArgs with context = fx.ttContext; meta = emptyMetadata - remRes <- submit (foldMap actAs [fx.operator, fx.lpRegistrar] <> discloseMany' settleDisc) $ + let preparation = Dvp.RemoveLiquidityPreparationArgs with + expectedPoolId = fx.poolId; poolCid = fx.poolCid + poolStateCid = addRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid + requestCid = Some reqCid; acceptanceCid = None + holder = alice; lpTokensToRedeem = lpAmount; knownTotalLpSupply = lpAmount + minBaseOut = 0.0; minQuoteOut = 0.0 + baseSliceCids = [addRes.baseSliceCid]; quoteSliceCids = [addRes.quoteSliceCid] + holderBaseReceiptCid = baseReceipt; holderQuoteReceiptCid = quoteReceipt + holderBurnSenderCid = burnSender + staged <- stageRemoveSettlement fx preparation now + remRes <- submit + (foldMap actAs [fx.operator, fx.lpRegistrar] <> discloseMany' staged.disclosures) $ exerciseCmd fx.dvpCid Dvp.PoolLiquidityRules_SettleRemoveLiquidity with - expectedPoolId = fx.poolId; poolCid = fx.poolCid - poolStateCid = addRes.poolStateCid; lpPolicyCid = addRes.lpPolicyCid - requestCid = Some reqCid; acceptanceCid = None - holder = alice; lpTokensToRedeem = lpAmount; knownTotalLpSupply = lpAmount - minBaseOut = 0.0; minQuoteOut = 0.0 - baseSliceCids = [addRes.baseSliceCid]; quoteSliceCids = [addRes.quoteSliceCid] - holderBaseReceiptCid = baseReceipt; holderQuoteReceiptCid = quoteReceipt - holderBurnSenderCid = burnSender - baseFactoryCid = fx.ttFactoryCid; quoteFactoryCid = fx.ttFactoryCid - lpFactoryCid = toInterfaceContractId fx.lpReg - baseQuoteSettleCid = fx.ttSettleCid; lpSettleCid = coerceContractId fx.lpReg - requestedAt = now; poolAdminExtraArgs = poolAdminCtx; lpRegistrarExtraArgs = emptyExtraArgs + expectedPoolId = preparation.expectedPoolId + poolCid = preparation.poolCid + poolStateCid = preparation.poolStateCid + lpPolicyCid = preparation.lpPolicyCid + requestCid = preparation.requestCid + acceptanceCid = preparation.acceptanceCid + holder = preparation.holder + lpTokensToRedeem = preparation.lpTokensToRedeem + knownTotalLpSupply = preparation.knownTotalLpSupply + minBaseOut = preparation.minBaseOut + minQuoteOut = preparation.minQuoteOut + baseSliceCids = preparation.baseSliceCids + quoteSliceCids = preparation.quoteSliceCids + holderBaseReceiptCid = preparation.holderBaseReceiptCid + holderQuoteReceiptCid = preparation.holderQuoteReceiptCid + holderBurnSenderCid = preparation.holderBurnSenderCid + baseFactoryCid = staged.lpFactoryCid + quoteFactoryCid = staged.lpFactoryCid + lpFactoryCid = staged.lpFactoryCid + baseQuoteSettleCid = staged.baseQuoteSettleCid + lpSettleCid = coerceContractId fx.lpReg + requestedAt = staged.requestedAt + poolAdminExtraArgs = staged.poolAdminExtraArgs + lpRegistrarExtraArgs = emptyExtraArgs + registrarBurnReceiverCid = Some staged.registrarBurnReceiverCid Some state <- queryContractId fx.operator remRes.poolStateCid state.reserves.baseAmount === 0.0 state.reserves.quoteAmount === 0.0 @@ -458,9 +575,19 @@ testRealRegistryDvpSwapSettles = do reqRes.allocationSpec.transferLegSides underpayInputs <- holdingCidsFor bob "Y" underpayCid <- ttAlloc fx.ttReg bob reqRes.settlement underpaySpec underpayInputs now - malformedSettleDisc <- ttSettleDisclosures fx - let swapCtx = ExtraArgs with context = fx.ttContext; meta = emptyMetadata - submitMustFail (actAs fx.operator <> discloseMany' malformedSettleDisc) $ + malformedBatch <- submit fx.operator $ + exerciseCmd fx.rulesCid PRules.PoolRules_PreviewSwapSettlement with + expectedPoolId = fx.poolId; poolCid = fx.poolCid + poolStateCid = addRes.poolStateCid + swapperAccount = Utils.basicAccount bob + inputInstrumentId = "Y"; inputAmount = swapIn; minOutputAmount = 0.0 + swapperAllocationCid = underpayCid + inputSliceCid = addRes.quoteSliceCid + outputSliceCids = [addRes.baseSliceCid] + quoteBinding = reqRes.quoteBinding + malformedSettlement <- getSettlementFactory fx.ttReg malformedBatch + submitMustFail + (actAs fx.operator <> discloseMany' malformedSettlement.disclosures) $ exerciseCmd fx.rulesCid PRules.PoolRules_Swap with expectedPoolId = fx.poolId; poolCid = fx.poolCid poolStateCid = addRes.poolStateCid @@ -469,8 +596,8 @@ testRealRegistryDvpSwapSettles = do swapperAllocationCid = underpayCid inputSliceCid = addRes.quoteSliceCid outputSliceCids = [addRes.baseSliceCid] - factoryCid = fx.ttSettleCid - extraArgs = swapCtx + factoryCid = malformedSettlement.factoryCid + extraArgs = malformedSettlement.arg.extraArgs quoteBinding = reqRes.quoteBinding -- 2. Bob authors that exact spec via the upstream TestTokenV2 registry, @@ -480,8 +607,18 @@ testRealRegistryDvpSwapSettles = do -- 3. Operator settles the swap via PoolRules_Swap against the upstream -- TestTokenV2 SettlementFactory, with the per-admin context. - settleDisc <- ttSettleDisclosures fx - swapRes <- submit (actAs fx.operator <> discloseMany' settleDisc) $ + settlementBatch <- submit fx.operator $ + exerciseCmd fx.rulesCid PRules.PoolRules_PreviewSwapSettlement with + expectedPoolId = fx.poolId; poolCid = fx.poolCid + poolStateCid = addRes.poolStateCid + swapperAccount = Utils.basicAccount bob + inputInstrumentId = "Y"; inputAmount = swapIn; minOutputAmount = 0.0 + swapperAllocationCid = swapAllocCid + inputSliceCid = addRes.quoteSliceCid + outputSliceCids = [addRes.baseSliceCid] + quoteBinding = reqRes.quoteBinding + settlement <- getSettlementFactory fx.ttReg settlementBatch + swapRes <- submit (actAs fx.operator <> discloseMany' settlement.disclosures) $ exerciseCmd fx.rulesCid PRules.PoolRules_Swap with expectedPoolId = fx.poolId; poolCid = fx.poolCid poolStateCid = addRes.poolStateCid @@ -490,8 +627,8 @@ testRealRegistryDvpSwapSettles = do swapperAllocationCid = swapAllocCid inputSliceCid = addRes.quoteSliceCid outputSliceCids = [addRes.baseSliceCid] - factoryCid = fx.ttSettleCid - extraArgs = swapCtx + factoryCid = settlement.factoryCid + extraArgs = settlement.arg.extraArgs quoteBinding = reqRes.quoteBinding Some state <- queryContractId fx.operator swapRes.poolStateCid @@ -513,7 +650,6 @@ data TwoAdminTrade = TwoAdminTrade with lpLeg : V2.TransferLeg ttAllocations : [V2.FinalizedAllocation] lpAllocations : [V2.FinalizedAllocation] - settleDisclosures : Disclosures' -- alice sends 1 X (TestTokenV2) against bob's 5 LP (our registry), with all -- four allocations authored against the trade's own settlement info. The @@ -545,25 +681,51 @@ setupTwoAdminTrade alice bob = do lpSender <- lpAlloc fx bob settlement (mkSpec fx.lpRegistrar bobAcct [lpLeg] False) [toInterfaceContractId bobLpHolding] now lpReceiver <- lpAlloc fx alice settlement (mkSpec fx.lpRegistrar aliceAcct [lpLeg] False) [] now - disc <- ttSettleDisclosures fx pure TwoAdminTrade with fx; tradeCid; xLeg; lpLeg ttAllocations = map Utils.finalAllocation [xSender, xReceiver] lpAllocations = map Utils.finalAllocation [lpSender, lpReceiver] - settleDisclosures = disc + +data DiscoveredTtSettlement = DiscoveredTtSettlement with + factoryCid : ContractId V2.SettlementFactory + extraArgs : ExtraArgs + disclosures : Disclosures' + +discoverTtTradeSettlement + : TwoAdminTrade -> [V2.TransferLeg] -> [V2.TransferLeg] + -> Script DiscoveredTtSettlement +discoverTtTradeSettlement t ttLegs lpLegs = do + batches <- submit t.fx.operator $ + exerciseCmd t.tradeCid MT.MatchedTrade_PreviewSettlement with + plansByAdmin = Map.fromList + [ (t.fx.ttAdmin, MT.SettlementBatchPlanV2 with + transferLegs = ttLegs; allocations = t.ttAllocations) + , (t.fx.lpRegistrar, MT.SettlementBatchPlanV2 with + transferLegs = lpLegs; allocations = t.lpAllocations) + ] + let ttBatch = fromSome (Map.lookup t.fx.ttAdmin batches) + enriched <- getSettlementFactory t.fx.ttReg ttBatch + pure DiscoveredTtSettlement with + factoryCid = enriched.factoryCid + extraArgs = enriched.arg.extraArgs + disclosures = enriched.disclosures -- The venue's settle, parameterized on what each batch is told to settle. mkTwoAdminSettle - : TwoAdminTrade -> Optional [V2.TransferLeg] -> Optional [V2.TransferLeg] + : TwoAdminTrade + -> ContractId V2.SettlementFactory + -> ExtraArgs + -> Optional [V2.TransferLeg] + -> Optional [V2.TransferLeg] -> Commands MT.MatchedTrade_SettleResult -mkTwoAdminSettle t ttLegs lpLegs = +mkTwoAdminSettle t ttFactoryCid ttExtraArgs ttLegs lpLegs = exerciseCmd t.tradeCid MT.MatchedTrade_Settle with batchesByAdmin = Map.fromList [ (t.fx.ttAdmin, MT.SettlementBatchV2 with transferLegs = ttLegs allocations = t.ttAllocations - factoryCid = t.fx.ttSettleCid - extraArgs = ExtraArgs with context = t.fx.ttContext; meta = emptyMetadata) + factoryCid = ttFactoryCid + extraArgs = ttExtraArgs) , (t.fx.lpRegistrar, MT.SettlementBatchV2 with transferLegs = lpLegs allocations = t.lpAllocations @@ -575,9 +737,9 @@ mkTwoAdminSettle t ttLegs lpLegs = -- readAs the LP registrar: our Holding is `signatory admin, owner`, so the -- venue is not a stakeholder of the LP holding the settle archives. -twoAdminSubmitOptions : TwoAdminTrade -> SubmitOptions -twoAdminSubmitOptions t = - actAs t.fx.operator <> readAs [t.fx.lpRegistrar] <> discloseMany' t.settleDisclosures +twoAdminSubmitOptions : TwoAdminTrade -> Disclosures' -> SubmitOptions +twoAdminSubmitOptions t disclosures = + actAs t.fx.operator <> readAs [t.fx.lpRegistrar] <> discloseMany' disclosures -- Handed the whole trade, a batch carries legs its own allocations do not -- authorize and the registry rejects it; handed no legs at all, the settle @@ -592,22 +754,27 @@ testMatchedTradeSettlesPerAdminLegSubsets = do bob <- allocateParty "bob" t <- setupTwoAdminTrade alice bob let allLegs = Some [t.xLeg, t.lpLeg] - rejected <- trySubmit (twoAdminSubmitOptions t) $ mkTwoAdminSettle t allLegs allLegs + malformed <- discoverTtTradeSettlement t [t.xLeg, t.lpLeg] [t.xLeg, t.lpLeg] + rejected <- trySubmit (twoAdminSubmitOptions t malformed.disclosures) $ + mkTwoAdminSettle t malformed.factoryCid malformed.extraArgs allLegs allLegs case rejected of Right _ -> abort "a batch given the full leg list must be rejected" Left e -> assertMsg ("expected missing authorizations, got: " <> show e) (T.isInfixOf "authorizations" (show e)) - omitted <- trySubmit (twoAdminSubmitOptions t) $ mkTwoAdminSettle t None None + omitted <- trySubmit (twoAdminSubmitOptions t mempty) $ + mkTwoAdminSettle t (coerceContractId t.fx.lpReg) emptyExtraArgs None None case omitted of Right _ -> abort "a batch with no transfer legs must be rejected" Left e -> assertMsg ("expected a required-legs abort, got: " <> show e) (T.isInfixOf "per-admin transfer legs are required" (show e)) - res <- submit (twoAdminSubmitOptions t) $ - mkTwoAdminSettle t (Some [t.xLeg]) (Some [t.lpLeg]) + discovered <- discoverTtTradeSettlement t [t.xLeg] [t.lpLeg] + res <- submit (twoAdminSubmitOptions t discovered.disclosures) $ + mkTwoAdminSettle t discovered.factoryCid discovered.extraArgs + (Some [t.xLeg]) (Some [t.lpLeg]) Map.size res.resultsByAdmin === 2 bobX <- holdingCidsFor bob "X" assertMsg "bob received the X leg" (not (null bobX)) diff --git a/trading-tests/CantonDex/Tests/RfqSettlementTests.daml b/trading-tests/CantonDex/Tests/RfqSettlementTests.daml index 8aed62a1..d0c8906d 100644 --- a/trading-tests/CantonDex/Tests/RfqSettlementTests.daml +++ b/trading-tests/CantonDex/Tests/RfqSettlementTests.daml @@ -2,7 +2,8 @@ -- settlement concept. It walks a request-for-quote from competing dealer -- quotes through settlement, then checks holdings and outstanding locks. -- --- | The RFQ round trip, end to end, against holding-tracking settlement. +-- | Daml Script integration for the RFQ round trip against holding-tracking +-- settlement. -- -- TokenStandardHarnessTests uses a holding-less registry. Its allocation -- factory returns an Allocation without locking holdings, and its settlement diff --git a/trading-tests/CantonDex/Tests/TokenStandardHarnessTests.daml b/trading-tests/CantonDex/Tests/TokenStandardHarnessTests.daml index 42d80f30..fe5a6530 100644 --- a/trading-tests/CantonDex/Tests/TokenStandardHarnessTests.daml +++ b/trading-tests/CantonDex/Tests/TokenStandardHarnessTests.daml @@ -1,8 +1,9 @@ --- | Token-standard-style end-to-end test of the matched-trade flow, mirroring --- splice-token-standard-test-v2's TradingAppV2 exercise: drive a RegistryApi --- registry (our DexRegistry over MockRegistry) through MatchedTrade -> --- per-authorizer allocation accept -> MatchedTrade_Settle, and verify the --- batch settles and the trade + requests are archived. +-- | Token-standard-interface integration test of the matched-trade flow, +-- mirroring splice-token-standard-test-v2's TradingAppV2 exercise: drive a +-- RegistryApi registry (our DexRegistry over MockRegistry) through MatchedTrade +-- -> per-authorizer allocation accept -> MatchedTrade_Settle, then verify the +-- batch choice completes and the trade + requests are archived. This fixture +-- has no holdings, so it proves the interface handshake, not value movement. -- Design context: `docs/concepts/design-tour.md#the-token-standard-settlement-spine`. module CantonDex.Tests.TokenStandardHarnessTests where diff --git a/trading-tests/CantonDex/Tests/TradeWorkflowTests.daml b/trading-tests/CantonDex/Tests/TradeWorkflowTests.daml new file mode 100644 index 00000000..2b093089 --- /dev/null +++ b/trading-tests/CantonDex/Tests/TradeWorkflowTests.daml @@ -0,0 +1,338 @@ +-- | Allocation-request, RFQ, and bilateral-trade choreography. +-- +-- Suggested reading order: +-- 1. testTradeAllocationRequestAcceptArchivesRequest +-- 2. testRfqAcceptProducesMatchedTradeWithReceipt +-- 3. testRfqExpireOperatorCleanup +-- 4. testMatchedTradeFullSettle +-- +-- These tests prove request consumption, ranking receipts, expiry authority, and +-- two-party settlement assembly. The matched-trade fixture has no holdings, so +-- it proves interface choreography rather than balance movement. +-- Design context: `docs/concepts/design-tour.md#rfq-and-otc-settlement`. +module CantonDex.Tests.TradeWorkflowTests where + +import DA.Assert +import DA.Map qualified as Map +import DA.Time + +import Daml.Script + +import Splice.Api.Token.AllocationV2 qualified as V2 +import Splice.Api.Token.AllocationInstructionV2 qualified as V2 +import Splice.Api.Token.AllocationInstructionV2 qualified as AllocationInstructionV2 +import Splice.Api.Token.AllocationRequestV2 qualified as V2 +import Splice.Api.Token.MetadataV1 +import Splice.Testing.Utils (emptyExtraArgs) + +import CantonDex.Dex.MatchedTrade qualified as MT +import CantonDex.Dex.PolicyReceipt qualified as PR +import CantonDex.Dex.Rfq qualified as Rfq +import CantonDex.Trading.Utils qualified as Utils +import CantonDex.Tests.WorkflowTestFixtures + +-- Accepting a trade allocation request consumes it ---------------------- +-- +-- A TradeAllocationRequest implements the standard V2 AllocationRequest +-- interface. Accept is the holder's acknowledgement of the allocation +-- specification; it consumes the request. The wallet composes Accept with the +-- corresponding AllocationFactory_Allocate command in the real trade flow. + +-- | Proves accepting a trade allocation request consumes that one-shot request. +testTradeAllocationRequestAcceptArchivesRequest : Script () +testTradeAllocationRequestAcceptArchivesRequest = do + operator <- allocateParty "operator" + admin <- allocateParty "admin" + alice <- allocateParty "alice" + bob <- allocateParty "bob" + now <- getTime + + -- Arrange: one requested leg moves 10 BTC from Alice to Bob, with the + -- operator as settlement executor. + let aliceAccount = Utils.basicAccount alice + bobAccount = Utils.basicAccount bob + leg = V2.TransferLeg with + transferLegId = "leg-1" + sender = aliceAccount + receiver = bobAccount + amount = 10.0 + instrumentId = "BTC" + meta = emptyMetadata + settlement = V2.SettlementInfo with + executors = [operator] + id = "test-trade" + cid = None + meta = emptyMetadata + + reqCid <- submit operator $ createCmd MT.TradeAllocationRequest with + authorizer = aliceAccount + admin + settlement + settlementDeadline = Some (addRelTime now (hours 1)) + transferLegs = [leg] + requestedAt = now + + -- Act: Alice accepts through the standard interface. + let reqIfaceCid : ContractId V2.AllocationRequest = toInterfaceContractId reqCid + acceptRes <- submit alice $ exerciseCmd reqIfaceCid V2.AllocationRequest_Accept with + actors = [alice] + extraArgs = emptyExtraArgs + -- Assert: Accept returns the request metadata and consumes the request. + acceptRes.meta === emptyMetadata + None <- queryContractId alice reqCid + + pure () + +-- RFQ acceptance records the ranking decision --------------------------- +-- +-- RFQ = request-for-quote: the trader asks dealers to bid, then accepts one. +-- Asserts that when three dealers quote and the trader accepts, the winner is +-- ranked by the stated policy (trusted tier, later expiry, earlier posting, +-- dealer id), a trade is created, and it carries a signed receipt proving that +-- ranking. Price is displayed in the receipt but is not a policy sort key. + +-- | Proves RFQ acceptance selects a quote and records its policy receipt. +testRfqAcceptProducesMatchedTradeWithReceipt : Script () +testRfqAcceptProducesMatchedTradeWithReceipt = do + operator <- allocateParty "operator" + admin <- allocateParty "admin" + alice <- allocateParty "alice" + orca <- allocateParty "orca-mm" + jump <- allocateParty "jump-tr" + galaxy <- allocateParty "galaxy-otc" + now <- getTime + + let expiresAt = addRelTime now (hours 1) + rfqId = "rfq-test-001" + + -- Alice creates an RFQ. + rfqCid <- submit alice $ createCmd Rfq.Rfq with + trader = alice + operator + rfqId + pair = "BTC/USDC" + side = Rfq.RFQ_Buy + size = 5.0 + expiresAt + whitelist = [orca, jump, galaxy] + createdAt = now + + -- Three dealers post quotes. Trusted dealers rank above whitelisted dealers; + -- Jump and Orca have equal expiry, so Jump's earlier posting ranks first. + quoteOrca <- submit orca $ createCmd Rfq.RfqQuote with + dealer = orca + trader = alice + operator + rfqId + price = 60530.00 + expiresAt = addRelTime now (seconds 30) + postedAt = now + tier = Rfq.TierTrusted + + quoteJump <- submit jump $ createCmd Rfq.RfqQuote with + dealer = jump + trader = alice + operator + rfqId + price = 60510.00 + expiresAt = addRelTime now (seconds 30) + postedAt = addRelTime now (seconds (-3)) + tier = Rfq.TierTrusted + + quoteGalaxy <- submit galaxy $ createCmd Rfq.RfqQuote with + dealer = galaxy + trader = alice + operator + rfqId + price = 60509.50 + expiresAt = addRelTime now (seconds 30) + postedAt = addRelTime now (seconds 8) + tier = Rfq.TierWhitelist + + -- Alice + operator jointly exercise Rfq_Accept (operator's authority + -- is needed because the resulting MatchedTrade is operator-signed). + -- Accept Jump's quote. It ranks first by tier and posting time even though + -- Galaxy has a marginally lower price. + acceptRes <- submit (actAs [alice, operator]) $ + exerciseCmd rfqCid Rfq.Rfq_Accept with + acceptedQuoteCid = quoteJump + consideredQuoteCids = [quoteOrca, quoteJump, quoteGalaxy] + admin + currentTime = now + signature = "0xtest-signature" + + -- Verify the receipt: + let r = acceptRes.receipt + r.acceptedDealer === jump + r.acceptedRank === 1 + r.consideredCount === 3 + PR.isWellFormed r === True + + -- Verify the MatchedTrade exists and carries the receipt. + Some trade <- queryContractId operator acceptRes.tradeCid + case trade.policyReceipt of + Some embedded -> embedded === r + None -> abort "Trade should carry a policy receipt" + + pure () + +-- The operator expires stale RFQs --------------------------------------- +-- +-- Only the operator can sweep an RFQ, and only after its deadline. +-- | Proves RFQ expiry cleanup is operator-only and deadline-gated. +testRfqExpireOperatorCleanup : Script () +testRfqExpireOperatorCleanup = do + operator <- allocateParty "operator-exp" + alice <- allocateParty "alice-exp" + orca <- allocateParty "orca-exp" + now <- getTime + + let expiresAt = addRelTime now (hours 1) + + rfqCid <- submit alice $ createCmd Rfq.Rfq with + trader = alice + operator + rfqId = "rfq-expire-001" + pair = "BTC/USDC" + side = Rfq.RFQ_Buy + size = 1.0 + expiresAt + whitelist = [orca] + createdAt = now + + -- Before the deadline the operator cannot expire it. + submitMustFail operator $ exerciseCmd rfqCid Rfq.Rfq_Expire with + currentTime = now + + -- The trader is not the controller of Rfq_Expire. + submitMustFail alice $ exerciseCmd rfqCid Rfq.Rfq_Expire with + currentTime = addRelTime now (hours 2) + + -- After the deadline the operator sweeps it. + submit operator $ exerciseCmd rfqCid Rfq.Rfq_Expire with + currentTime = addRelTime now (hours 2) + + remaining <- queryContractId operator rfqCid + remaining === None + + +-- A bilateral MatchedTrade settles atomically --------------------------- +-- +-- Verifies the OTC (over-the-counter, dealt directly not via the order book) +-- settlement choreography: the operator requests allocations, each side +-- authorizes its own mock Allocation, and the operator settles both in one +-- batch. MockRegistry neither locks funds nor creates final holdings. + +-- | Proves both mock allocations are consumed by one settlement exercise. +testMatchedTradeFullSettle : Script () +testMatchedTradeFullSettle = do + operator <- allocateParty "venue" + admin <- allocateParty "admin-mt" + alice <- allocateParty "alice-mt" + bob <- allocateParty "bob-mt" + now <- getTime + + (factoryCid, settleCid) <- setupRegistries admin [operator, alice, bob] + + -- Build a MatchedTrade with two-way legs (alice <-> bob in BTC). + let aliceAccount = Utils.basicAccount alice + bobAccount = Utils.basicAccount bob + legs = + [ V2.TransferLeg with + transferLegId = "leg-1" + sender = aliceAccount + receiver = bobAccount + amount = 1.0 + instrumentId = "BTC" + meta = emptyMetadata + ] + tradeCid <- submit operator $ createCmd MT.MatchedTrade with + venue = operator + admin + transferLegs = legs + settlementDeadline = None + policyReceipt = None + + -- Operator requests allocations. + reqCids <- submit operator $ exerciseCmd tradeCid MT.MatchedTrade_RequestAllocations + length reqCids === 2 -- one per authorizer + + -- Each authorizer accepts via the V2 interface, then creates their + -- allocation under their own authority. Alice is sender of "leg-1"; + -- Bob is receiver. Both need an allocation. + let mkAlloc party cid = do + -- The accept choice consumes the request; we then create the + -- allocation via the factory. + let reqIface : ContractId V2.AllocationRequest = toInterfaceContractId cid + _ <- submit party $ exerciseCmd reqIface V2.AllocationRequest_Accept with + actors = [party] + extraArgs = emptyExtraArgs + -- Build allocation for this party using the trade's settlement. + let acct = Utils.basicAccount party + settlement = MT.mkTradeSettlementInfo tradeCid + (MT.MatchedTrade with + venue = operator + admin + transferLegs = legs + settlementDeadline = None + policyReceipt = None) + spec = V2.AllocationSpecification with + admin + authorizer = acct + transferLegSides = Utils.legsToSides acct legs + settlementDeadline = None + nextIterationFunding = None + committed = False + meta = emptyMetadata + allocateArg = AllocationInstructionV2.AllocationFactory_Allocate with + settlement + allocation = spec + requestedAt = now + inputHoldingCids = [] + actors = [party] + extraArgs = emptyExtraArgs + instr <- submit party $ exerciseCmd factoryCid allocateArg + case instr.output of + V2.AllocationInstructionResult_Completed allocCid -> pure allocCid + _ -> abort "allocation must complete" + + -- We need to know which request belongs to whom. Query and split. + reqs <- query @MT.TradeAllocationRequest operator + let reqsByOwner = [ (Utils.accountOwner req.authorizer, cid) | (cid, req) <- reqs ] + -- The active req cids may differ from the freshly-returned reqCids + -- because authorizers haven't accepted yet. Find the per-party CIDs. + let aliceReqCid = case [ c | (p, c) <- reqsByOwner, p == alice ] of + (c :: _) -> c + [] -> error "alice request missing" + bobReqCid = case [ c | (p, c) <- reqsByOwner, p == bob ] of + (c :: _) -> c + [] -> error "bob request missing" + + let aliceReqIface : ContractId V2.AllocationRequest = + toInterfaceContractId aliceReqCid + submitMustFail (actAs [bob] <> readAs [operator]) $ + exerciseCmd aliceReqIface V2.AllocationRequest_Accept with + actors = [bob] + extraArgs = emptyExtraArgs + + aliceAllocCid <- mkAlloc alice aliceReqCid + bobAllocCid <- mkAlloc bob bobReqCid + + -- Operator settles. Both allocations belong to admin's batch. + settleResult <- submit operator $ exerciseCmd tradeCid MT.MatchedTrade_Settle with + batchesByAdmin = Map.fromList + [ (admin, MT.SettlementBatchV2 with + transferLegs = Some legs + allocations = + [ Utils.finalAllocation aliceAllocCid + , Utils.finalAllocation bobAllocCid + ] + factoryCid = settleCid + extraArgs = emptyExtraArgs) + ] + allocationRequests = [] -- already consumed via Accept + dexPairCid = None + + -- Verify the result has one per-admin entry. + Map.size settleResult.resultsByAdmin === 1 + pure () diff --git a/trading-tests/CantonDex/Tests/WorkflowTestFixtures.daml b/trading-tests/CantonDex/Tests/WorkflowTestFixtures.daml new file mode 100644 index 00000000..5be87963 --- /dev/null +++ b/trading-tests/CantonDex/Tests/WorkflowTestFixtures.daml @@ -0,0 +1,216 @@ +-- | Shared holding-less fixtures for the workflow-focused integration tests. +-- +-- These helpers isolate DEX contract choreography from token accounting. The +-- mock allocation and settlement factories create/consume interface contracts, +-- but they do not lock, debit, or credit Holding contracts. Tests that claim +-- value movement live in PoolLiquidityRulesTests, RegistryConservationTests, +-- RfqSettlementTests, and RealRegistryDvpTests. +-- +-- Imported by PoolWorkflowTests and ChoiceContextWorkflowTests; setupRegistries +-- is also reused by the order and bilateral-trade suites. +module CantonDex.Tests.WorkflowTestFixtures where + +import DA.List (head, tail) + +import Daml.Script + +import Splice.Api.Token.HoldingV2 qualified as V2 +import Splice.Api.Token.AllocationV2 qualified as V2 +import Splice.Api.Token.AllocationInstructionV2 qualified as V2 +import Splice.Api.Token.MetadataV1 + +import CantonDex.Lp.Policy qualified as LP +import CantonDex.Dex.Pool qualified as Pool +import CantonDex.Dex.PoolState qualified as PState +import CantonDex.Dex.PoolRules qualified as PRules +import CantonDex.Dex.PoolModel qualified as PM +import CantonDex.Dex.PoolLiquidityRules qualified as Dvp +import CantonDex.Testing.MockRegistry qualified as Mock + +-- Create the split pool contracts the operator uses for swap + DvP +-- liquidity, plus the LP policy. +data PoolSetup = PoolSetup with + setupPoolId : Pool.PoolId + setupPoolCid : ContractId Pool.Pool + setupStateCid : ContractId PState.PoolState + setupRulesCid : ContractId PRules.PoolRules + setupLiquidityRulesCid : ContractId Dvp.PoolLiquidityRules + setupPolicyCid : ContractId LP.LPTokenPolicy + +setupPool : Party -> Party -> Party -> Script PoolSetup +setupPool operator lpRegistrar admin = do + -- One BTC/USDC pool. The pool mints its own "BTC-USDC-LP" share token to + -- liquidity providers, issued by the LP registrar. Fee is 0.30% (30 bps). + let poolId = "BTC-USDC" + lpInstrumentId = V2.InstrumentId with admin = lpRegistrar; id = "BTC-USDC-LP" + -- Pool: the static config (the two sides, who runs it, the fee). + poolCid <- submit operator $ createCmd Pool.Pool with + poolId + operator + lpRegistrar + admin + baseInstrumentId = "BTC" + quoteInstrumentId = "USDC" + lpInstrumentId + feeBps = 30 + -- PoolState: the pool's reserve and LP-supply accounting. It starts empty and + -- Unfunded; the first successful add transition flips it to Active. + stateCid <- submit operator $ createCmd PState.PoolState with + poolId + operator + lpRegistrar + status = Pool.PS_Unfunded + reserves = Pool.PoolReserves with baseAmount = 0.0; quoteAmount = 0.0 + totalLpSupply = 0.0 + publicReaders = [] + -- PoolRules: the swap choices (trade against the pool). + rulesCid <- submit operator $ createCmd PRules.PoolRules with operator + -- PoolLiquidityRules: the add/remove-liquidity choices, run jointly by the + -- operator and the LP registrar (both signatures are required). + dvpCid <- submit (actAs [operator, lpRegistrar]) $ + createCmd Dvp.PoolLiquidityRules with operator; lpRegistrar + -- LPTokenPolicy: tracks how many LP shares exist across all providers. + policyCid <- submit lpRegistrar $ createCmd LP.LPTokenPolicy with + lpRegistrar + operator + lpInstrumentId + totalSupply = 0.0 + active = True + pure PoolSetup with + setupPoolId = poolId + setupPoolCid = poolCid + setupStateCid = stateCid + setupRulesCid = rulesCid + setupLiquidityRulesCid = dvpCid + setupPolicyCid = policyCid + +-- Stand up the holding-less registry fixture. Its allocation factory turns a +-- specification into a mock Allocation contract, and its settlement factory +-- completes the batch choices without debiting, locking, or crediting holdings. +-- These replace a real token issuer so choice choreography can be isolated. +setupRegistries : Party -> [Party] -> Script ( ContractId V2.AllocationFactory + , ContractId V2.SettlementFactory + ) +setupRegistries admin users = do + factoryCid <- submit admin $ createCmd Mock.MockAllocationFactory with admin; users; requireContext = False + settleCid <- submit admin $ createCmd Mock.MockSettlementFactory with admin; users; requireContext = False + pure (toInterfaceContractId factoryCid, toInterfaceContractId settleCid) + +-- Exercise the delivery-versus-payment (DvP) liquidity choreography through +-- Token Standard interfaces. The operator opens the request; the depositor +-- authorizes mock Allocation contracts for the two deposits and LP-mint +-- receipt; then operator and LP registrar jointly settle all three. Because +-- MockRegistry has no Holding contracts, this helper proves the requested +-- specifications, controllers, and state transitions—not actual value movement. +-- The pool's LP amount is the square root of base*quote. +-- +-- The two `Decimal` arguments are base then quote, in that order. +dvpFundPool + : Party + -> Party + -> ContractId V2.AllocationFactory + -> ContractId V2.SettlementFactory + -> Pool.PoolId + -> ContractId Pool.Pool + -> ContractId PState.PoolState + -> ContractId LP.LPTokenPolicy + -> ContractId Dvp.PoolLiquidityRules + -> Party + -> Decimal + -> Decimal + -> Time + -> ExtraArgs + -> Script Dvp.PoolLiquidityRules_SettleAddResult +dvpFundPool operator lpRegistrar factoryCid settleCid poolId poolCid stateCid policyCid dvpCid recipient baseAmount quoteAmount now extraArgs = do + let lpAmount = PM.sqrtDecimal (baseAmount * quoteAmount) + -- Operator opens the add-liquidity request; it lists three required mock + -- allocation specifications: base, quote, and the LP-mint receipt. + reqCid <- submit operator $ exerciseCmd dvpCid Dvp.PoolLiquidityRules_RequestAddLiquidity with + poolCid; recipient; baseAmount; quoteAmount; lpAmount; requestedAt = now; settleAt = None + Some req <- queryContractId operator reqCid + -- The three specifications, in order, and a helper that creates one mock + -- Allocation contract for each (the fixture completes immediately). + let baseSpec = head req.allocations + quoteSpec = head (tail req.allocations) + receiptSpec = head (tail (tail req.allocations)) + mkOne spec = do + res <- submit recipient $ exerciseCmd factoryCid V2.AllocationFactory_Allocate with + settlement = req.settlement + allocation = spec + requestedAt = now + inputHoldingCids = [] + actors = [recipient] + extraArgs + case res.output of + V2.AllocationInstructionResult_Completed cid -> pure cid + _ -> abort "Mock factory should complete immediately" + baseAllocCid <- mkOne baseSpec + quoteAllocCid <- mkOne quoteSpec + receiptAllocCid <- mkOne receiptSpec + + let preparation = Dvp.AddLiquidityPreparationArgs with + expectedPoolId = poolId + poolCid + poolStateCid = stateCid + lpPolicyCid = policyCid + requestCid = Some reqCid + acceptanceCid = None + recipient + lpBaseDepositCid = baseAllocCid + lpQuoteDepositCid = quoteAllocCid + lpReceiptCid = receiptAllocCid + baseAmount + quoteAmount + minLpTokens = 0.0 + knownTotalLpSupply = 0.0 + allocate actor arg = do + result <- submit actor $ exerciseCmd factoryCid + (arg with extraArgs = extraArgs) + case result.output of + V2.AllocationInstructionResult_Completed cid -> pure cid + _ -> abort "Mock factory should complete staged allocation immediately" + + allocationPlan <- submit (actAs [operator, lpRegistrar]) $ + exerciseCmd dvpCid Dvp.PoolLiquidityRules_PreviewAddAllocations with + preparation + requestedAt = now + operatorBaseReceiverCid <- allocate operator allocationPlan.baseReceiver + operatorQuoteReceiverCid <- allocate operator allocationPlan.quoteReceiver + registrarMintCid <- allocate lpRegistrar allocationPlan.lpMintSender + + -- Exercise the read-only batch preview too: this fixture mirrors the same + -- operation-specific discovery sequence used by the backend. + _settlementPlan <- submit (actAs [operator, lpRegistrar]) $ + exerciseCmd dvpCid Dvp.PoolLiquidityRules_PreviewAddSettlement with + preparation + operatorBaseReceiverCid + operatorQuoteReceiverCid + registrarMintCid + + submit (actAs [operator, lpRegistrar]) $ + exerciseCmd dvpCid Dvp.PoolLiquidityRules_SettleAddLiquidity with + expectedPoolId = preparation.expectedPoolId + poolCid = preparation.poolCid + poolStateCid = preparation.poolStateCid + lpPolicyCid = preparation.lpPolicyCid + requestCid = preparation.requestCid + acceptanceCid = preparation.acceptanceCid + recipient = preparation.recipient + lpBaseDepositCid = preparation.lpBaseDepositCid + lpQuoteDepositCid = preparation.lpQuoteDepositCid + lpReceiptCid = preparation.lpReceiptCid + baseFactoryCid = factoryCid + quoteFactoryCid = factoryCid + lpFactoryCid = factoryCid + baseQuoteSettleCid = settleCid + lpSettleCid = settleCid + baseAmount = preparation.baseAmount + quoteAmount = preparation.quoteAmount + minLpTokens = preparation.minLpTokens + knownTotalLpSupply = preparation.knownTotalLpSupply + requestedAt = now + poolAdminExtraArgs = extraArgs + lpRegistrarExtraArgs = extraArgs + operatorBaseReceiverCid = Some operatorBaseReceiverCid + operatorQuoteReceiverCid = Some operatorQuoteReceiverCid + registrarMintCid = Some registrarMintCid diff --git a/trading/CantonDex/Dex/DexPair.daml b/trading/CantonDex/Dex/DexPair.daml index f45bcf33..cd8b05bb 100644 --- a/trading/CantonDex/Dex/DexPair.daml +++ b/trading/CantonDex/Dex/DexPair.daml @@ -3,6 +3,11 @@ -- counters, and active flag. Pair lifecycle changes go through the choices -- defined here. -- +-- `active` and `tradingMode` are listing/orchestration policy in this reference. +-- PoolRules and OrderMatchExecution do not fetch a DexPair, so these fields are +-- not an on-ledger settlement gate. Deployments that need such a gate must bind +-- the pair contract into their terminal choices and validate it there. +-- -- Design guide: `docs/concepts/design-tour.md#pair-and-governance-state`. module CantonDex.Dex.DexPair where @@ -22,6 +27,15 @@ data FeeModel = FeeModel with poolFeeBps : Int -- ^ Pool swap fee retained for LPs, bps. deriving (Eq, Show) +validFeeBps : Int -> Bool +validFeeBps fee = fee >= 0 && fee < 10000 + +validFeeModel : FeeModel -> Bool +validFeeModel fees = + validFeeBps fees.makerFeeBps + && validFeeBps fees.takerFeeBps + && validFeeBps fees.poolFeeBps + template DexPair with operator : Party admin : Party @@ -32,11 +46,13 @@ template DexPair with quoteInstrumentId : Text -- ^ `id` component of the quote instrument, under `admin`. tradingMode : TradingMode - -- ^ Which surfaces (order book, pool, or both) the pair exposes. + -- ^ Which surfaces off-ledger discovery and routing should expose. Active + -- Daml settlement choices do not fetch this listing record. feeModel : FeeModel -- ^ Maker/taker/pool fee schedule in bps. active : Bool - -- ^ When False, the pair is listed but trading is gated off. + -- ^ Off-ledger listing/routing flag. When False the pair remains listed; + -- this field alone does not prevent a direct Daml settlement exercise. publicReaders : Optional [Party] -- ^ Parties allowed to observe the listing; None/[] = operator-private -- except for admin. @@ -48,6 +64,12 @@ template DexPair with signatory operator observer admin :: optional [] identity publicReaders + ensure + baseInstrumentId /= "" + && quoteInstrumentId /= "" + && baseInstrumentId /= quoteInstrumentId + && validFeeModel feeModel + choice DexPair_UpdateFeeModel : ContractId DexPair with newFeeModel : FeeModel @@ -80,6 +102,8 @@ template DexPair with -- ^ Notional touched per instrument id by the matched trade. controller operator do + assertMsg "Trade notionals must be non-negative" + (all (\(_, notional) -> notional >= 0.0) (TextMap.toList legNotionals)) let curMaker = optional TextMap.empty identity accumulatedMakerFees curTaker = optional TextMap.empty identity accumulatedTakerFees bumpedMaker = foldl diff --git a/trading/CantonDex/Dex/MatchedTrade.daml b/trading/CantonDex/Dex/MatchedTrade.daml index 9bdc2462..85b8f815 100644 --- a/trading/CantonDex/Dex/MatchedTrade.daml +++ b/trading/CantonDex/Dex/MatchedTrade.daml @@ -18,6 +18,26 @@ import CantonDex.Dex.DexPair qualified as DexPair import CantonDex.Dex.PolicyReceipt qualified as PR import CantonDex.Trading.Utils qualified as Utils +-- A settlement plan must identify each positive transfer leg exactly once. +-- Registry settlement performs its own coverage checks; keeping the same basic +-- shape invariant on the DEX contract makes malformed trades impossible to +-- publish in the first place. +validTransferLegs : [V2.TransferLeg] -> Bool +validTransferLegs legs = + legs /= [] + && all + (\leg -> + leg.transferLegId /= "" + && leg.instrumentId /= "" + && leg.amount > 0.0 + && leg.sender /= leg.receiver) + legs + && uniqueTexts [leg.transferLegId | leg <- legs] + where + uniqueTexts xs = + length xs + == length (foldl (\seen x -> if x `elem` seen then seen else x :: seen) [] xs) + data TradeAllocationRequestView = TradeAllocationRequestView with authorizer : V2.Account settlement : V2.SettlementInfo @@ -35,6 +55,13 @@ template TradeAllocationRequest with signatory settlement.executors observer Utils.accountParties authorizer + ensure + validTransferLegs transferLegs + && all + (\leg -> leg.sender == authorizer || leg.receiver == authorizer) + transferLegs + && optional True (\deadline -> requestedAt <= deadline) settlementDeadline + interface instance V2.AllocationRequest for TradeAllocationRequest where view = V2.AllocationRequestView with originalRequestCid = None @@ -88,6 +115,14 @@ data SettlementBatchV2 = SettlementBatchV2 with -- Upgrades require new fields last, so this stays at the end. deriving (Eq, Show) +-- Registry-discovery input without a factory or choice context. The preview +-- turns each plan into the exact SettleBatch argument the consuming choice +-- will exercise. +data SettlementBatchPlanV2 = SettlementBatchPlanV2 with + allocations : [V2.FinalizedAllocation] + transferLegs : [V2.TransferLeg] + deriving (Eq, Show) + data MatchedTrade_SettleResult = MatchedTrade_SettleResult with resultsByAdmin : Map.Map Party V2.SettlementFactory_SettleBatchResult deriving (Eq, Show) @@ -103,6 +138,21 @@ mkTradeSettlementInfo tradeCid trade = V2.SettlementInfo with cid = Some (coerceContractId tradeCid) meta = PR.foldPolicyReceiptIntoMetadata trade.policyReceipt emptyMetadata +mkTradeSettlementArguments + : Party + -> V2.SettlementInfo + -> [V2.TransferLeg] + -> [V2.FinalizedAllocation] + -> ExtraArgs + -> V2.SettlementFactory_SettleBatch +mkTradeSettlementArguments venue settlement transferLegs allocations extraArgs = + V2.SettlementFactory_SettleBatch with + settlement + transferLegs + allocations + actors = [venue] + extraArgs + -- | Every account party appearing on the trade's legs, so a counterparty can -- see the trade directly rather than waiting for an AllocationRequest. legParties : [V2.TransferLeg] -> [Party] @@ -126,9 +176,10 @@ template MatchedTrade with -- The receipt's claimed signer must equal the trade's signatory. ensure - case policyReceipt of - None -> True - Some r -> r.signedBy == venue && PR.isWellFormed r + validTransferLegs transferLegs + && case policyReceipt of + None -> True + Some r -> r.signedBy == venue && PR.isWellFormed r nonconsuming choice MatchedTrade_RequestAllocations : [ContractId TradeAllocationRequest] with @@ -145,6 +196,19 @@ template MatchedTrade with transferLegs = legs requestedAt = now + nonconsuming choice MatchedTrade_PreviewSettlement + : Map.Map Party V2.SettlementFactory_SettleBatch + with + plansByAdmin : Map.Map Party SettlementBatchPlanV2 + controller venue + do + let settlement = mkTradeSettlementInfo self this + pure $ Map.fromList + [ (batchAdmin, mkTradeSettlementArguments + venue settlement plan.transferLegs plan.allocations Utils.emptyExtraArgs) + | (batchAdmin, plan) <- Map.toList plansByAdmin + ] + choice MatchedTrade_Settle : MatchedTrade_SettleResult with batchesByAdmin : Map.Map Party SettlementBatchV2 @@ -163,12 +227,9 @@ template MatchedTrade with Some legs -> pure legs None -> abort "MatchedTrade_Settle: per-admin transfer legs are required on every settlement batch" - result <- exercise batch.factoryCid V2.SettlementFactory_SettleBatch with - settlement - transferLegs = batchLegs - allocations = batch.allocations - actors = [venue] - extraArgs = batch.extraArgs + let settlementArguments = mkTradeSettlementArguments + venue settlement batchLegs batch.allocations batch.extraArgs + result <- exercise batch.factoryCid settlementArguments pure (batchAdmin, result) forA_ dexPairCid $ \pairCid -> do diff --git a/trading/CantonDex/Dex/Order.daml b/trading/CantonDex/Dex/Order.daml index 93207b65..38b53de6 100644 --- a/trading/CantonDex/Dex/Order.daml +++ b/trading/CantonDex/Dex/Order.daml @@ -118,7 +118,18 @@ template Order with signatory operator observer trader - ensure remainingQty > 0.0 + ensure + baseInstrumentId /= "" + && quoteInstrumentId /= "" + && baseInstrumentId /= quoteInstrumentId + && limitPrice > 0.0 + && remainingQty > 0.0 + && settlementRef.id /= "" + && case (status, allocationCid) of + (OS_Pending, None) -> True + (OS_Funded, Some _) -> True + (OS_PartiallyFilled, Some _) -> True + _ -> False -- Bind an already-trader-funded allocation onto the order. This does NOT -- call the factory: the trader authors the allocation (so their holdings @@ -223,6 +234,12 @@ template OrderAllocationRequest with signatory operator observer trader + ensure + lockInstrumentId /= "" + && lockAmount > 0.0 + && orderRef.id /= "" + && requestedAt <= optional requestedAt identity expiry + interface instance V2.AllocationRequest for OrderAllocationRequest where view = V2.AllocationRequestView with originalRequestCid = None diff --git a/trading/CantonDex/Dex/OrderFundingRequest.daml b/trading/CantonDex/Dex/OrderFundingRequest.daml index 4dac7711..8823380a 100644 --- a/trading/CantonDex/Dex/OrderFundingRequest.daml +++ b/trading/CantonDex/Dex/OrderFundingRequest.daml @@ -25,7 +25,12 @@ template OrderFundingRequest with signatory trader observer operator - ensure quantity > 0.0 && limitPrice > 0.0 + ensure + baseInstrumentId /= "" + && quoteInstrumentId /= "" + && baseInstrumentId /= quoteInstrumentId + && quantity > 0.0 + && limitPrice > 0.0 choice OrderFundingRequest_Cancel : () controller trader diff --git a/trading/CantonDex/Dex/OrderMatchExecution.daml b/trading/CantonDex/Dex/OrderMatchExecution.daml index e5d989cf..c3a1b0be 100644 --- a/trading/CantonDex/Dex/OrderMatchExecution.daml +++ b/trading/CantonDex/Dex/OrderMatchExecution.daml @@ -112,6 +112,106 @@ rollOrderForward orderCid order filledQty residualFunding nextAllocationCid = do | otherwise -> abort "fully filled order must not roll funding forward" _ -> abort "residual funding and rolled-forward allocation disagree" +data PreparedOrderMatch = PreparedOrderMatch with + preparedBuyOrder : Order + preparedSellOrder : Order + preparedBuyerFunding : Optional Utils.Funding + preparedSellerFunding : Optional Utils.Funding + preparedSettlementArguments : V2.SettlementFactory_SettleBatch + +prepareOrderMatch + : Party + -> MatchedOrderPair + -> ContractId Order + -> ContractId Order + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> Update PreparedOrderMatch +prepareOrderMatch + operator match buyOrderCid sellOrderCid + buyerAllocationCid sellerAllocationCid = do + buyOrder <- fetch buyOrderCid + sellOrder <- fetch sellOrderCid + + assertMsg "buy order operator mismatch" (buyOrder.operator == operator) + assertMsg "sell order operator mismatch" (sellOrder.operator == operator) + assertMsg "buy order must be a Bid" (buyOrder.side == Bid) + assertMsg "sell order must be an Ask" (sellOrder.side == Ask) + assertMsg "buy order is not funded" + (buyOrder.status == OS_Funded || buyOrder.status == OS_PartiallyFilled) + assertMsg "sell order is not funded" + (sellOrder.status == OS_Funded || sellOrder.status == OS_PartiallyFilled) + assertMsg "buyer allocation is not the bid order's allocation" + (buyOrder.allocationCid == Some buyerAllocationCid) + assertMsg "seller allocation is not the ask order's allocation" + (sellOrder.allocationCid == Some sellerAllocationCid) + assertMsg "buyer account is not the bid trader" + (match.buyerAccount.owner == Some buyOrder.trader) + assertMsg "seller account is not the ask trader" + (match.sellerAccount.owner == Some sellOrder.trader) + assertMsg "registry admin mismatch" (buyOrder.admin == sellOrder.admin) + assertMsg "base instrument mismatch" + (buyOrder.baseInstrumentId == sellOrder.baseInstrumentId + && match.baseInstrumentId == buyOrder.baseInstrumentId) + assertMsg "quote instrument mismatch" + (buyOrder.quoteInstrumentId == sellOrder.quoteInstrumentId + && match.quoteInstrumentId == buyOrder.quoteInstrumentId) + assertMsg "fill quantity must be positive" (match.fillQty > 0.0) + assertMsg "fill exceeds bid remaining quantity" + (match.fillQty <= buyOrder.remainingQty) + assertMsg "fill exceeds ask remaining quantity" + (match.fillQty <= sellOrder.remainingQty) + assertMsg "fill price must be positive" (match.fillPrice > 0.0) + assertMsg "fill price above bid limit" (match.fillPrice <= buyOrder.limitPrice) + assertMsg "fill price below ask limit" (match.fillPrice >= sellOrder.limitPrice) + + buyerAllocation <- fetch buyerAllocationCid + sellerAllocation <- fetch sellerAllocationCid + let buyerAllocationView = view buyerAllocation + sellerAllocationView = view sellerAllocation + assertMsg "buyer allocation belongs to a different settlement" + (buyerAllocationView.settlement == sellerAllocationView.settlement) + assertMsg "order settlement executor mismatch" + (buyerAllocationView.settlement.executors == [operator]) + assertMsg "buyer allocation authorizer mismatch" + (buyerAllocationView.allocation.authorizer == match.buyerAccount) + assertMsg "seller allocation authorizer mismatch" + (sellerAllocationView.allocation.authorizer == match.sellerAccount) + assertMsg "buyer allocation registry admin mismatch" + (buyerAllocationView.allocation.admin == buyOrder.admin) + assertMsg "seller allocation registry admin mismatch" + (sellerAllocationView.allocation.admin == sellOrder.admin) + + let transferLegs = mkMatchTransferLegs match + sideFunding : Order -> V2.Allocation -> V2.Account -> Optional Utils.Funding + sideFunding order allocation account + | match.fillQty >= order.remainingQty = None + | otherwise = + remainderFunding account (allocationFunding allocation) transferLegs + buyerFunding = sideFunding buyOrder buyerAllocation match.buyerAccount + sellerFunding = sideFunding sellOrder sellerAllocation match.sellerAccount + buyerFinalized = Utils.mkFinalizedAllocation + buyerAllocationCid + (Utils.legsToSides match.buyerAccount transferLegs) + buyerFunding + sellerFinalized = Utils.mkFinalizedAllocation + sellerAllocationCid + (Utils.legsToSides match.sellerAccount transferLegs) + sellerFunding + settlementArguments = V2.SettlementFactory_SettleBatch with + settlement = buyerAllocationView.settlement + transferLegs + allocations = [buyerFinalized, sellerFinalized] + actors = [operator] + extraArgs = Utils.emptyExtraArgs + + pure PreparedOrderMatch with + preparedBuyOrder = buyOrder + preparedSellOrder = sellOrder + preparedBuyerFunding = buyerFunding + preparedSellerFunding = sellerFunding + preparedSettlementArguments = settlementArguments + template OrderMatchExecution with operator : Party matchId : Text @@ -132,123 +232,33 @@ template OrderMatchExecution with where signatory operator + -- Read-only discovery surface for the registry's canonical settlement + -- endpoint. Execution repeats the same preparation against live state, so + -- any intervening order or allocation change aborts rather than settling + -- a stale preview. + choice OrderMatchExecution_PreviewSettlement + : V2.SettlementFactory_SettleBatch + controller operator + do + prepared <- prepareOrderMatch + operator match buyOrderCid sellOrderCid + buyerAllocationCid sellerAllocationCid + pure prepared.preparedSettlementArguments + choice OrderMatchExecution_Execute : OrderMatch_ExecuteResult with factoryCid : ContractId V2.SettlementFactory extraArgs : ExtraArgs - -- ^ Registry choice context for the SettleBatch. The caller - -- fetches this off-ledger (`getChoiceContext admin`); empty for - -- the self-registry, real for a context-requiring registry. + -- ^ Operation-specific registry context for this exact SettleBatch + -- argument; empty for the self-registry. controller operator do - -- Enforce price, quantity, and instrument terms at match time. - -- The off-ledger matcher proposes `match`; this choice fetches both - -- orders and refuses any fill the orders' own terms do not permit, so - -- a buggy or malicious matcher cannot fill a resting order outside its - -- limit price or for instruments/quantities it never agreed to. - buyOrder <- fetch buyOrderCid - sellOrder <- fetch sellOrderCid - - assertMsg "buy order operator mismatch" (buyOrder.operator == operator) - assertMsg "sell order operator mismatch" (sellOrder.operator == operator) - assertMsg "buy order must be a Bid" (buyOrder.side == Bid) - assertMsg "sell order must be an Ask" (sellOrder.side == Ask) - - -- A never-funded order has no collateral behind it, so a fill would be - -- recorded against nothing. - assertMsg "buy order is not funded" - (buyOrder.status == OS_Funded || buyOrder.status == OS_PartiallyFilled) - assertMsg "sell order is not funded" - (sellOrder.status == OS_Funded || sellOrder.status == OS_PartiallyFilled) - - -- Each order may settle only the allocation bound to that order. - assertMsg "buyer allocation is not the bid order's allocation" - (buyOrder.allocationCid == Some buyerAllocationCid) - assertMsg "seller allocation is not the ask order's allocation" - (sellOrder.allocationCid == Some sellerAllocationCid) - - -- The match's accounts must be the orders' own traders, so the legs - -- move the actual order owners' funds (not third parties'). - assertMsg "buyer account is not the bid trader" - (match.buyerAccount.owner == Some buyOrder.trader) - assertMsg "seller account is not the ask trader" - (match.sellerAccount.owner == Some sellOrder.trader) - - -- Instrument ids are only meaningful under a registry admin, so two - -- orders naming the same id under different admins are different - -- instruments. - assertMsg "registry admin mismatch" (buyOrder.admin == sellOrder.admin) - - -- Instruments must agree across both orders and the match. - assertMsg "base instrument mismatch" - (buyOrder.baseInstrumentId == sellOrder.baseInstrumentId - && match.baseInstrumentId == buyOrder.baseInstrumentId) - assertMsg "quote instrument mismatch" - (buyOrder.quoteInstrumentId == sellOrder.quoteInstrumentId - && match.quoteInstrumentId == buyOrder.quoteInstrumentId) - - -- Quantity must be positive and within both orders' remaining size. - assertMsg "fill quantity must be positive" (match.fillQty > 0.0) - assertMsg "fill exceeds bid remaining quantity" - (match.fillQty <= buyOrder.remainingQty) - assertMsg "fill exceeds ask remaining quantity" - (match.fillQty <= sellOrder.remainingQty) - - -- Price band: a buyer never pays above their limit and a seller never - -- receives below theirs. A crossing book guarantees such a price exists - -- (bid limit >= ask limit); the cleared fill price must sit in [ask, bid]. - assertMsg "fill price must be positive" (match.fillPrice > 0.0) - assertMsg "fill price above bid limit" - (match.fillPrice <= buyOrder.limitPrice) - assertMsg "fill price below ask limit" - (match.fillPrice >= sellOrder.limitPrice) - - buyerAllocation <- fetch buyerAllocationCid - sellerAllocation <- fetch sellerAllocationCid - let buyerAllocationView = view buyerAllocation - sellerAllocationView = view sellerAllocation - assertMsg "buyer allocation belongs to a different settlement" - (buyerAllocationView.settlement == sellerAllocationView.settlement) - assertMsg "order settlement executor mismatch" - (buyerAllocationView.settlement.executors == [operator]) - assertMsg "buyer allocation authorizer mismatch" - (buyerAllocationView.allocation.authorizer == match.buyerAccount) - assertMsg "seller allocation authorizer mismatch" - (sellerAllocationView.allocation.authorizer == match.sellerAccount) - assertMsg "buyer allocation registry admin mismatch" - (buyerAllocationView.allocation.admin == buyOrder.admin) - assertMsg "seller allocation registry admin mismatch" - (sellerAllocationView.allocation.admin == sellOrder.admin) - - let transferLegs = mkMatchTransferLegs match - settlement = buyerAllocationView.settlement - - -- Each side authorizes both legs; legsToSides projects the account's own - -- sides. Remainder funding is derived from the live allocation budget, - -- not recomputed from face terms, so repeated fills preserve the exact - -- registry-backed residual. A fully filled side rolls nothing forward. - let sideFunding : Order -> V2.Allocation -> V2.Account -> Optional Utils.Funding - sideFunding order allocation account - | match.fillQty >= order.remainingQty = None - | otherwise = - remainderFunding account (allocationFunding allocation) transferLegs - buyerFunding = sideFunding buyOrder buyerAllocation match.buyerAccount - sellerFunding = sideFunding sellOrder sellerAllocation match.sellerAccount - buyerFinalized = Utils.mkFinalizedAllocation - buyerAllocationCid - (Utils.legsToSides match.buyerAccount transferLegs) - buyerFunding - sellerFinalized = Utils.mkFinalizedAllocation - sellerAllocationCid - (Utils.legsToSides match.sellerAccount transferLegs) - sellerFunding - - settleResult <- exercise factoryCid V2.SettlementFactory_SettleBatch with - settlement - transferLegs - allocations = [buyerFinalized, sellerFinalized] - actors = [operator] - extraArgs + prepared <- prepareOrderMatch + operator match buyOrderCid sellOrderCid + buyerAllocationCid sellerAllocationCid + let settlementArguments = + prepared.preparedSettlementArguments with extraArgs = extraArgs + settleResult <- exercise factoryCid settlementArguments let nextIterCids = Utils.nextIterationAllocationCids settleResult buyerNext = case nextIterCids of @@ -261,16 +271,18 @@ template OrderMatchExecution with -- Settlement and both order transitions are atomic. Every remainder is -- created in the same transaction as the next allocation it references. buyRemainderCid <- rollOrderForward - buyOrderCid buyOrder match.fillQty buyerFunding buyerNext + buyOrderCid prepared.preparedBuyOrder match.fillQty + prepared.preparedBuyerFunding buyerNext sellRemainderCid <- rollOrderForward - sellOrderCid sellOrder match.fillQty sellerFunding sellerNext + sellOrderCid prepared.preparedSellOrder match.fillQty + prepared.preparedSellerFunding sellerNext now <- getTime settledTradeCid <- create MT.SettledTrade with operator - admin = buyOrder.admin + admin = prepared.preparedBuyOrder.admin matchId - transferLegs + transferLegs = settlementArguments.transferLegs settledAt = now pure OrderMatch_ExecuteResult with diff --git a/trading/CantonDex/Dex/Pool.daml b/trading/CantonDex/Dex/Pool.daml index 7bc07704..cbffd032 100644 --- a/trading/CantonDex/Dex/Pool.daml +++ b/trading/CantonDex/Dex/Pool.daml @@ -53,3 +53,13 @@ template Pool with where signatory operator observer lpRegistrar + + ensure + poolId /= "" + && baseInstrumentId /= "" + && quoteInstrumentId /= "" + && baseInstrumentId /= quoteInstrumentId + && lpInstrumentId.id /= "" + && lpInstrumentId.admin == lpRegistrar + && feeBps >= 0 + && feeBps < 10000 diff --git a/trading/CantonDex/Dex/PoolLiquidityRules.daml b/trading/CantonDex/Dex/PoolLiquidityRules.daml index 4099483c..c15a7c0b 100644 --- a/trading/CantonDex/Dex/PoolLiquidityRules.daml +++ b/trading/CantonDex/Dex/PoolLiquidityRules.daml @@ -9,6 +9,7 @@ module CantonDex.Dex.PoolLiquidityRules where import DA.Assert (assertWithinDeadline) import DA.Foldable (forA_) +import DA.List (dedup) import DA.TextMap qualified as TextMap import Splice.Api.Token.AllocationV2 qualified as V2 @@ -209,6 +210,406 @@ removeLiquiditySpecs pool operator holder baseOuts quoteOuts lpBurnAmount = , dvpSpec pool.admin holderAcct quoteLegs False , dvpSpec pool.lpRegistrar holderAcct [burnLeg] True ] +data AddLiquidityAllocationPlan = AddLiquidityAllocationPlan with + baseReceiver : V2.AllocationFactory_Allocate + quoteReceiver : V2.AllocationFactory_Allocate + lpMintSender : V2.AllocationFactory_Allocate + deriving (Eq, Show) + +data AddLiquiditySettlementPlan = AddLiquiditySettlementPlan with + baseQuoteBatch : V2.SettlementFactory_SettleBatch + lpBatch : V2.SettlementFactory_SettleBatch + deriving (Eq, Show) + +data RemoveLiquidityAllocationPlan = RemoveLiquidityAllocationPlan with + lpBurnReceiver : V2.AllocationFactory_Allocate + deriving (Eq, Show) + +data RemoveLiquiditySettlementPlan = RemoveLiquiditySettlementPlan with + baseQuoteBatch : V2.SettlementFactory_SettleBatch + lpBatch : V2.SettlementFactory_SettleBatch + deriving (Eq, Show) + +data PreparedAddLiquidity = PreparedAddLiquidity with + addPool : Pool + addState : PS.PoolState + addRecipientAccount : V2.Account + addOperatorAccount : V2.Account + addSettlement : V2.SettlementInfo + addBaseDepositLeg : V2.TransferLeg + addQuoteDepositLeg : V2.TransferLeg + addBaseRefundLegs : [V2.TransferLeg] + addQuoteRefundLegs : [V2.TransferLeg] + addLpMintLeg : V2.TransferLeg + addLpAmount : Decimal + addBaseUsed : Decimal + addQuoteUsed : Decimal + +prepareAddLiquidity + : Party + -> Party + -> PoolId + -> ContractId Pool + -> ContractId PS.PoolState + -> ContractId LP.LPTokenPolicy + -> Optional (ContractId LAR.LiquidityAllocationRequest) + -> Optional (ContractId LAR.LiquidityAllocationAcceptance) + -> Party + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> Decimal + -> Decimal + -> Decimal + -> Decimal + -> Update PreparedAddLiquidity +prepareAddLiquidity + operator lpRegistrar expectedPoolId poolCid poolStateCid lpPolicyCid + requestCid acceptanceCid recipient lpBaseDepositCid lpQuoteDepositCid + lpReceiptCid baseAmount quoteAmount minLpTokens knownTotalLpSupply = do + forA_ [lpBaseDepositCid, lpQuoteDepositCid, lpReceiptCid] + enforceAllocationDeadline + binding <- resolveBinding requestCid acceptanceCid + let settlement = PM.poolSettlement poolCid operator + validateAgainstBinding binding recipient settlement + [lpBaseDepositCid, lpQuoteDepositCid, lpReceiptCid] + (pool, state) <- PM.fetchPoolContext operator expectedPoolId poolCid poolStateCid + assertMsg "Pool cannot be Paused" (state.status /= PS_Paused) + assertMsg "LP registrar mismatch" (pool.lpRegistrar == lpRegistrar) + assertMsg "Base amount must be positive" (baseAmount > 0.0) + assertMsg "Quote amount must be positive" (quoteAmount > 0.0) + assertMsg "recorded LP supply must be non-negative" (state.totalLpSupply >= 0.0) + policy <- fetch lpPolicyCid + assertMsg "LP policy supply must be non-negative" (policy.totalSupply >= 0.0) + assertMsg "LPTokenPolicy/PoolState supply divergence" + (policy.totalSupply == state.totalLpSupply) + assertMsg "knownTotalLpSupply must match recorded supply" + (knownTotalLpSupply == state.totalLpSupply) + + let recipientAcct = Utils.basicAccount recipient + operatorAcct = Utils.basicAccount operator + fairLp = + if state.totalLpSupply == 0.0 + then PM.sqrtDecimal (baseAmount * quoteAmount) + else min ((baseAmount * state.totalLpSupply) / state.reserves.baseAmount) + ((quoteAmount * state.totalLpSupply) / state.reserves.quoteAmount) + lpAmount <- allocationLegAmount "lp-mint" lpReceiptCid + assertMsg "LP tokens below minimum" (lpAmount >= minLpTokens) + assertMsg "LP receipt exceeds fair share beyond dust tolerance" + (lpAmount - fairLp <= lpMintDustTolerance) + assertMsg "LP receipt shortfall beyond dust tolerance" + (fairLp - lpAmount <= lpMintDustTolerance) + + let (baseUsed, quoteUsed) = + if state.totalLpSupply == 0.0 + then (baseAmount, quoteAmount) + else PM.ratioMatchedDeposit state.reserves baseAmount quoteAmount + baseRefund = baseAmount - baseUsed + quoteRefund = quoteAmount - quoteUsed + assertMsg "Ratio-matched deposit must be positive on both sides" + (baseUsed > 0.0 && quoteUsed > 0.0) + + let baseDepositLeg = dvpLeg + "lp-base-deposit" recipientAcct operatorAcct baseAmount pool.baseInstrumentId + quoteDepositLeg = dvpLeg + "lp-quote-deposit" recipientAcct operatorAcct quoteAmount pool.quoteInstrumentId + refundLeg legId instrumentId amount = + dvpLeg legId operatorAcct recipientAcct amount instrumentId + baseRefundLegs = + [refundLeg "lp-base-refund" pool.baseInstrumentId baseRefund | baseRefund > 0.0] + quoteRefundLegs = + [refundLeg "lp-quote-refund" pool.quoteInstrumentId quoteRefund | quoteRefund > 0.0] + lpMintLeg = Lp.lpMintLeg recipientAcct pool.lpInstrumentId.id lpAmount + + pure PreparedAddLiquidity with + addPool = pool + addState = state + addRecipientAccount = recipientAcct + addOperatorAccount = operatorAcct + addSettlement = settlement + addBaseDepositLeg = baseDepositLeg + addQuoteDepositLeg = quoteDepositLeg + addBaseRefundLegs = baseRefundLegs + addQuoteRefundLegs = quoteRefundLegs + addLpMintLeg = lpMintLeg + addLpAmount = lpAmount + addBaseUsed = baseUsed + addQuoteUsed = quoteUsed + +mkAddAllocationPlan + : Party -> Party -> Time -> PreparedAddLiquidity -> AddLiquidityAllocationPlan +mkAddAllocationPlan operator lpRegistrar requestedAt prepared = + AddLiquidityAllocationPlan with + baseReceiver = mkOperatorReceiver + operator prepared.addPool.admin prepared.addSettlement + prepared.addBaseDepositLeg requestedAt Utils.emptyExtraArgs + quoteReceiver = mkOperatorReceiver + operator prepared.addPool.admin prepared.addSettlement + prepared.addQuoteDepositLeg requestedAt Utils.emptyExtraArgs + lpMintSender = V2.AllocationFactory_Allocate with + settlement = prepared.addSettlement + allocation = Lp.mintSenderSpec lpRegistrar prepared.addLpMintLeg + requestedAt + inputHoldingCids = [] + actors = [lpRegistrar] + extraArgs = Utils.emptyExtraArgs + +mkAddSettlementPlan + : Party + -> PreparedAddLiquidity + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ExtraArgs + -> ExtraArgs + -> AddLiquiditySettlementPlan +mkAddSettlementPlan + operator prepared lpBaseDepositCid lpQuoteDepositCid lpReceiptCid + operatorBaseReceiverCid operatorQuoteReceiverCid registrarMintCid + poolAdminExtraArgs lpRegistrarExtraArgs = + let baseQuoteBatch = V2.SettlementFactory_SettleBatch with + settlement = prepared.addSettlement + transferLegs = + [prepared.addBaseDepositLeg, prepared.addQuoteDepositLeg] + ++ prepared.addBaseRefundLegs ++ prepared.addQuoteRefundLegs + allocations = + [ Utils.mkFinalizedAllocation lpBaseDepositCid + (Utils.legsToSides prepared.addRecipientAccount prepared.addBaseRefundLegs) None + , Utils.mkFinalizedAllocation lpQuoteDepositCid + (Utils.legsToSides prepared.addRecipientAccount prepared.addQuoteRefundLegs) None + , Utils.mkFinalizedAllocation operatorBaseReceiverCid + (Utils.legsToSides prepared.addOperatorAccount prepared.addBaseRefundLegs) + (Some (TextMap.fromList + [(prepared.addPool.baseInstrumentId, prepared.addBaseUsed)])) + , Utils.mkFinalizedAllocation operatorQuoteReceiverCid + (Utils.legsToSides prepared.addOperatorAccount prepared.addQuoteRefundLegs) + (Some (TextMap.fromList + [(prepared.addPool.quoteInstrumentId, prepared.addQuoteUsed)])) + ] + actors = [operator] + extraArgs = poolAdminExtraArgs + lpBatch = V2.SettlementFactory_SettleBatch with + settlement = prepared.addSettlement + transferLegs = [prepared.addLpMintLeg] + allocations = + [Utils.finalAllocation registrarMintCid, Utils.finalAllocation lpReceiptCid] + actors = [operator] + extraArgs = lpRegistrarExtraArgs + in AddLiquiditySettlementPlan with baseQuoteBatch; lpBatch + +data PreparedRemoveLiquidity = PreparedRemoveLiquidity with + removePool : Pool + removeState : PS.PoolState + removeHolderAccount : V2.Account + removeOperatorAccount : V2.Account + removeSettlement : V2.SettlementInfo + removeBurnLeg : V2.TransferLeg + removeBaseDraw : PM.SliceDraw + removeQuoteDraw : PM.SliceDraw + removeBaseDelivery : PE.SideDelivery + removeQuoteDelivery : PE.SideDelivery + removeBaseOut : Decimal + removeQuoteOut : Decimal + +prepareRemoveLiquidity + : Party + -> Party + -> PoolId + -> ContractId Pool + -> ContractId PS.PoolState + -> ContractId LP.LPTokenPolicy + -> Optional (ContractId LAR.LiquidityAllocationRequest) + -> Optional (ContractId LAR.LiquidityAllocationAcceptance) + -> Party + -> Decimal + -> Decimal + -> Decimal + -> Decimal + -> [ContractId PSlice.PoolSlice] + -> [ContractId PSlice.PoolSlice] + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> Update PreparedRemoveLiquidity +prepareRemoveLiquidity + operator lpRegistrar expectedPoolId poolCid poolStateCid lpPolicyCid + requestCid acceptanceCid holder lpTokensToRedeem knownTotalLpSupply + minBaseOut minQuoteOut baseSliceCids quoteSliceCids holderBaseReceiptCid + holderQuoteReceiptCid holderBurnSenderCid = do + forA_ [holderBaseReceiptCid, holderQuoteReceiptCid, holderBurnSenderCid] + enforceAllocationDeadline + binding <- resolveBinding requestCid acceptanceCid + let settlement = PM.poolSettlement poolCid operator + validateAgainstBinding binding holder settlement + [holderBaseReceiptCid, holderQuoteReceiptCid, holderBurnSenderCid] + (pool, state) <- PM.fetchPoolContext operator expectedPoolId poolCid poolStateCid + assertMsg "Pool must be Active to remove liquidity" (state.status == PS_Active) + assertMsg "LP registrar mismatch" (pool.lpRegistrar == lpRegistrar) + assertMsg "LP tokens must be positive" (lpTokensToRedeem > 0.0) + assertMsg "recorded LP supply must be positive" (state.totalLpSupply > 0.0) + assertMsg "known LP supply must be positive" (knownTotalLpSupply > 0.0) + assertMsg "Cannot redeem more than supply" (lpTokensToRedeem <= knownTotalLpSupply) + policy <- fetch lpPolicyCid + assertMsg "LP policy supply must be positive" (policy.totalSupply > 0.0) + assertMsg "LPTokenPolicy/PoolState supply divergence" + (policy.totalSupply == state.totalLpSupply) + assertMsg "knownTotalLpSupply must match recorded supply" + (knownTotalLpSupply == state.totalLpSupply) + + let share = PM.floorDiv lpTokensToRedeem knownTotalLpSupply + baseOut = PM.floorMul state.reserves.baseAmount share + quoteOut = PM.floorMul state.reserves.quoteAmount share + assertMsg "Base output below minimum" (baseOut >= minBaseOut) + assertMsg "Quote output below minimum" (quoteOut >= minQuoteOut) + assertMsg "Remove base slices must be unique" + (length (dedup baseSliceCids) == length baseSliceCids) + assertMsg "Remove quote slices must be unique" + (length (dedup quoteSliceCids) == length quoteSliceCids) + + baseItems <- forA baseSliceCids (\cid -> do s <- fetch cid; pure (cid, s)) + quoteItems <- forA quoteSliceCids (\cid -> do s <- fetch cid; pure (cid, s)) + forA_ baseItems $ \(_, s) -> do + assertMsg "Base slice belongs to a different pool" (s.poolId == pool.poolId) + assertMsg "Base slice operator mismatch" (s.operator == operator) + assertMsg "Base slice is on the wrong reserve side" (s.side == BaseSide) + forA_ quoteItems $ \(_, s) -> do + assertMsg "Quote slice belongs to a different pool" (s.poolId == pool.poolId) + assertMsg "Quote slice operator mismatch" (s.operator == operator) + assertMsg "Quote slice is on the wrong reserve side" (s.side == QuoteSide) + assertMsg "Provided base slices cannot cover the redemption" + (foldl (\a (_, s) -> a + s.amount) 0.0 baseItems >= baseOut) + assertMsg "Provided quote slices cannot cover the redemption" + (foldl (\a (_, s) -> a + s.amount) 0.0 quoteItems >= quoteOut) + + let holderAcct = Utils.basicAccount holder + operatorAcct = Utils.basicAccount operator + burnLeg = Lp.lpBurnLeg holderAcct pool.lpInstrumentId.id lpTokensToRedeem + baseDraw = PM.drawFromSlices baseOut baseItems + quoteDraw = PM.drawFromSlices quoteOut quoteItems + baseDel = PE.buildSideDelivery + "lp-base-out-" operatorAcct holderAcct pool.baseInstrumentId baseDraw + quoteDel = PE.buildSideDelivery + "lp-quote-out-" operatorAcct holderAcct pool.quoteInstrumentId quoteDraw + + pure PreparedRemoveLiquidity with + removePool = pool + removeState = state + removeHolderAccount = holderAcct + removeOperatorAccount = operatorAcct + removeSettlement = settlement + removeBurnLeg = burnLeg + removeBaseDraw = baseDraw + removeQuoteDraw = quoteDraw + removeBaseDelivery = baseDel + removeQuoteDelivery = quoteDel + removeBaseOut = baseOut + removeQuoteOut = quoteOut + +mkRemoveAllocationPlan + : Party -> Time -> PreparedRemoveLiquidity -> RemoveLiquidityAllocationPlan +mkRemoveAllocationPlan lpRegistrar requestedAt prepared = + RemoveLiquidityAllocationPlan with + lpBurnReceiver = V2.AllocationFactory_Allocate with + settlement = prepared.removeSettlement + allocation = Lp.burnReceiptSpec lpRegistrar prepared.removeBurnLeg + requestedAt + inputHoldingCids = [] + actors = [lpRegistrar] + extraArgs = Utils.emptyExtraArgs + +mkRemoveSettlementPlan + : Party + -> PreparedRemoveLiquidity + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ContractId V2.Allocation + -> ExtraArgs + -> ExtraArgs + -> RemoveLiquiditySettlementPlan +mkRemoveSettlementPlan + operator prepared holderBaseReceiptCid holderQuoteReceiptCid + holderBurnSenderCid registrarBurnReceiverCid + poolAdminExtraArgs lpRegistrarExtraArgs = + let baseQuoteBatch = V2.SettlementFactory_SettleBatch with + settlement = prepared.removeSettlement + transferLegs = prepared.removeBaseDelivery.legs ++ prepared.removeQuoteDelivery.legs + allocations = + prepared.removeBaseDelivery.sliceFinalizeds + ++ [Utils.finalAllocation holderBaseReceiptCid] + ++ prepared.removeQuoteDelivery.sliceFinalizeds + ++ [Utils.finalAllocation holderQuoteReceiptCid] + actors = [operator] + extraArgs = poolAdminExtraArgs + lpBatch = V2.SettlementFactory_SettleBatch with + settlement = prepared.removeSettlement + transferLegs = [prepared.removeBurnLeg] + allocations = + [ Utils.finalAllocation holderBurnSenderCid + , Utils.finalAllocation registrarBurnReceiverCid + ] + actors = [operator] + extraArgs = lpRegistrarExtraArgs + in RemoveLiquiditySettlementPlan with baseQuoteBatch; lpBatch + +data AddLiquidityPreparationArgs = AddLiquidityPreparationArgs with + expectedPoolId : PoolId + poolCid : ContractId Pool + poolStateCid : ContractId PS.PoolState + lpPolicyCid : ContractId LP.LPTokenPolicy + requestCid : Optional (ContractId LAR.LiquidityAllocationRequest) + acceptanceCid : Optional (ContractId LAR.LiquidityAllocationAcceptance) + recipient : Party + lpBaseDepositCid : ContractId V2.Allocation + lpQuoteDepositCid : ContractId V2.Allocation + lpReceiptCid : ContractId V2.Allocation + baseAmount : Decimal + quoteAmount : Decimal + minLpTokens : Decimal + knownTotalLpSupply : Decimal + deriving (Eq, Show) + +prepareAddFrom + : Party -> Party -> AddLiquidityPreparationArgs -> Update PreparedAddLiquidity +prepareAddFrom operator lpRegistrar args = + prepareAddLiquidity + operator lpRegistrar args.expectedPoolId args.poolCid args.poolStateCid + args.lpPolicyCid args.requestCid args.acceptanceCid args.recipient + args.lpBaseDepositCid args.lpQuoteDepositCid args.lpReceiptCid + args.baseAmount args.quoteAmount args.minLpTokens args.knownTotalLpSupply + +data RemoveLiquidityPreparationArgs = RemoveLiquidityPreparationArgs with + expectedPoolId : PoolId + poolCid : ContractId Pool + poolStateCid : ContractId PS.PoolState + lpPolicyCid : ContractId LP.LPTokenPolicy + requestCid : Optional (ContractId LAR.LiquidityAllocationRequest) + acceptanceCid : Optional (ContractId LAR.LiquidityAllocationAcceptance) + holder : Party + lpTokensToRedeem : Decimal + knownTotalLpSupply : Decimal + minBaseOut : Decimal + minQuoteOut : Decimal + baseSliceCids : [ContractId PSlice.PoolSlice] + quoteSliceCids : [ContractId PSlice.PoolSlice] + holderBaseReceiptCid : ContractId V2.Allocation + holderQuoteReceiptCid : ContractId V2.Allocation + holderBurnSenderCid : ContractId V2.Allocation + deriving (Eq, Show) + +prepareRemoveFrom + : Party -> Party -> RemoveLiquidityPreparationArgs -> Update PreparedRemoveLiquidity +prepareRemoveFrom operator lpRegistrar args = + prepareRemoveLiquidity + operator lpRegistrar args.expectedPoolId args.poolCid args.poolStateCid + args.lpPolicyCid args.requestCid args.acceptanceCid args.holder + args.lpTokensToRedeem args.knownTotalLpSupply args.minBaseOut args.minQuoteOut + args.baseSliceCids args.quoteSliceCids args.holderBaseReceiptCid + args.holderQuoteReceiptCid args.holderBurnSenderCid + template PoolLiquidityRules with operator : Party lpRegistrar : Party @@ -253,23 +654,47 @@ template PoolLiquidityRules with create LAR.LiquidityAllocationRequest with operator; lp = holder; settlement; allocations; requestedAt; settleAt - -- DvP add: the LP's committed deposits + LP-mint receipt settle - -- atomically; the operator's receiver allocations roll forward into the - -- two new slices; the registrar mints LP tokens to the LP. Only the - -- ratio-matched part of the deposit reaches the slices — an off-ratio - -- excess buys no LP tokens and is refunded to the LP in the same batch. - nonconsuming choice PoolLiquidityRules_SettleAddLiquidity : PoolLiquidityRules_SettleAddResult + -- The registry sees each exact allocation request before the operator or + -- registrar creates it. This is a read-only preview; no funds move. + nonconsuming choice PoolLiquidityRules_PreviewAddAllocations + : AddLiquidityAllocationPlan + with + preparation : AddLiquidityPreparationArgs + requestedAt : Time + controller operator, lpRegistrar + do + prepared <- prepareAddFrom operator lpRegistrar preparation + pure $ mkAddAllocationPlan operator lpRegistrar requestedAt prepared + + -- Once those allocations exist, preview the two exact per-admin batches. + nonconsuming choice PoolLiquidityRules_PreviewAddSettlement + : AddLiquiditySettlementPlan + with + preparation : AddLiquidityPreparationArgs + operatorBaseReceiverCid : ContractId V2.Allocation + operatorQuoteReceiverCid : ContractId V2.Allocation + registrarMintCid : ContractId V2.Allocation + controller operator, lpRegistrar + do + prepared <- prepareAddFrom operator lpRegistrar preparation + pure $ mkAddSettlementPlan + operator prepared preparation.lpBaseDepositCid + preparation.lpQuoteDepositCid preparation.lpReceiptCid + operatorBaseReceiverCid operatorQuoteReceiverCid registrarMintCid + Utils.emptyExtraArgs Utils.emptyExtraArgs + + -- DvP add: allocation creation is staged only so registries can return + -- operation-specific context. Both settlement batches, pool slices, + -- reserve accounting, and LP supply update remain one atomic transaction. + nonconsuming choice PoolLiquidityRules_SettleAddLiquidity + : PoolLiquidityRules_SettleAddResult with expectedPoolId : PoolId poolCid : ContractId Pool poolStateCid : ContractId PS.PoolState lpPolicyCid : ContractId LP.LPTokenPolicy requestCid : Optional (ContractId LAR.LiquidityAllocationRequest) - -- ^ Live request for direct-allocation integrations. Mutually exclusive - -- with `acceptanceCid`; exactly one must be present. acceptanceCid : Optional (ContractId LAR.LiquidityAllocationAcceptance) - -- ^ Acceptance evidence for wallets that consume the request through - -- AllocationRequest_Accept. recipient : Party lpBaseDepositCid : ContractId V2.Allocation lpQuoteDepositCid : ContractId V2.Allocation @@ -284,125 +709,56 @@ template PoolLiquidityRules with minLpTokens : Decimal knownTotalLpSupply : Decimal requestedAt : Time - -- Split-admin DvP: the base/quote batch settles under pool.admin and - -- the LP mint/burn batch under pool.lpRegistrar, so each authority - -- boundary carries its own registry choice context. poolAdminExtraArgs : ExtraArgs lpRegistrarExtraArgs : ExtraArgs + -- Optional tail fields preserve the deployed choice shape. New callers + -- stage these allocations after operation-specific registry discovery; + -- older callers omit them and use the original factory fields above. + operatorBaseReceiverCid : Optional (ContractId V2.Allocation) + operatorQuoteReceiverCid : Optional (ContractId V2.Allocation) + registrarMintCid : Optional (ContractId V2.Allocation) controller operator, lpRegistrar do - -- Every LP-authored allocation must be unexpired (the registry does - -- not enforce settlementDeadline per-settle). - forA_ [lpBaseDepositCid, lpQuoteDepositCid, lpReceiptCid] enforceAllocationDeadline - binding <- resolveBinding requestCid acceptanceCid - validateAgainstBinding binding recipient (PM.poolSettlement poolCid operator) - [lpBaseDepositCid, lpQuoteDepositCid, lpReceiptCid] - (pool, state) <- PM.fetchPoolContext operator expectedPoolId poolCid poolStateCid - -- First-funding (Unfunded) or a funded pool (Active); paused rejects. - assertMsg "Pool must not be paused" (state.status /= PS_Paused) - assertMsg "Base amount must be positive" (baseAmount > 0.0) - assertMsg "Quote amount must be positive" (quoteAmount > 0.0) - policy <- fetch lpPolicyCid - assertMsg "LPTokenPolicy/PoolState supply divergence" - (policy.totalSupply == state.totalLpSupply) - assertMsg "knownTotalLpSupply must match recorded supply" - (knownTotalLpSupply == state.totalLpSupply) - - let recipientAcct = Utils.basicAccount recipient - settlement = PM.poolSettlement poolCid operator - -- Fair LP entitlement: sqrt(k) at first funding, else pro-rata. - fairLp = - if state.totalLpSupply == 0.0 - then PM.sqrtDecimal (baseAmount * quoteAmount) - else min ((baseAmount * state.totalLpSupply) / state.reserves.baseAmount) - ((quoteAmount * state.totalLpSupply) / state.reserves.quoteAmount) - -- Mint exactly the receipt amount, bounded symmetrically within dust: - -- the backend's floored JS-double quote and Daml's Newton sqrtDecimal - -- can land up to one 1e-10 bucket on either side of fairLp, so a - -- strict bound would abort an otherwise-fair add. - lpAmount <- allocationLegAmount "lp-mint" lpReceiptCid - assertMsg "LP tokens below minimum" (lpAmount >= minLpTokens) - assertMsg "LP receipt exceeds fair share beyond dust tolerance" - (lpAmount - fairLp <= lpMintDustTolerance) - assertMsg "LP receipt shortfall beyond dust tolerance" - (fairLp - lpAmount <= lpMintDustTolerance) - - -- Only the ratio-matched part backs the minted LP tokens, so only that - -- much may enter the pool; the long side's excess is refunded below. - let (baseUsed, quoteUsed) = - if state.totalLpSupply == 0.0 - then (baseAmount, quoteAmount) - else PM.ratioMatchedDeposit state.reserves baseAmount quoteAmount - baseRefund = baseAmount - baseUsed - quoteRefund = quoteAmount - quoteUsed - assertMsg "Ratio-matched deposit must be positive on both sides" - (baseUsed > 0.0 && quoteUsed > 0.0) - - let operatorAcct = Utils.basicAccount operator - baseDepositLeg = V2.TransferLeg with - transferLegId = "lp-base-deposit" - sender = recipientAcct; receiver = operatorAcct - amount = baseAmount; instrumentId = pool.baseInstrumentId - meta = emptyMetadata - quoteDepositLeg = V2.TransferLeg with - transferLegId = "lp-quote-deposit" - sender = recipientAcct; receiver = operatorAcct - amount = quoteAmount; instrumentId = pool.quoteInstrumentId - meta = emptyMetadata - refundLeg legId instrumentId amount = V2.TransferLeg with - transferLegId = legId - sender = operatorAcct; receiver = recipientAcct - amount; instrumentId; meta = emptyMetadata - baseRefundLegs = - [refundLeg "lp-base-refund" pool.baseInstrumentId baseRefund | baseRefund > 0.0] - quoteRefundLegs = - [refundLeg "lp-quote-refund" pool.quoteInstrumentId quoteRefund | quoteRefund > 0.0] - lpMintLeg = Lp.lpMintLeg recipientAcct pool.lpInstrumentId.id lpAmount - - opBaseRecv <- exercise baseFactoryCid - (mkOperatorReceiver operator pool.admin settlement baseDepositLeg requestedAt poolAdminExtraArgs) - >>= PM.extractCompleted - opQuoteRecv <- exercise quoteFactoryCid - (mkOperatorReceiver operator pool.admin settlement quoteDepositLeg requestedAt poolAdminExtraArgs) - >>= PM.extractCompleted - regMint <- exercise lpFactoryCid - (V2.AllocationFactory_Allocate with - settlement - allocation = Lp.mintSenderSpec lpRegistrar lpMintLeg - requestedAt; inputHoldingCids = []; actors = [lpRegistrar]; extraArgs = lpRegistrarExtraArgs) - >>= PM.extractCompleted - - -- base/quote batch (pool.admin): deposits in, operator receivers roll - -- forward with nextIterationFunding on the finalized step. - bqResult <- exercise baseQuoteSettleCid V2.SettlementFactory_SettleBatch with - settlement - transferLegs = [baseDepositLeg, quoteDepositLeg] ++ baseRefundLegs ++ quoteRefundLegs - allocations = - [ Utils.mkFinalizedAllocation lpBaseDepositCid - (Utils.legsToSides recipientAcct baseRefundLegs) None - , Utils.mkFinalizedAllocation lpQuoteDepositCid - (Utils.legsToSides recipientAcct quoteRefundLegs) None - , Utils.mkFinalizedAllocation opBaseRecv - (Utils.legsToSides operatorAcct baseRefundLegs) - (Some (TextMap.fromList [(pool.baseInstrumentId, baseUsed)])) - , Utils.mkFinalizedAllocation opQuoteRecv - (Utils.legsToSides operatorAcct quoteRefundLegs) - (Some (TextMap.fromList [(pool.quoteInstrumentId, quoteUsed)])) - ] - actors = [operator] - extraArgs = poolAdminExtraArgs + let preparation = AddLiquidityPreparationArgs with + expectedPoolId; poolCid; poolStateCid; lpPolicyCid + requestCid; acceptanceCid; recipient + lpBaseDepositCid; lpQuoteDepositCid; lpReceiptCid + baseAmount; quoteAmount; minLpTokens; knownTotalLpSupply + prepared <- prepareAddFrom operator lpRegistrar preparation + let allocationPlan = mkAddAllocationPlan operator lpRegistrar requestedAt prepared + (baseReceiverCid, quoteReceiverCid, mintCid) <- + case (operatorBaseReceiverCid, operatorQuoteReceiverCid, registrarMintCid) of + (Some b, Some q, Some m) -> pure (b, q, m) + (None, None, None) -> do + b <- exercise baseFactoryCid + (allocationPlan.baseReceiver with extraArgs = poolAdminExtraArgs) + >>= PM.extractCompleted + q <- exercise quoteFactoryCid + (allocationPlan.quoteReceiver with extraArgs = poolAdminExtraArgs) + >>= PM.extractCompleted + m <- exercise lpFactoryCid + (allocationPlan.lpMintSender with extraArgs = lpRegistrarExtraArgs) + >>= PM.extractCompleted + pure (b, q, m) + _ -> abort "SettleAddLiquidity: staged allocation cids must be all present or all absent" + let pool = prepared.addPool + state = prepared.addState + lpAmount = prepared.addLpAmount + baseUsed = prepared.addBaseUsed + quoteUsed = prepared.addQuoteUsed + settlementPlan = mkAddSettlementPlan + operator prepared preparation.lpBaseDepositCid + preparation.lpQuoteDepositCid preparation.lpReceiptCid + baseReceiverCid quoteReceiverCid mintCid + poolAdminExtraArgs lpRegistrarExtraArgs + + bqResult <- exercise baseQuoteSettleCid settlementPlan.baseQuoteBatch (nextBaseAllocCid, nextQuoteAllocCid) <- case Utils.nextIterationAllocationCids bqResult of [_, _, Some b, Some q] -> pure (b, q) _ -> abort "SettleAddLiquidity: operator receivers must roll forward" - -- LP-mint batch (pool.lpRegistrar). - _ <- exercise lpSettleCid V2.SettlementFactory_SettleBatch with - settlement - transferLegs = [lpMintLeg] - allocations = [Utils.finalAllocation regMint, Utils.finalAllocation lpReceiptCid] - actors = [operator] - extraArgs = lpRegistrarExtraArgs + _ <- exercise lpSettleCid settlementPlan.lpBatch baseSliceCid <- create PSlice.PoolSlice with poolId = pool.poolId; operator; side = BaseSide @@ -411,9 +767,10 @@ template PoolLiquidityRules with poolId = pool.poolId; operator; side = QuoteSide allocationCid = nextQuoteAllocCid; amount = quoteUsed - newPolicyCid <- exercise lpPolicyCid LP.LPTokenPolicy_RecordMint with amount = lpAmount + newPolicyCid <- exercise preparation.lpPolicyCid + LP.LPTokenPolicy_RecordMint with amount = lpAmount - archiveBinding requestCid acceptanceCid + archiveBinding preparation.requestCid preparation.acceptanceCid -- Per-choice delta conservation. This choice creates exactly -- one slice per side; the reserve deltas it writes must equal those -- created slice amounts. @@ -423,7 +780,7 @@ template PoolLiquidityRules with (newBaseSlice.amount == baseUsed) assertMsg "add: quote reserve delta must equal created quote slice amount" (newQuoteSlice.amount == quoteUsed) - archive poolStateCid + archive preparation.poolStateCid newStateCid <- create state with status = PS_Active reserves = PoolReserves with @@ -439,21 +796,40 @@ template PoolLiquidityRules with baseAdded = Some baseUsed quoteAdded = Some quoteUsed - -- DvP remove: pro-rata over aggregate reserves; sourced operator slices - -- deliver base+quote to the holder (symmetric to swap) and the holder's - -- LP tokens burn to burnAccount. - nonconsuming choice PoolLiquidityRules_SettleRemoveLiquidity : PoolLiquidityRules_SettleRemoveResult + nonconsuming choice PoolLiquidityRules_PreviewRemoveAllocations + : RemoveLiquidityAllocationPlan + with + preparation : RemoveLiquidityPreparationArgs + requestedAt : Time + controller operator, lpRegistrar + do + prepared <- prepareRemoveFrom operator lpRegistrar preparation + pure $ mkRemoveAllocationPlan lpRegistrar requestedAt prepared + + nonconsuming choice PoolLiquidityRules_PreviewRemoveSettlement + : RemoveLiquiditySettlementPlan + with + preparation : RemoveLiquidityPreparationArgs + registrarBurnReceiverCid : ContractId V2.Allocation + controller operator, lpRegistrar + do + prepared <- prepareRemoveFrom operator lpRegistrar preparation + pure $ mkRemoveSettlementPlan + operator prepared preparation.holderBaseReceiptCid + preparation.holderQuoteReceiptCid preparation.holderBurnSenderCid + registrarBurnReceiverCid Utils.emptyExtraArgs Utils.emptyExtraArgs + + -- DvP remove: the pre-created registrar burn receiver is combined with + -- both per-admin settlement batches in one atomic pool-state transition. + nonconsuming choice PoolLiquidityRules_SettleRemoveLiquidity + : PoolLiquidityRules_SettleRemoveResult with expectedPoolId : PoolId poolCid : ContractId Pool poolStateCid : ContractId PS.PoolState lpPolicyCid : ContractId LP.LPTokenPolicy requestCid : Optional (ContractId LAR.LiquidityAllocationRequest) - -- ^ Live request for direct-allocation integrations. Mutually exclusive - -- with `acceptanceCid`; exactly one must be present. acceptanceCid : Optional (ContractId LAR.LiquidityAllocationAcceptance) - -- ^ Acceptance evidence for wallets that consume the request through - -- AllocationRequest_Accept. holder : Party lpTokensToRedeem : Decimal knownTotalLpSupply : Decimal @@ -470,72 +846,39 @@ template PoolLiquidityRules with baseQuoteSettleCid : ContractId V2.SettlementFactory lpSettleCid : ContractId V2.SettlementFactory requestedAt : Time - -- Per-authority registry choice contexts (see SettleAdd). poolAdminExtraArgs : ExtraArgs lpRegistrarExtraArgs : ExtraArgs + registrarBurnReceiverCid : Optional (ContractId V2.Allocation) controller operator, lpRegistrar do - forA_ [holderBaseReceiptCid, holderQuoteReceiptCid, holderBurnSenderCid] - enforceAllocationDeadline - binding <- resolveBinding requestCid acceptanceCid - validateAgainstBinding binding holder (PM.poolSettlement poolCid operator) - [holderBaseReceiptCid, holderQuoteReceiptCid, holderBurnSenderCid] - (pool, state) <- PM.fetchPoolContext operator expectedPoolId poolCid poolStateCid - assertMsg "Pool must be Active to remove liquidity" (state.status == PS_Active) - assertMsg "LP tokens must be positive" (lpTokensToRedeem > 0.0) - assertMsg "Cannot redeem more than supply" (lpTokensToRedeem <= knownTotalLpSupply) - policy <- fetch lpPolicyCid - assertMsg "LPTokenPolicy/PoolState supply divergence" - (policy.totalSupply == state.totalLpSupply) - assertMsg "knownTotalLpSupply must match recorded supply" - (knownTotalLpSupply == state.totalLpSupply) - - -- Both the share and the payout floor: the pool never pays out more - -- than the exact pro-rata share, keeping x*y=k non-decreasing. - let share = PM.floorDiv lpTokensToRedeem knownTotalLpSupply - baseOut = PM.floorMul state.reserves.baseAmount share - quoteOut = PM.floorMul state.reserves.quoteAmount share - assertMsg "Base output below minimum" (baseOut >= minBaseOut) - assertMsg "Quote output below minimum" (quoteOut >= minQuoteOut) - - -- Draw the redemption across an ordered slice prefix per side: full - -- slices drain, only the boundary slice is re-wrapped. - baseItems <- forA baseSliceCids (\cid -> do s <- fetch cid; pure (cid, s)) - quoteItems <- forA quoteSliceCids (\cid -> do s <- fetch cid; pure (cid, s)) - assertMsg "Provided base slices cannot cover the redemption" - (foldl (\a (_, s) -> a + s.amount) 0.0 baseItems >= baseOut) - assertMsg "Provided quote slices cannot cover the redemption" - (foldl (\a (_, s) -> a + s.amount) 0.0 quoteItems >= quoteOut) - - let holderAcct = Utils.basicAccount holder - operatorAcct = Utils.basicAccount operator - settlement = PM.poolSettlement poolCid operator - burnLeg = Lp.lpBurnLeg holderAcct pool.lpInstrumentId.id lpTokensToRedeem - baseDraw = PM.drawFromSlices baseOut baseItems - quoteDraw = PM.drawFromSlices quoteOut quoteItems - baseDel = PE.buildSideDelivery "lp-base-out-" operatorAcct holderAcct pool.baseInstrumentId baseDraw - quoteDel = PE.buildSideDelivery "lp-quote-out-" operatorAcct holderAcct pool.quoteInstrumentId quoteDraw - - regBurn <- exercise lpFactoryCid - (V2.AllocationFactory_Allocate with - settlement - allocation = Lp.burnReceiptSpec lpRegistrar burnLeg - requestedAt; inputHoldingCids = []; actors = [lpRegistrar]; extraArgs = lpRegistrarExtraArgs) - >>= PM.extractCompleted - - -- base/quote batch (pool.admin): per-slice delivery legs to the holder - -- (receiver = holder's receipt allocations); boundary slices roll - -- forward, the rest drain. - bqResult <- exercise baseQuoteSettleCid V2.SettlementFactory_SettleBatch with - settlement - transferLegs = baseDel.legs ++ quoteDel.legs - allocations = - baseDel.sliceFinalizeds - ++ [Utils.finalAllocation holderBaseReceiptCid] - ++ quoteDel.sliceFinalizeds - ++ [Utils.finalAllocation holderQuoteReceiptCid] - actors = [operator] - extraArgs = poolAdminExtraArgs + let preparation = RemoveLiquidityPreparationArgs with + expectedPoolId; poolCid; poolStateCid; lpPolicyCid + requestCid; acceptanceCid; holder + lpTokensToRedeem; knownTotalLpSupply; minBaseOut; minQuoteOut + baseSliceCids; quoteSliceCids + holderBaseReceiptCid; holderQuoteReceiptCid; holderBurnSenderCid + prepared <- prepareRemoveFrom operator lpRegistrar preparation + burnReceiverCid <- case registrarBurnReceiverCid of + Some cid -> pure cid + None -> do + let allocationPlan = mkRemoveAllocationPlan lpRegistrar requestedAt prepared + exercise lpFactoryCid + (allocationPlan.lpBurnReceiver with extraArgs = lpRegistrarExtraArgs) + >>= PM.extractCompleted + let pool = prepared.removePool + state = prepared.removeState + baseOut = prepared.removeBaseOut + quoteOut = prepared.removeQuoteOut + baseDraw = prepared.removeBaseDraw + quoteDraw = prepared.removeQuoteDraw + baseDel = prepared.removeBaseDelivery + quoteDel = prepared.removeQuoteDelivery + settlementPlan = mkRemoveSettlementPlan + operator prepared preparation.holderBaseReceiptCid + preparation.holderQuoteReceiptCid preparation.holderBurnSenderCid + burnReceiverCid poolAdminExtraArgs lpRegistrarExtraArgs + + bqResult <- exercise baseQuoteSettleCid settlementPlan.baseQuoteBatch -- Roll-forward (Some) entries are the boundary slices, in order: -- base boundary then quote boundary. let someNexts = [ c | Some c <- Utils.nextIterationAllocationCids bqResult ] @@ -548,13 +891,7 @@ template PoolLiquidityRules with (False, False, []) -> pure (None, None) _ -> abort "remove: unexpected boundary roll-forward count" - -- LP-burn batch (pool.lpRegistrar): the holder's LP holding burns. - _ <- exercise lpSettleCid V2.SettlementFactory_SettleBatch with - settlement - transferLegs = [burnLeg] - allocations = [Utils.finalAllocation holderBurnSenderCid, Utils.finalAllocation regBurn] - actors = [operator] - extraArgs = lpRegistrarExtraArgs + _ <- exercise lpSettleCid settlementPlan.lpBatch forA_ (baseDel.drainedSliceCids ++ quoteDel.drainedSliceCids) archive -- Re-wrap each boundary slice: archive the old; create a fresh one for @@ -572,9 +909,10 @@ template PoolLiquidityRules with boundaryBaseSliceCid <- rewrapBoundary BaseSide baseRollCid baseDel.boundary boundaryQuoteSliceCid <- rewrapBoundary QuoteSide quoteRollCid quoteDel.boundary - newPolicyCid <- exercise lpPolicyCid LP.LPTokenPolicy_RecordBurn with amount = lpTokensToRedeem + newPolicyCid <- exercise preparation.lpPolicyCid + LP.LPTokenPolicy_RecordBurn with amount = preparation.lpTokensToRedeem - archiveBinding requestCid acceptanceCid + archiveBinding preparation.requestCid preparation.acceptanceCid -- Per-choice delta conservation. The reserve delta per side -- must equal the net slice-amount change this choice performed: @@ -593,11 +931,11 @@ template PoolLiquidityRules with let newBase = state.reserves.baseAmount - baseOut newQuote = state.reserves.quoteAmount - quoteOut - archive poolStateCid + archive preparation.poolStateCid newStateCid <- create state with status = if newBase == 0.0 && newQuote == 0.0 then PS_Unfunded else PS_Active reserves = PoolReserves with baseAmount = newBase; quoteAmount = newQuote - totalLpSupply = state.totalLpSupply - lpTokensToRedeem + totalLpSupply = state.totalLpSupply - preparation.lpTokensToRedeem pure PoolLiquidityRules_SettleRemoveResult with poolStateCid = newStateCid diff --git a/trading/CantonDex/Dex/PoolRules.daml b/trading/CantonDex/Dex/PoolRules.daml index 39f4670f..072d443e 100644 --- a/trading/CantonDex/Dex/PoolRules.daml +++ b/trading/CantonDex/Dex/PoolRules.daml @@ -129,6 +129,55 @@ prepareSwap operator poolCid swapperAccount inputInstrumentId inputAmount bindin preparedSwapInLeg = swapInLeg preparedOutputDelivery = outputDelivery +prepareBoundSwap + : Party + -> PoolId + -> ContractId Pool + -> ContractId PoolState + -> V2.Account + -> Text + -> Decimal + -> Decimal + -> ContractId PoolSlice + -> [ContractId PoolSlice] + -> Optional SwapQuoteBinding + -> Update PreparedSwap +prepareBoundSwap + operator expectedPoolId poolCid poolStateCid swapperAccount + inputInstrumentId inputAmount minOutputAmount inputSliceCid outputSliceCids + quoteBinding = do + let binding = fromSomeNote "PoolRules: quoteBinding is required" quoteBinding + assertMsg "Swap quote pool id mismatch" (expectedPoolId == binding.expectedPoolId) + assertMsg "Swap quote state mismatch" (poolStateCid == binding.poolStateCid) + assertMsg "Swap quote input slice mismatch" (inputSliceCid == binding.inputSliceCid) + assertMsg "Swap quote output slices mismatch" (outputSliceCids == binding.outputSliceCids) + assertMsg "Swap quote slippage minimum mismatch" (minOutputAmount == binding.minOutputAmount) + prepareSwap operator poolCid swapperAccount inputInstrumentId inputAmount binding + +swapSettlementArguments + : Party + -> ContractId Pool + -> ContractId V2.Allocation + -> PreparedSwap + -> ExtraArgs + -> V2.SettlementFactory_SettleBatch +swapSettlementArguments operator poolCid swapperAllocationCid prepared extraArgs = + let poolAccount = Utils.basicAccount operator + inputSlice = prepared.preparedInputSlice + inputInstrumentId = prepared.preparedSwapInLeg.instrumentId + inputAmount = prepared.preparedSwapInLeg.amount + swapperFinalized = Utils.finalAllocation swapperAllocationCid + inputFinalized = Utils.mkFinalizedAllocation inputSlice.allocationCid + (Utils.legsToSides poolAccount [prepared.preparedSwapInLeg]) + (Some (TextMap.fromList [(inputInstrumentId, inputSlice.amount + inputAmount)])) + in V2.SettlementFactory_SettleBatch with + settlement = poolSettlement poolCid operator + transferLegs = prepared.preparedSwapInLeg :: prepared.preparedOutputDelivery.legs + allocations = + swapperFinalized :: inputFinalized :: prepared.preparedOutputDelivery.sliceFinalizeds + actors = [operator] + extraArgs + -- Result of PoolRules_ReconcileState: the audited slice totals. data PoolRules_ReconcileResult = PoolRules_ReconcileResult with sliceCount : Int @@ -173,6 +222,32 @@ template PoolRules with False pure PoolRules_RequestSwapResult with settlement; allocationSpec; quoteBinding + -- Return the exact SettleBatch argument used by `PoolRules_Swap`. The + -- backend sends this value to the instrument registry's canonical + -- settlement-factory endpoint, then supplies the returned factory and + -- choice context to the real swap. + nonconsuming choice PoolRules_PreviewSwapSettlement : V2.SettlementFactory_SettleBatch + with + expectedPoolId : PoolId + poolCid : ContractId Pool + poolStateCid : ContractId PoolState + swapperAccount : V2.Account + inputInstrumentId : Text + inputAmount : Decimal + minOutputAmount : Decimal + swapperAllocationCid : ContractId V2.Allocation + inputSliceCid : ContractId PoolSlice + outputSliceCids : [ContractId PoolSlice] + quoteBinding : Optional SwapQuoteBinding + controller operator + do + prepared <- prepareBoundSwap + operator expectedPoolId poolCid poolStateCid swapperAccount + inputInstrumentId inputAmount minOutputAmount inputSliceCid + outputSliceCids quoteBinding + pure $ swapSettlementArguments + operator poolCid swapperAllocationCid prepared Utils.emptyExtraArgs + -- Constant-product swap (multi-slice): prices against global reserves, -- sources amountOut from an ordered output-slice prefix (draining full -- ones, re-allocating the boundary leftover); the input slice grows. @@ -193,14 +268,10 @@ template PoolRules with quoteBinding : Optional SwapQuoteBinding controller operator do - let binding = fromSomeNote "PoolRules_Swap: quoteBinding is required" quoteBinding - assertMsg "Swap quote pool id mismatch" (expectedPoolId == binding.expectedPoolId) - assertMsg "Swap quote state mismatch" (poolStateCid == binding.poolStateCid) - assertMsg "Swap quote input slice mismatch" (inputSliceCid == binding.inputSliceCid) - assertMsg "Swap quote output slices mismatch" (outputSliceCids == binding.outputSliceCids) - assertMsg "Swap quote slippage minimum mismatch" (minOutputAmount == binding.minOutputAmount) - prepared <- prepareSwap - operator poolCid swapperAccount inputInstrumentId inputAmount binding + prepared <- prepareBoundSwap + operator expectedPoolId poolCid poolStateCid swapperAccount + inputInstrumentId inputAmount minOutputAmount inputSliceCid + outputSliceCids quoteBinding let pool = prepared.preparedPool state = prepared.preparedState inputSlice = prepared.preparedInputSlice @@ -209,22 +280,12 @@ template PoolRules with swapInLeg = prepared.preparedSwapInLeg outDel = prepared.preparedOutputDelivery draw = prepared.preparedDraw - poolAccount = Utils.basicAccount operator - settlement = poolSettlement poolCid operator + settlementArgs = swapSettlementArguments + operator poolCid swapperAllocationCid prepared extraArgs -- The trader authored every leg side, so settlement cannot add or -- alter trader authority here. Pool allocations still roll forward. - let swapperFinalized = Utils.finalAllocation swapperAllocationCid - inputFinalized = Utils.mkFinalizedAllocation inputSlice.allocationCid - (Utils.legsToSides poolAccount [swapInLeg]) - (Some (TextMap.fromList [(inputInstrumentId, inputSlice.amount + inputAmount)])) - - settleResult <- exercise factoryCid V2.SettlementFactory_SettleBatch with - settlement - transferLegs = swapInLeg :: outDel.legs - allocations = swapperFinalized :: inputFinalized :: outDel.sliceFinalizeds - actors = [operator] - extraArgs + settleResult <- exercise factoryCid settlementArgs let nextIterCids = Utils.nextIterationAllocationCids settleResult -- SettleBatch order: [swapper, input, fully…, boundary?]. Guard the diff --git a/trading/CantonDex/Dex/PoolSlice.daml b/trading/CantonDex/Dex/PoolSlice.daml index b34679c7..3dd3c44d 100644 --- a/trading/CantonDex/Dex/PoolSlice.daml +++ b/trading/CantonDex/Dex/PoolSlice.daml @@ -30,3 +30,5 @@ template PoolSlice with -- choice that writes the slice. where signatory operator + + ensure poolId /= "" && amount > 0.0 diff --git a/trading/CantonDex/Dex/PoolState.daml b/trading/CantonDex/Dex/PoolState.daml index 141bf6cb..f5dbc2d8 100644 --- a/trading/CantonDex/Dex/PoolState.daml +++ b/trading/CantonDex/Dex/PoolState.daml @@ -48,6 +48,12 @@ template PoolState with signatory operator observer (lpRegistrar :: publicReaders) + ensure + poolId /= "" + && reserves.baseAmount >= 0.0 + && reserves.quoteAmount >= 0.0 + && totalLpSupply >= 0.0 + -- The lpRegistrar records post-mint/burn LP supply back onto the pool -- state. choice PoolState_RecordLPSupply : ContractId PoolState diff --git a/trading/CantonDex/Dex/Rfq.daml b/trading/CantonDex/Dex/Rfq.daml index b6254c99..6f13d4ae 100644 --- a/trading/CantonDex/Dex/Rfq.daml +++ b/trading/CantonDex/Dex/Rfq.daml @@ -64,7 +64,15 @@ template Rfq with signatory trader observer operator :: whitelist - ensure size > 0.0 + ensure + rfqId /= "" + && validPair pair + && size > 0.0 + && createdAt < expiresAt + && whitelist /= [] + && uniqueParties whitelist + && trader `notElem` whitelist + && operator `notElem` whitelist choice Rfq_Cancel : () controller trader @@ -98,6 +106,9 @@ template Rfq with controller trader, operator do assertMsg "RFQ must not be expired" (currentTime < expiresAt) + assertMsg "Signature must not be empty" (signature /= "") + assertMsg "Considered quotes must be unique" + (length consideredQuoteCids == length (dedupContractIds consideredQuoteCids)) consideredPairs <- forA consideredQuoteCids (\cid -> do q <- fetch cid; pure (cid, q)) accepted <- fetch acceptedQuoteCid @@ -111,6 +122,12 @@ template Rfq with forA_ consideredPairs $ \(_, q) -> do assertMsg "All considered quotes must reference this RFQ" $ q.rfqId == rfqId + assertMsg "Considered quote names a different trader" $ + q.trader == trader + assertMsg "Considered quote names a different operator" $ + q.operator == operator + assertMsg "Considered quote is from a dealer outside the RFQ whitelist" $ + q.dealer `elem` whitelist assertMsg "Considered quote must still be valid" $ currentTime < q.expiresAt @@ -217,12 +234,16 @@ template RfqQuote with -- ^ Deadline after which this quote is no longer valid. postedAt : Time tier : DealerTier - -- ^ Operator-assigned tier at post time; operator observes the quote. + -- ^ Dealer-declared policy tier. The operator observes it and endorses + -- the considered quote set when jointly authorizing Rfq_Accept. where signatory dealer observer trader, operator - ensure price > 0.0 + ensure + rfqId /= "" + && price > 0.0 + && postedAt < expiresAt choice RfqQuote_Withdraw : () controller dealer @@ -263,3 +284,15 @@ splitPair : Text -> (Text, Text) splitPair pair = case Text.splitOn "/" pair of [b, q] -> (b, q) _ -> ("", "") + +validPair : Text -> Bool +validPair pair = case splitPair pair of + (base, quote) -> base /= "" && quote /= "" && base /= quote + +uniqueParties : [Party] -> Bool +uniqueParties parties = + length parties + == length (foldl (\seen p -> if p `elem` seen then seen else p :: seen) [] parties) + +dedupContractIds : [ContractId RfqQuote] -> [ContractId RfqQuote] +dedupContractIds = foldl (\seen cid -> if cid `elem` seen then seen else cid :: seen) [] diff --git a/trading/CantonDex/Lp/Policy.daml b/trading/CantonDex/Lp/Policy.daml index 1fac0aad..0c25165c 100644 --- a/trading/CantonDex/Lp/Policy.daml +++ b/trading/CantonDex/Lp/Policy.daml @@ -21,7 +21,10 @@ template LPTokenPolicy with signatory lpRegistrar observer operator - ensure lpInstrumentId.admin == lpRegistrar + ensure + lpInstrumentId.admin == lpRegistrar + && lpInstrumentId.id /= "" + && totalSupply >= 0.0 choice LPTokenPolicy_RecordMint : ContractId LPTokenPolicy with @@ -29,6 +32,7 @@ template LPTokenPolicy with controller lpRegistrar do assertMsg "Policy must be active" active + assertMsg "Mint amount must be positive" (amount > 0.0) create this with totalSupply = totalSupply + amount choice LPTokenPolicy_RecordBurn : ContractId LPTokenPolicy @@ -37,6 +41,7 @@ template LPTokenPolicy with controller lpRegistrar do assertMsg "Policy must be active" active + assertMsg "Burn amount must be positive" (amount > 0.0) assertMsg "Cannot burn more than supply" (amount <= totalSupply) create this with totalSupply = totalSupply - amount diff --git a/trading/CantonDex/Trading/Utils.daml b/trading/CantonDex/Trading/Utils.daml index 717c9ef5..2b813df1 100644 --- a/trading/CantonDex/Trading/Utils.daml +++ b/trading/CantonDex/Trading/Utils.daml @@ -8,7 +8,14 @@ import DA.TextMap qualified as TextMap import Splice.Api.Token.AllocationV2 qualified as V2 import Splice.Api.Token.HoldingV2 qualified as HoldingV2 -import Splice.Api.Token.MetadataV1 (emptyMetadata) +import Splice.Api.Token.MetadataV1 (ExtraArgs (..), emptyChoiceContext, emptyMetadata) + +-- | Empty context used only when constructing an off-ledger discovery preview. +-- The registry response replaces it before the corresponding choice executes. +emptyExtraArgs : ExtraArgs +emptyExtraArgs = ExtraArgs with + context = emptyChoiceContext + meta = emptyMetadata type Funding = TextMap.TextMap Decimal diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 29acca9d..20d0680c 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -13,6 +13,10 @@ export default defineConfig({ integrations: [ starlight({ title: 'Canton DEX', + // A repo-base-aware page lives at src/pages/404.astro. Keeping it outside + // the docs collection avoids both a missing-entry warning and a duplicate + // `/404` route during static generation. + disable404Route: true, description: 'A full-stack Token Standard V2 (CIP-0112) reference DEX for the Canton Network.', customCss: ['./src/styles/custom.css'], @@ -43,9 +47,46 @@ export default defineConfig({ }, ], sidebar: [ - { label: 'Start here', items: [{ label: 'Getting Started', slug: 'getting-started' }] }, - { label: 'Concepts', items: [{ autogenerate: { directory: 'concepts' } }] }, - { label: 'Guides', items: [{ autogenerate: { directory: 'guides' } }] }, + { + label: 'Newcomer learning path', + items: [ + { label: 'Canton & Daml Primer', slug: 'concepts/canton-daml-primer' }, + { label: 'Overview', slug: 'concepts/overview' }, + { label: 'Getting Started', slug: 'getting-started' }, + { label: 'AMM-first Walkthrough', slug: 'tutorials/amm-first-walkthrough' }, + { label: '15-minute Design Tour', slug: 'concepts/design-tour' }, + { label: 'Architecture', slug: 'concepts/architecture' }, + { label: 'Workflow Design', slug: 'concepts/workflows' }, + { label: 'Make Your First AMM Change', slug: 'tutorials/make-your-first-amm-change' }, + { label: 'Builder Guide', slug: 'guides/builder-guide' }, + ], + }, + { + label: 'Concepts', + items: [ + { label: 'Liquidity & Custody', slug: 'concepts/liquidity-and-custody' }, + { label: 'LP Tokens', slug: 'concepts/lp-tokens' }, + { label: 'Pricing', slug: 'concepts/pricing' }, + { label: 'Glossary', slug: 'concepts/glossary' }, + { label: 'Non-goals', slug: 'concepts/non-goals' }, + ], + }, + { + label: 'Guides', + items: [ + { label: 'Local Canton (DPM sandbox)', slug: 'guides/localnet' }, + { label: 'Using the dApp', slug: 'guides/using-the-dapp' }, + { label: 'Add a Trading Pair', slug: 'guides/add-a-trading-pair' }, + { label: 'Add an LP or Instrument', slug: 'guides/add-lp-or-instrument' }, + { label: 'Choice Context', slug: 'guides/choice-context' }, + { label: 'Registry Integration', slug: 'guides/registry-integration' }, + { label: 'Deployment', slug: 'guides/deployment' }, + { label: 'Run on a Testnet', slug: 'guides/run-on-testnet' }, + { label: 'Operator Guide', slug: 'guides/operator-guide' }, + { label: 'Operator Runbook', slug: 'guides/operator-runbook' }, + { label: 'Validator Test Plan', slug: 'guides/validator-test-plan' }, + ], + }, { label: 'Reference', items: [{ autogenerate: { directory: 'reference' } }] }, ], }), diff --git a/website/package-lock.json b/website/package-lock.json index c90d0456..1be8e134 100644 --- a/website/package-lock.json +++ b/website/package-lock.json @@ -1,42 +1,45 @@ { "name": "website", - "version": "0.0.1", + "version": "0.6.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "website", - "version": "0.0.1", + "version": "0.6.0", "dependencies": { - "@astrojs/starlight": "^0.41.2", - "astro": "^7.0.2", - "sharp": "^0.34.5" + "@astrojs/starlight": "^0.41.9", + "astro": "^7.2.8", + "sharp": "^0.35.4" + }, + "engines": { + "node": ">=22.12.0" } }, "node_modules/@astrojs/compiler-binding": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.3.0.tgz", - "integrity": "sha512-zlsOT5COD9hRwplJCgQhS21unxON5AKirf0vgt1ijXwuseYIaZdm2ZOpF8fsz+DY9EyXx+I/ukxtg7uoBep68A==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.4.0.tgz", + "integrity": "sha512-x2RjDUuWfwLNtc3mjAdSRInwqh/rqbLar9cm/5FOMbHvmYZB7yfKewzSclAxWjIZsypJDXv1lhaP2WG+P8TK3g==", "license": "MIT", "engines": { "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@astrojs/compiler-binding-darwin-arm64": "0.3.0", - "@astrojs/compiler-binding-darwin-x64": "0.3.0", - "@astrojs/compiler-binding-linux-arm64-gnu": "0.3.0", - "@astrojs/compiler-binding-linux-arm64-musl": "0.3.0", - "@astrojs/compiler-binding-linux-x64-gnu": "0.3.0", - "@astrojs/compiler-binding-linux-x64-musl": "0.3.0", - "@astrojs/compiler-binding-wasm32-wasi": "0.3.0", - "@astrojs/compiler-binding-win32-arm64-msvc": "0.3.0", - "@astrojs/compiler-binding-win32-x64-msvc": "0.3.0" + "@astrojs/compiler-binding-darwin-arm64": "0.4.0", + "@astrojs/compiler-binding-darwin-x64": "0.4.0", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.4.0", + "@astrojs/compiler-binding-linux-arm64-musl": "0.4.0", + "@astrojs/compiler-binding-linux-x64-gnu": "0.4.0", + "@astrojs/compiler-binding-linux-x64-musl": "0.4.0", + "@astrojs/compiler-binding-wasm32-wasi": "0.4.0", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.4.0", + "@astrojs/compiler-binding-win32-x64-msvc": "0.4.0" } }, "node_modules/@astrojs/compiler-binding-darwin-arm64": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.3.0.tgz", - "integrity": "sha512-3n0uu+uJpnCq8b4JFi3uGDsIisAvHctxSmH+cIO9Gbei1H1Y1QXaYboXyiWJugUmprr3OEYP7+LdodzpVFzLMQ==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.4.0.tgz", + "integrity": "sha512-ZVUwHundaQyFNjE6uoa0usaC0WOCitDCLS/4mdb4rOiJXwVUuKJBMxI5WMzXLWmamsXtK/Z//ifLXvV5Yeh4Hw==", "cpu": [ "arm64" ], @@ -50,9 +53,9 @@ } }, "node_modules/@astrojs/compiler-binding-darwin-x64": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.3.0.tgz", - "integrity": "sha512-scxNGKjOBydMo1QR4LtK0FMgh7ubQomJDv953nz2msQFkPKke/0FpPv/cQM0T/kuZdReZQFU8Oz3iOrP/6WHEg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.4.0.tgz", + "integrity": "sha512-FI6G8AY8u6fR1SI/QRR5yGMwtvZwP34CDmZpZ5HwJGa50UM1VISTLhqkhV4a476pmgd25X1Aur2dqw6hUnrlKA==", "cpu": [ "x64" ], @@ -66,9 +69,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.3.0.tgz", - "integrity": "sha512-NZrWLolVUANmrnl0zrFK/Sx5Sock1gEUT49ALfMTTCA5Ya2ec/BoJXMIg4KgE+wZcrdXJ8e+WyEhM7YLk/FJkA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.4.0.tgz", + "integrity": "sha512-lB9gLFJK7m82EnjaU8nlRBEfcwGNeHidW3sSjODTUjMNaoewVuUz9fwwdY5M4jiSXIqWLH3yl6TX8FTDKA74Sw==", "cpu": [ "arm64" ], @@ -82,9 +85,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.3.0.tgz", - "integrity": "sha512-PjwRmKgMFDsFhg82g0poXlIY8Qn3fMA3hXjaR0coJWJzTJsRH9ATU0j2ocigjtU1h3vL/yR7yLUxGj/lTCq73g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.4.0.tgz", + "integrity": "sha512-HPbvWqbxFxyaoQJhLxCaSjtYBx9KBo7JGVzEFZCmMl968a2PsSH0UfiODYgYPXofTOIsIH2aoCcrHXML0IA3ig==", "cpu": [ "arm64" ], @@ -98,9 +101,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.3.0.tgz", - "integrity": "sha512-Dr69VJYlnSfyL8gzELW6S4mE41P7TDPn1IKjwMnjdZ7+dxgJI50oMLFSk1LVe26bHmWB3ktuh8fDVK1THI9e9A==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.4.0.tgz", + "integrity": "sha512-tQKolMxoJ/+0AmLWm1PmJ/i+z3i10ZU1bNuVjEDulCf48azEMtUNjTZgHJ5MPtpYRNc7dlETr8QujUfduzoC7Q==", "cpu": [ "x64" ], @@ -114,9 +117,9 @@ } }, "node_modules/@astrojs/compiler-binding-linux-x64-musl": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.3.0.tgz", - "integrity": "sha512-AEt+bRw8PfImCcyRH1lpXVB8CdmQ1K/wPo5u99iec4/U/XdNvQZ715YVuNzIJpbJXelgQeZ5H2+Ea7XwRyWY5g==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.4.0.tgz", + "integrity": "sha512-5v5YymudsxMHp3NBLCS8BUlu5CRqeLtWD9cKS/4nIhIEHCbpz9okmVV6I0HWqmBAPhWYcDa3vw/vltYPrOQCTA==", "cpu": [ "x64" ], @@ -130,25 +133,25 @@ } }, "node_modules/@astrojs/compiler-binding-wasm32-wasi": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.3.0.tgz", - "integrity": "sha512-U80tA1j8V6LjhiTZzVCtG4E8hrNVVNXDGV5fCgJ94q8FU9CPH+XwdDDhLzBybfWhKfyItXmQiZNRPTiPCYTpVg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.4.0.tgz", + "integrity": "sha512-m/phuH3x3PREvv1OnkM44NoPh4MatUadix1fB1u5SvMLCyDTUZykDJbKnWf1cjnYmHdlB8HcjTjl6JrCqAIXcw==", "cpu": [ "wasm32" ], "license": "MIT", "optional": true, "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.6" + "@napi-rs/wasm-runtime": "^1.2.2" }, "engines": { "node": ">=14.0.0" } }, "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.3.0.tgz", - "integrity": "sha512-CpY1RII2r1XMpOUVD1VR/F2wtuRsiOCkFULS10Khyj8/DFZMtxVuUCAWGw+CW2Ka0h6eP3Xc1CA+glFlvXMPxA==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.4.0.tgz", + "integrity": "sha512-B9zYf3okEY83kM8gydlpH2BHP00w4ifxPqlYlWrgTwuD6wnkrJDCwBlgy1q31cERjCJRXN1lrE2VmkLvFjv/6g==", "cpu": [ "arm64" ], @@ -162,9 +165,9 @@ } }, "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.3.0.tgz", - "integrity": "sha512-qmFbs769oeeGrRebAnCW7aBk8m71vf85W/dX/jddfx5Z06/w0wf7TZCfJPOX1Fld2t+4N+iXzfGEJG+zJQ+bzg==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.4.0.tgz", + "integrity": "sha512-zB0Nrv0dGc0zZWPGDRmmETTPhDRqyZjAjk+gWMlVrJX5U89obpB3VUUE1ZiHxOCN5LQojeLK6O8L/dnoHolvNQ==", "cpu": [ "x64" ], @@ -178,26 +181,26 @@ } }, "node_modules/@astrojs/compiler-rs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.3.0.tgz", - "integrity": "sha512-J2qEVHtIDjEM9TxwmwuebOGmZNwhKu/dR7P7qBpnJKGmBBX0vdweQ/4cEXhj8fBbWVUB5V12xWChri3CgKNULQ==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.4.0.tgz", + "integrity": "sha512-koVikeon1kreEy+/JzLQRy3vzHHQVOjycs4degg4vFufKApZOwMZvSSAEztYNhmcQVfNVsVZZI4cEge3cexAbQ==", "license": "MIT", "dependencies": { - "@astrojs/compiler-binding": "0.3.0" + "@astrojs/compiler-binding": "0.4.0" }, "engines": { "node": ">=22.12.0" } }, "node_modules/@astrojs/internal-helpers": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.0.tgz", - "integrity": "sha512-Ry2R3VPeIN4uPCSA4xQc+e+vsJXkalKpEbDc07hV+a/o5Bs2N/s/uDcPJH/05L19DKh9tAy7e6JM3YZ6Cxfezw==", + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.10.4.tgz", + "integrity": "sha512-nozZSy/mKYLqe4YrqbKtdOszedAfXYCtw3wZ0d+CAjz4GqQ4L9rl1ltIL5BlgwmYVinJg/RZ0MgGuWOdlyRZlA==", "license": "MIT", "dependencies": { "@types/hast": "^3.0.4", "@types/mdast": "^4.0.4", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "picomatch": "^4.0.4", "retext-smartypants": "^6.2.0", "shiki": "^4.0.2", @@ -206,12 +209,12 @@ } }, "node_modules/@astrojs/markdown-remark": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.0.tgz", - "integrity": "sha512-+YxmVQu1Bd+MFfSzjq1rOJvD9+nIOJzz5YIIhdIH01RrxRkKbyKoEgyIqP3yv51MhzMDgd79QaPv+kCVPT8vHw==", + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.2.4.tgz", + "integrity": "sha512-MvspGMynWKAjTe4/lTUdmBPHIFKNVLTCF6UlyWGogTGzNrTvjD+D4n48k7h8swxsEPKHK2TwxkZO7uoaCv1Pow==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.10.0", + "@astrojs/internal-helpers": "0.10.4", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", "hast-util-from-html": "^2.0.3", @@ -231,25 +234,170 @@ } }, "node_modules/@astrojs/markdown-satteri": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.3.2.tgz", - "integrity": "sha512-feXuUPy41gVfeM7EHT1ciUim8ozGr+YHXab9uUBc1Hk8y60DQosO8ldL+AoPXnCAoGj1OChwHfvXmmJ6XVnY9A==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.3.8.tgz", + "integrity": "sha512-n8ItpFTCmlDsVR5+rwDmehSf+jFCYLWmZiisZNGuF7xILqYhsVeBfcha4qgS2Seq3fAc9Tm58ZVrvqFQ6RQgRQ==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.10.0", + "@astrojs/internal-helpers": "0.10.4", "@astrojs/prism": "4.0.2", "github-slugger": "^2.0.0", - "satteri": "^0.9.1" + "satteri": "^0.10.3" + } + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz", + "integrity": "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-darwin-x64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.10.5.tgz", + "integrity": "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz", + "integrity": "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.10.5.tgz", + "integrity": "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.10.5.tgz", + "integrity": "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.10.5.tgz", + "integrity": "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.10.5.tgz", + "integrity": "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.10.5.tgz", + "integrity": "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.10.5.tgz", + "integrity": "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@astrojs/markdown-satteri/node_modules/satteri": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.10.5.tgz", + "integrity": "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" + }, + "optionalDependencies": { + "@bruits/satteri-darwin-arm64": "0.10.5", + "@bruits/satteri-darwin-x64": "0.10.5", + "@bruits/satteri-linux-arm64-gnu": "0.10.5", + "@bruits/satteri-linux-arm64-musl": "0.10.5", + "@bruits/satteri-linux-x64-gnu": "0.10.5", + "@bruits/satteri-linux-x64-musl": "0.10.5", + "@bruits/satteri-wasm32-wasi": "0.10.5", + "@bruits/satteri-win32-arm64-msvc": "0.10.5", + "@bruits/satteri-win32-x64-msvc": "0.10.5" } }, "node_modules/@astrojs/mdx": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-7.0.1.tgz", - "integrity": "sha512-N8qZnqhTbydVuXlHhyUocNIccbuqsYIagSGbmzcOsJrOJZPT9Q/Z1jIa7kwFk/rqIjsNpPSOeahNoGBiR+dZoA==", + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-7.0.8.tgz", + "integrity": "sha512-RNuwq2ccTSi7NX9YqlR0noaWoVdOrZuJvdfWXotAzdhMVB0b0zEMLnNU+xar7le0GwTMlTObD/QAu8jeHA/3nA==", "license": "MIT", "dependencies": { - "@astrojs/internal-helpers": "0.10.0", - "@astrojs/markdown-remark": "7.2.0", + "@astrojs/internal-helpers": "0.10.4", + "@astrojs/markdown-remark": "7.2.4", "@mdx-js/mdx": "^3.1.1", "acorn": "^8.16.0", "es-module-lexer": "^2.0.0", @@ -300,14 +448,14 @@ } }, "node_modules/@astrojs/starlight": { - "version": "0.41.2", - "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.41.2.tgz", - "integrity": "sha512-1h9AhFW5uWgqEoTqOVLMnXvawa0TaqLSr14UyTbnArQL6W0TolTfMWpG5hrvbw+NO6wwdSOFz5iwhVtkGUBtsw==", + "version": "0.41.9", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.41.9.tgz", + "integrity": "sha512-c76+isiSQzJRRnkT7JqUqqM/2Rg+fKhtA08zRe06EJRgqsW4WEJ+MtySIOjXc59oO1aGbVZRbxiBfYp72zsVpw==", "license": "MIT", "dependencies": { - "@astrojs/markdown-satteri": "^0.3.2", - "@astrojs/mdx": "^7.0.0", - "@astrojs/sitemap": "^3.7.2", + "@astrojs/markdown-satteri": "^0.3.5", + "@astrojs/mdx": "^7.0.5", + "@astrojs/sitemap": "^3.7.3", "@pagefind/default-ui": "^1.3.0", "@types/hast": "^3.0.4", "@types/js-yaml": "^4.0.9", @@ -346,16 +494,15 @@ } }, "node_modules/@astrojs/telemetry": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.2.tgz", - "integrity": "sha512-j8DNruA8ors99Al39RYZPJK4DC1bKkoNm93mAMuBhY9TCNC4R8n1q7ovFnJ5qhGh5Lsh7pa1gpQVpYpsJPeTHQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.3.tgz", + "integrity": "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==", "license": "MIT", "dependencies": { "ci-info": "^4.4.0", "dset": "^3.1.4", "is-docker": "^4.0.0", - "is-wsl": "^3.1.1", - "which-pm-runs": "^1.1.0" + "package-manager-detector": "^1.6.0" }, "engines": { "node": "18.20.8 || ^20.3.0 || >=22.0.0" @@ -1071,9 +1218,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", "cpu": [ "arm64" ], @@ -1083,19 +1230,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ "x64" ], @@ -1105,19 +1252,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ "arm64" ], @@ -1131,9 +1297,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], @@ -1147,9 +1313,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], @@ -1163,9 +1329,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], @@ -1179,9 +1345,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", "cpu": [ "ppc64" ], @@ -1195,9 +1361,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", "cpu": [ "riscv64" ], @@ -1211,9 +1377,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ "s390x" ], @@ -1227,9 +1393,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], @@ -1243,9 +1409,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], @@ -1259,9 +1425,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", "cpu": [ "x64" ], @@ -1275,9 +1441,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", "cpu": [ "arm" ], @@ -1287,19 +1453,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ "arm64" ], @@ -1309,19 +1475,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", "cpu": [ "ppc64" ], @@ -1331,19 +1497,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.3" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", "cpu": [ "riscv64" ], @@ -1353,19 +1519,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", "cpu": [ "s390x" ], @@ -1375,19 +1541,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", "cpu": [ "x64" ], @@ -1397,19 +1563,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", "cpu": [ "arm64" ], @@ -1419,19 +1585,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", "cpu": [ "x64" ], @@ -1441,38 +1607,64 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-wasm32/node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.4" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", "cpu": [ "arm64" ], @@ -1482,16 +1674,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ "ia32" ], @@ -1501,16 +1693,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], @@ -1520,7 +1712,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -1570,21 +1762,24 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", "license": "MIT", "optional": true, "dependencies": { "@tybys/wasm-util": "^0.10.3" }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, "node_modules/@oslojs/encoding": { @@ -1947,34 +2142,6 @@ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", "license": "MIT" }, - "node_modules/@rollup/pluginutils": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", - "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" - }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } - } - }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", - "license": "MIT" - }, "node_modules/@shikijs/core": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.3.0.tgz", @@ -2110,9 +2277,9 @@ } }, "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -2185,9 +2352,9 @@ "license": "ISC" }, "node_modules/acorn": { - "version": "8.17.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", - "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -2283,43 +2450,43 @@ } }, "node_modules/astro": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/astro/-/astro-7.0.5.tgz", - "integrity": "sha512-KR/zBBgU6I+F5vDoTsuTbXbBmI565CToDOgPr0pPiL2Hgrvx4CV8Cg2wtT6VqqyOS7BIfBkdagKD+40SojGl7w==", + "version": "7.2.8", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.2.8.tgz", + "integrity": "sha512-19nfgzl1IHNYzIfamUIr4dYSQcovWfMO5JGlN7Ahlg6Q6R+nzSPWAh+VO/mSg0hhcea35JHY0wL5ADZUi0WiYQ==", "license": "MIT", "dependencies": { - "@astrojs/compiler-rs": "^0.3.0", - "@astrojs/internal-helpers": "0.10.0", - "@astrojs/markdown-satteri": "0.3.2", - "@astrojs/telemetry": "3.3.2", + "@astrojs/compiler-rs": "^0.4.0", + "@astrojs/internal-helpers": "0.10.4", + "@astrojs/markdown-satteri": "0.3.8", + "@astrojs/telemetry": "3.3.3", "@capsizecss/unpack": "^4.0.0", "@clack/prompts": "^1.1.0", "@oslojs/encoding": "^1.1.0", - "@rollup/pluginutils": "^5.3.0", "am-i-vibing": "^0.4.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", - "cookie": "^1.1.1", + "cookie": "^2.0.1", "devalue": "^5.8.1", - "diff": "^8.0.3", + "diff": "^9.0.0", "dset": "^3.1.4", "es-module-lexer": "^2.0.0", "esbuild": "^0.28.0", + "find-proc": "0.1.0", "flattie": "^1.1.1", "fontace": "~0.4.1", "get-tsconfig": "5.0.0-beta.4", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", - "js-yaml": "^4.1.1", + "js-yaml": "^4.3.0", "jsonc-parser": "^3.3.1", - "magic-string": "^0.30.21", + "magic-string": "^1.0.0", "magicast": "^0.5.2", "mrmime": "^2.0.1", - "neotraverse": "^0.6.18", + "neotraverse": "^1.0.1", "obug": "^2.1.1", "p-limit": "^7.3.0", "p-queue": "^9.1.0", @@ -2334,7 +2501,7 @@ "tinyexec": "^1.0.4", "tinyglobby": "^0.2.15", "ultrahtml": "^1.6.0", - "unifont": "~0.7.4", + "unifont": "~0.7.5", "unstorage": "^1.17.5", "vite": "^8.0.13", "vitefu": "^1.1.2", @@ -2355,10 +2522,10 @@ "url": "https://opencollective.com/astrodotbuild" }, "optionalDependencies": { - "sharp": "^0.34.0 || ^0.35.0" + "sharp": "^0.35.4" }, "peerDependencies": { - "@astrojs/markdown-remark": "7.2.0" + "@astrojs/markdown-remark": "7.2.4" }, "peerDependenciesMeta": { "@astrojs/markdown-remark": { @@ -2379,6 +2546,15 @@ "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" } }, + "node_modules/astro/node_modules/magic-string": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.2.3.tgz", + "integrity": "sha512-Bpb0W2TbLKOZ7vJnOUnVRGq3WL2p+ISV29M6hYPL1AFCpyKZpdr5ytiXoTSSxRVhg8YW7f65+6gbG8WG6PCa/g==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -2557,12 +2733,12 @@ } }, "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "type": "opencollective", @@ -2585,16 +2761,16 @@ } }, "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" }, "funding": { "url": "https://github.com/sponsors/fb55" @@ -2630,9 +2806,9 @@ } }, "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -2766,9 +2942,9 @@ } }, "node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -3006,9 +3182,9 @@ } }, "node_modules/estree-util-scope": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", - "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.1.tgz", + "integrity": "sha512-B0np3dcdxqILX5e9nEi5/Fr4K7gL4oYFVPV1zRa2e9wRCbQoZZNWOZFYyoInvXUPJXBXjss+QXlWLJChDEHDkA==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -3122,6 +3298,15 @@ } } }, + "node_modules/find-proc": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/find-proc/-/find-proc-0.1.0.tgz", + "integrity": "sha512-OaOpEYv2PiQ7SQ5LIrl+deA1XaWcxEjnpM6VuWXTUvn+teIXxeFTLDmu18/zDQpFmHN4o3oDBX+BT0AGwEhemg==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/flattie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", @@ -3703,39 +3888,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-inside-container/node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -3748,25 +3900,10 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/js-yaml": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", - "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "funding": [ { "type": "github", @@ -5191,9 +5328,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.15", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", - "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", "funding": [ { "type": "github", @@ -5209,9 +5346,9 @@ } }, "node_modules/neotraverse": { - "version": "0.6.18", - "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", - "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", + "integrity": "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==", "license": "MIT", "engines": { "node": ">= 10" @@ -5288,9 +5425,9 @@ } }, "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", "license": "MIT" }, "node_modules/oniguruma-parser": { @@ -5457,9 +5594,9 @@ } }, "node_modules/postcss": { - "version": "8.5.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", - "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", "funding": [ { "type": "opencollective", @@ -5476,7 +5613,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.12", + "nanoid": "^3.3.17", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5996,9 +6133,9 @@ } }, "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" @@ -6017,47 +6154,52 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shiki": { @@ -6183,18 +6325,18 @@ } }, "node_modules/svgo": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", - "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.1.0.tgz", + "integrity": "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==", "license": "MIT", "dependencies": { "commander": "^11.1.0", - "css-select": "^5.1.0", + "css-select": "^6.0.0", "css-tree": "^3.0.1", - "css-what": "^6.1.0", + "css-what": "^7.0.0", "csso": "^5.0.5", "picocolors": "^1.1.1", - "sax": "^1.5.0" + "sax": "1.6.1" }, "bin": { "svgo": "bin/svgo.js" @@ -6292,6 +6434,15 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/undici-types": { "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", @@ -6318,14 +6469,14 @@ } }, "node_modules/unifont": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", - "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.5.tgz", + "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==", "license": "MIT", "dependencies": { "css-tree": "^3.1.0", - "ofetch": "^1.5.1", - "ohash": "^2.0.11" + "ohash": "^2.0.11", + "undici": "^8.0.0" } }, "node_modules/unist-util-find-after": { @@ -6726,15 +6877,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/which-pm-runs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", - "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", diff --git a/website/package.json b/website/package.json index 8693ca27..03b43917 100644 --- a/website/package.json +++ b/website/package.json @@ -14,8 +14,8 @@ "astro": "astro" }, "dependencies": { - "@astrojs/starlight": "^0.41.2", - "astro": "^7.0.2", - "sharp": "^0.34.5" + "@astrojs/starlight": "^0.41.9", + "astro": "^7.2.8", + "sharp": "^0.35.4" } -} \ No newline at end of file +} diff --git a/website/scripts/sync-docs.mjs b/website/scripts/sync-docs.mjs index 4e024de7..88653789 100644 --- a/website/scripts/sync-docs.mjs +++ b/website/scripts/sync-docs.mjs @@ -83,4 +83,5 @@ for (const abs of walk(DOCS)) { writeFileSync(outAbs, `---\ntitle: ${JSON.stringify(title)}\n---\n\n${src}`); count++; } + console.log(`sync-docs: wrote ${count} pages to ${relative(REPO, OUT)}`); diff --git a/website/src/pages/404.astro b/website/src/pages/404.astro new file mode 100644 index 00000000..9c85cd14 --- /dev/null +++ b/website/src/pages/404.astro @@ -0,0 +1,24 @@ +--- +import StarlightPage from '@astrojs/starlight/components/StarlightPage.astro'; + +Astro.response.status = 404; +--- + + + diff --git a/website/src/styles/custom.css b/website/src/styles/custom.css index 87da75bf..1d74b4a6 100644 --- a/website/src/styles/custom.css +++ b/website/src/styles/custom.css @@ -48,7 +48,7 @@ @font-face { font-family: "Archivo"; - src: url("/Canton-Dex-Reference-Implementation/fonts/archivo.woff2") format("woff2"); + src: url("/fonts/archivo.woff2") format("woff2"); font-weight: 100 900; font-stretch: 62% 125%; font-style: normal; @@ -56,7 +56,7 @@ } @font-face { font-family: "Archivo"; - src: url("/Canton-Dex-Reference-Implementation/fonts/archivo-italic.woff2") format("woff2"); + src: url("/fonts/archivo-italic.woff2") format("woff2"); font-weight: 100 900; font-stretch: 62% 125%; font-style: italic; @@ -64,14 +64,14 @@ } @font-face { font-family: "JetBrains Mono"; - src: url("/Canton-Dex-Reference-Implementation/fonts/jetbrains-mono.woff2") format("woff2"); + src: url("/fonts/jetbrains-mono.woff2") format("woff2"); font-weight: 100 800; font-style: normal; font-display: swap; } @font-face { font-family: "JetBrains Mono"; - src: url("/Canton-Dex-Reference-Implementation/fonts/jetbrains-mono-italic.woff2") format("woff2"); + src: url("/fonts/jetbrains-mono-italic.woff2") format("woff2"); font-weight: 100 800; font-style: italic; font-display: swap;