From 6e1ad82b17270ee4d7f7f550925b2302f0f6544e Mon Sep 17 00:00:00 2001 From: Nicolas Fernandez Date: Sun, 22 Mar 2026 16:26:34 -0300 Subject: [PATCH 1/9] add guards.one Cardano Pythathon submission --- lazer/cardano/guards-one/README.md | 79 ++ lazer/cardano/guards-one/source/.env.example | 22 + lazer/cardano/guards-one/source/.gitignore | 9 + lazer/cardano/guards-one/source/NEXT_STEPS.md | 69 ++ lazer/cardano/guards-one/source/README.md | 158 +++ .../source/apps/backend/package.json | 11 + .../source/apps/backend/src/collector.ts | 27 + .../source/apps/backend/src/dashboard.ts | 399 ++++++ .../source/apps/backend/src/demo-state.ts | 290 +++++ .../guards-one/source/apps/backend/src/env.ts | 56 + .../source/apps/backend/src/export-ui-data.ts | 16 + .../source/apps/backend/src/fixtures.ts | 33 + .../source/apps/backend/src/keeper.ts | 135 ++ .../source/apps/backend/src/preview-server.ts | 84 ++ .../source/apps/backend/src/risk-engine.ts | 20 + .../source/apps/backend/src/simulate.ts | 4 + .../source/apps/backend/src/storage.ts | 131 ++ .../apps/backend/tests/dashboard.test.ts | 21 + .../source/apps/backend/tests/e2e.test.ts | 83 ++ .../source/apps/backend/tests/storage.test.ts | 56 + .../cardano/guards-one/source/apps/ui/app.js | 617 ++++++++++ .../source/apps/ui/data/demo-state.json | 268 ++++ .../guards-one/source/apps/ui/index.html | 203 +++ .../guards-one/source/apps/ui/styles.css | 881 +++++++++++++ .../guards-one/source/docs/functional-v4.md | 113 ++ .../source/docs/landing-frontend-spec.md | 100 ++ .../cardano/guards-one/source/docs/roadmap.md | 39 + lazer/cardano/guards-one/source/package.json | 18 + .../source/packages/cardano/package.json | 12 + .../source/packages/cardano/src/index.ts | 2 + .../packages/cardano/src/policy-vault.ts | 379 ++++++ .../source/packages/cardano/src/types.ts | 65 + .../cardano/tests/policy-vault.test.ts | 421 +++++++ .../source/packages/core/package.json | 9 + .../source/packages/core/src/capabilities.ts | 50 + .../source/packages/core/src/engine.ts | 488 ++++++++ .../source/packages/core/src/fixtures.ts | 130 ++ .../source/packages/core/src/hash.ts | 24 + .../source/packages/core/src/index.ts | 6 + .../source/packages/core/src/math.ts | 135 ++ .../source/packages/core/src/types.ts | 183 +++ .../source/packages/core/tests/engine.test.ts | 209 ++++ .../source/packages/core/tests/math.test.ts | 71 ++ .../source/packages/evm/package.json | 14 + .../source/packages/evm/src/fixtures.ts | 23 + .../source/packages/evm/src/index.ts | 151 +++ .../source/packages/evm/tests/index.test.ts | 56 + .../source/packages/svm/package.json | 14 + .../source/packages/svm/src/fixtures.ts | 23 + .../source/packages/svm/src/index.ts | 151 +++ .../source/packages/svm/tests/index.test.ts | 56 + .../cardano/guards-one/source/pnpm-lock.yaml | 1090 +++++++++++++++++ .../guards-one/source/pnpm-workspace.yaml | 3 + lazer/cardano/guards-one/source/tsconfig.json | 33 + 54 files changed, 7740 insertions(+) create mode 100644 lazer/cardano/guards-one/README.md create mode 100644 lazer/cardano/guards-one/source/.env.example create mode 100644 lazer/cardano/guards-one/source/.gitignore create mode 100644 lazer/cardano/guards-one/source/NEXT_STEPS.md create mode 100644 lazer/cardano/guards-one/source/README.md create mode 100644 lazer/cardano/guards-one/source/apps/backend/package.json create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/collector.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/dashboard.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/demo-state.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/env.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/export-ui-data.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/fixtures.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/keeper.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/preview-server.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/risk-engine.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/simulate.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/src/storage.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/tests/dashboard.test.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/tests/e2e.test.ts create mode 100644 lazer/cardano/guards-one/source/apps/backend/tests/storage.test.ts create mode 100644 lazer/cardano/guards-one/source/apps/ui/app.js create mode 100644 lazer/cardano/guards-one/source/apps/ui/data/demo-state.json create mode 100644 lazer/cardano/guards-one/source/apps/ui/index.html create mode 100644 lazer/cardano/guards-one/source/apps/ui/styles.css create mode 100644 lazer/cardano/guards-one/source/docs/functional-v4.md create mode 100644 lazer/cardano/guards-one/source/docs/landing-frontend-spec.md create mode 100644 lazer/cardano/guards-one/source/docs/roadmap.md create mode 100644 lazer/cardano/guards-one/source/package.json create mode 100644 lazer/cardano/guards-one/source/packages/cardano/package.json create mode 100644 lazer/cardano/guards-one/source/packages/cardano/src/index.ts create mode 100644 lazer/cardano/guards-one/source/packages/cardano/src/policy-vault.ts create mode 100644 lazer/cardano/guards-one/source/packages/cardano/src/types.ts create mode 100644 lazer/cardano/guards-one/source/packages/cardano/tests/policy-vault.test.ts create mode 100644 lazer/cardano/guards-one/source/packages/core/package.json create mode 100644 lazer/cardano/guards-one/source/packages/core/src/capabilities.ts create mode 100644 lazer/cardano/guards-one/source/packages/core/src/engine.ts create mode 100644 lazer/cardano/guards-one/source/packages/core/src/fixtures.ts create mode 100644 lazer/cardano/guards-one/source/packages/core/src/hash.ts create mode 100644 lazer/cardano/guards-one/source/packages/core/src/index.ts create mode 100644 lazer/cardano/guards-one/source/packages/core/src/math.ts create mode 100644 lazer/cardano/guards-one/source/packages/core/src/types.ts create mode 100644 lazer/cardano/guards-one/source/packages/core/tests/engine.test.ts create mode 100644 lazer/cardano/guards-one/source/packages/core/tests/math.test.ts create mode 100644 lazer/cardano/guards-one/source/packages/evm/package.json create mode 100644 lazer/cardano/guards-one/source/packages/evm/src/fixtures.ts create mode 100644 lazer/cardano/guards-one/source/packages/evm/src/index.ts create mode 100644 lazer/cardano/guards-one/source/packages/evm/tests/index.test.ts create mode 100644 lazer/cardano/guards-one/source/packages/svm/package.json create mode 100644 lazer/cardano/guards-one/source/packages/svm/src/fixtures.ts create mode 100644 lazer/cardano/guards-one/source/packages/svm/src/index.ts create mode 100644 lazer/cardano/guards-one/source/packages/svm/tests/index.test.ts create mode 100644 lazer/cardano/guards-one/source/pnpm-lock.yaml create mode 100644 lazer/cardano/guards-one/source/pnpm-workspace.yaml create mode 100644 lazer/cardano/guards-one/source/tsconfig.json diff --git a/lazer/cardano/guards-one/README.md b/lazer/cardano/guards-one/README.md new file mode 100644 index 00000000..e127d5b4 --- /dev/null +++ b/lazer/cardano/guards-one/README.md @@ -0,0 +1,79 @@ +# Team SOLx-AR Pythathon Submission + +## Details + +Team Name: SOLx-AR +Submission Name: guards.one +Team Members: TODO before final submission +Contact: TODO before final submission + +## Project Description + +guards.one is an oracle-aware treasury control plane for Cardano treasuries with a multichain-native policy engine. It turns treasury risk rules into bounded execution intents and operator-visible actions. + +### What does this project do? + +- monitors treasury liquid value, stable ratio, drawdown versus EMA, oracle freshness, and confidence +- escalates through a risk ladder: `normal -> watch -> partial de-risk -> full stable exit -> auto re-entry` +- simulates the Cardano two-step execution flow: authorize intent first, execute the approved stable swap second +- ships a replayable operator UI inspired by treasury / multisig desks so judges can see the full breach lifecycle quickly + +### How does it integrate with Pyth? + +- uses Pyth price feeds as the primary market data source for `ADA/USD` and the stable reserve reference +- evaluates `price`, `emaPrice`, oracle freshness, and confidence inside the shared risk engine +- models the Cardano preprod witness flow with the provided `PYTH_PREPROD_POLICY_ID` and signed update envelope in the control-plane simulator +- keeps the codebase ready for the real off-chain Pyth Cardano SDK integration in the next implementation step + +### What problem does it solve? + +DAOs and on-chain treasuries do not fail only because an asset moves `X%`; they fail when protected fiat value drops below the floor they intended to defend. guards.one automates that defense with transparent oracle-aware rules instead of ad hoc human reactions. + +## Repository Structure + +```text +lazer/cardano/guards-one/ +├── README.md # Hackathon submission README +└── source/ + ├── apps/ # Backend demo server and operator UI + ├── docs/ # Functional spec, roadmap, frontend spec + ├── packages/ # Core engine, Cardano adapter, SVM/EVM scaffolds + ├── package.json + ├── pnpm-lock.yaml + ├── pnpm-workspace.yaml + ├── tsconfig.json + └── .env.example +``` + +## How to Test + +### Prerequisites + +- Node.js 22+ +- pnpm 10+ + +### Setup & Run Instructions + +```bash +cd lazer/cardano/guards-one/source +pnpm install +pnpm typecheck +pnpm test +pnpm simulate +pnpm export:ui-data +pnpm preview +``` + +Then open `http://localhost:4310` and use the `Replay breach` action in the UI. + +## Deployment Information + +Network: Cardano preprod (simulated control-plane path in this submission) +Contract Address(es): N/A in this snapshot; the current repo still uses the Cardano policy-vault simulator/spec +Demo URL: local preview via `pnpm preview` + +## Notes for Reviewers + +- The current submission includes the working core engine, backend simulation, multichain scaffolding, and replayable operator UI. +- The next technical step after the hackathon is porting the Cardano control-plane simulator to `Aiken` and wiring real Pyth signed updates on preprod. +- Replace the `TODO` team/contact fields before final submission. diff --git a/lazer/cardano/guards-one/source/.env.example b/lazer/cardano/guards-one/source/.env.example new file mode 100644 index 00000000..dcf1d09b --- /dev/null +++ b/lazer/cardano/guards-one/source/.env.example @@ -0,0 +1,22 @@ +APP_PUBLIC_NAME=guards.one +APP_INTERNAL_NAME=anaconda +PORT=4310 + +PYTH_API_KEY=replace_with_pyth_api_key +PYTH_PREPROD_POLICY_ID=d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6 +PYTH_API_BASE_URL=https://api.pyth.network +PYTH_STREAM_CHANNEL=fixed_rate@200ms +PYTH_PRIMARY_FEED_ID=pyth-ada-usd +CARDANO_PYTH_STATE_REFERENCE=replace_with_pyth_state_reference_utxo + +CARDANO_NETWORK=preprod +CARDANO_PROVIDER=blockfrost +CARDANO_PROVIDER_URL=https://cardano-preprod.blockfrost.io/api/v0 +CARDANO_BLOCKFROST_PROJECT_ID=replace_with_blockfrost_project_id +CARDANO_EXECUTION_ROUTE_ID=cardano-minswap-ada-usdm +CARDANO_EXECUTION_HOT_WALLET_ADDRESS=replace_with_execution_hot_wallet +CARDANO_EXECUTION_HOT_WALLET_SKEY_PATH=./secrets/execution-hot.skey +CARDANO_GOVERNANCE_WALLET_ADDRESS=replace_with_governance_wallet +CARDANO_GOVERNANCE_SKEY_PATH=./secrets/governance.skey + +AUDIT_DB_PATH=./data/guards-one.sqlite diff --git a/lazer/cardano/guards-one/source/.gitignore b/lazer/cardano/guards-one/source/.gitignore new file mode 100644 index 00000000..de18100f --- /dev/null +++ b/lazer/cardano/guards-one/source/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +.DS_Store +.pnpm-store/ +*.log +.env +.env.* +!.env.example +secrets/ +*.sqlite diff --git a/lazer/cardano/guards-one/source/NEXT_STEPS.md b/lazer/cardano/guards-one/source/NEXT_STEPS.md new file mode 100644 index 00000000..862469db --- /dev/null +++ b/lazer/cardano/guards-one/source/NEXT_STEPS.md @@ -0,0 +1,69 @@ +# NEXT STEPS + +This is the canonical tracker for the repository. Every new task, bug, review comment, or deployment prerequisite should be added here and checked off when done. + +## Rules +- Update this file in the same commit that changes the underlying work. +- Keep items concrete and testable. +- If something is blocked, note the blocker inline instead of deleting the task. + +## Current Review Pass + +### PR #1 · Docs +- [x] Align README wording with the documentation-only state of the branch +- [x] Standardize risk-ladder terminology across README, functional spec, and Copilot instructions +- [x] Clarify `ema_price` vs `emaPrice` +- [x] Clarify the "two-transaction flow" wording in the frontend spec + +### PR #2 · Core +- [x] Value stable assets using oracle price when available so depegs affect portfolio math +- [x] Enforce route `chainId` and `live` status in route selection +- [x] Sort snapshot IDs deterministically before hashing/auditing +- [x] Remove root scripts that reference apps before those workspaces exist +- [x] Regenerate the lockfile/workspace state to match the branch contents +- [x] Add a depeg regression test for stable valuation + +### PR #3 · Multichain +- [x] Reuse core shared types instead of duplicating multichain primitives +- [x] Align SVM/EVM stage naming with the core risk ladder +- [x] Validate simulation inputs before computing outputs +- [x] Add invalid-pricing regression tests for SVM and EVM + +### PR #4 · Cardano +- [x] Replace fragile `try/catch` tests with assertions that fail when no error is thrown +- [x] Enforce `result.routeId === intent.routeId` +- [x] Derive tx validity from intent/policy instead of hardcoded constants +- [x] Reject authorization while an intent is already in flight +- [x] Wire `pythStateReference` into the tx envelope +- [x] Reuse `intent.reasonHash` in the tx metadata +- [x] Validate vault, chain, execution time window, and max sell bounds on completion +- [x] Make simulated `averagePrice` consistent with the trade math + +### PR #5 · Apps +- [x] Fix invalid CSS `min()` usage with `calc()` +- [x] Replace broken local docs links in the UI with a safe repo/demo strategy +- [x] Align fallback chip tokens with existing styles +- [x] Escape or avoid unsafe HTML injection in the UI renderer +- [x] Remove the unused backend connector field +- [x] Make audit event ordering deterministic +- [x] Harden preview path resolution against traversal and absolute-path bugs +- [x] Avoid crashing when `Host` is missing in the preview server + +### PR #6 · UI Demo +- [x] Make the watch frame deterministic by using a separate watch-only snapshot before partial de-risk +- [x] Validate ladder tone tokens before interpolating them into CSS class names + +## Product / Stack Pending +- [x] Bootstrap root `.env` and `.env.example` for Pyth/Cardano preprod and deploy settings +- [x] Rebuild the operator UI with a Squads-inspired treasury shell and dark dashboard layout +- [x] Add deterministic demo frames and replay controls for the breach -> de-risk -> exit -> recovery scenario +- [x] Refresh the local preview flow so the UI can be shown as a working product demo +- [ ] Port `PolicyVault` to `Aiken` +- [ ] Integrate real Pyth signed updates and preprod witness flow +- [ ] Integrate a live Cardano swap venue +- [ ] Replace SQLite demo persistence with deployable storage +- [ ] Add CI for tests and typecheck +- [ ] Capture final demo screenshots / recording for the hackathon pitch +- [ ] Prepare the `pyth-examples` submission tree under `/lazer/cardano/my-project/` +- [ ] Adapt the submission README to the hackathon template from `pyth-examples` +- [ ] Open the submission PR from the fork back to `pyth-network/pyth-examples` diff --git a/lazer/cardano/guards-one/source/README.md b/lazer/cardano/guards-one/source/README.md new file mode 100644 index 00000000..b895b84e --- /dev/null +++ b/lazer/cardano/guards-one/source/README.md @@ -0,0 +1,158 @@ +# guards.one + +guards.one is an oracle-aware treasury policy enforcement MVP for Cardano with a multichain-native core. This documentation bootstrap describes the target architecture and the implementation plan that lands in the follow-up PR stack. + +Built for the Buenos Aires Pythathon, the project treats Pyth as a core system dependency, not a cosmetic data source: risk state changes, execution authorization, and auditability all depend on oracle evidence. Public branding is `guards.one`; the internal package/tooling namespace remains `anaconda`. + +## Product thesis +guards.one is not a dashboard that happens to read oracle data. It is a treasury control plane that turns risk rules into executable actions. + +The core loop is: +1. ingest Pyth snapshots, +2. evaluate treasury health using drawdown, fiat floors, freshness, and confidence, +3. authorize a bounded execution intent, +4. execute the approved route from the hot bucket, +5. anchor an audit trail with intent, result, and oracle evidence. + +For the MVP, `Cardano` is the live execution target and the risk logic is intentionally portable so future `SVM` and `EVM` connectors can consume the same business model without a refactor. + +## Why this matters +- DAOs and on-chain treasuries often define treasury mandates in percentages, but the real failure mode is breaching an absolute protection floor in fiat or stable terms. +- Oracle-driven automation should not only track spot price; it must also reason about stale data, confidence widening, and recovery hysteresis. +- guards.one converts those constraints into a bounded two-step execution model: authorize based on oracle evidence, then execute only an approved route from a capped hot wallet. + +## Implemented MVP scope +The current repository already includes: +- a shared core policy engine with liquid-value based triggers using `price`, `emaPrice`, `confidence`, freshness, and fiat floors +- a Cardano control-plane simulator and later an `Aiken` port +- backend services for collector, keeper, risk engine, audit logging, and end-to-end simulation +- a Squads-inspired operator UI shell for the treasury demo +- `SVM` and `EVM` scaffolding packages so the core remains multichain-native from day one + +## Why Pyth is required +- The policy engine evaluates both `price` and `emaPrice`. +- Automatic execution is blocked when the oracle snapshot is stale. +- Confidence widening is treated as a first-class risk signal, not just an informational metric. +- Every execution path is designed around carrying oracle evidence into the control plane so the decision can be audited later. + +## Canonical risk ladder +Display names and code identifiers will stay aligned across docs and implementation: +- `Normal` (`normal`): no policy breach, treasury operates normally +- `Watch` (`watch`): early warning, no forced swap yet +- `Partial DeRisk` (`partial_derisk`): sell just enough risky exposure to rebuild the protected stable floor / target ratio +- `Full Stable Exit` (`full_exit`): move the remaining risky bucket into the approved stable asset +- `Frozen` (`frozen`): stale or low-quality oracle conditions block automatic execution +- `Auto Re-entry`: recovery path that becomes available only after hysteresis and cooldown + +The engine measures both percentage risk and absolute protected value: + +`liquid_value = amount * pyth_price * (1 - haircut_bps / 10000)` + +That means the trigger is not just “asset dropped X%”, but also “the treasury or a protected asset fell below the fiat floor we promised to defend”. + +## Target architecture +### Shared core +- `PolicyConfig` +- `OracleSnapshot` +- `RiskStage` +- `ExecutionIntent` +- `ExecutionResult` +- `RouteSpec` +- `TreasuryConnector` + +### Cardano MVP path +- `Tx 1`: authorize execution with oracle-aware validation and an emitted `ExecutionIntent` +- `Tx 2`: execute the approved swap from the execution hot bucket and anchor the result + +### Multichain stance +- `Cardano`: live path in the MVP +- `SVM`: scaffold package with capability matrix and route simulation +- `EVM`: scaffold package with capability matrix and route simulation + +The business logic is shared; execution remains local to each connected treasury chain. + +## Intended demo flow +1. A treasury is configured with a protected asset, approved stable, floor thresholds, guardrails, and route policy. +2. The collector ingests an oracle snapshot and the risk engine evaluates treasury state. +3. If a breach is detected, the Cardano control plane authorizes a bounded `ExecutionIntent`. +4. The keeper executes a swap from the hot bucket using the approved route and anchors the result. +5. Once the market recovers beyond the re-entry threshold and cooldown, the system can auto re-enter. + +The current simulation and UI replay cover this full sequence: +- `partial_derisk` +- `full_exit` +- `auto re-entry` + +The frontend now exposes that run as a deterministic operator demo: +- a workspace card and treasury shell inspired by premium multisig / treasury desks +- account tables for the hot risk bucket and stable reserve +- a replayable timeline for `watch -> partial de-risk -> full stable exit -> recovery` +- a chart built from the simulated backend series +- audit cards rendered from the backend event log + +## Planned repo layout +The follow-up PR stack introduces: +- `packages/core`: shared types, formulas, policy engine, fixtures, tests +- `packages/cardano`: Cardano control-plane simulator and execution connector +- `packages/svm`: scaffold connector and tests +- `packages/evm`: scaffold connector and tests +- `apps/backend`: collector, keeper, audit store, simulation, e2e tests +- `apps/ui`: static landing and dashboard shell +- `docs`: functional spec, roadmap, landing/frontend spec + +## Development workflow +The repository is intentionally organized as a monorepo because the core risk model must stay chain-agnostic while adapters evolve independently. + +Suggested review order: +1. `docs/` for product scope and UX intent +2. `packages/core` for the business model and policy engine +3. `packages/cardano` for the Cardano control-plane simulator +4. `apps/backend` for orchestration, persistence, and e2e flow +5. `apps/ui` for the operator-facing shell + +## Planned scripts +These commands land in the follow-up code PRs: +- `pnpm test`: run the full test suite +- `pnpm typecheck`: run TypeScript type checks +- `pnpm simulate`: execute the end-to-end backend simulation +- `pnpm export:ui-data`: generate the backend demo payload for the static UI +- `pnpm preview`: serve `apps/ui` plus `/api/demo-state` locally + +## Quick start (once the code PRs land) +```bash +pnpm install +pnpm typecheck +pnpm test +pnpm simulate +pnpm export:ui-data +pnpm preview +``` + +Open `http://localhost:4310` after `pnpm preview` to inspect the static frontend using a backend-generated demo payload. + +## Current repo status +- The docs, core engine, backend simulation, UI shell, and multichain scaffolding are merged in `main`. +- The living execution tracker is maintained in `NEXT_STEPS.md`. +- The next engineering steps are the real `Aiken` port, real Pyth witness flow, and a live Cardano swap venue. + +## Constraints and non-goals +- Cardano is the only live execution target in this MVP. +- `SVM` and `EVM` are scaffolded from day one so the business logic does not need a future refactor. +- Hedge/perps are intentionally left in the roadmap, not in the live MVP. +- The Cardano validator path is modeled as a TypeScript simulator/spec in this repo; a compiled `Aiken` validator is the next implementation step. + +## Documentation +- [Functional spec](./docs/functional-v4.md) +- [Roadmap](./docs/roadmap.md) +- [Landing / frontend spec](./docs/landing-frontend-spec.md) +- [Execution tracker](./NEXT_STEPS.md) + +## Environment +- Root `.env` is used for local backend scripts and deploy preparation. +- `.env` is gitignored and already seeded locally with the provided Pyth API key and preprod policy id. +- `.env.example` is safe to commit and documents the variables needed for Pyth, Cardano preprod, wallets, provider, and audit storage. + +## Next implementation steps +1. Port the Cardano policy-vault simulator to an `Aiken` validator and wire the real Pyth witness flow. +2. Replace the simulated execution connector with a real Cardano route builder and settlement path. +3. Capture final screenshots / recording from the local replay demo for the pitch and `pyth-examples` submission. diff --git a/lazer/cardano/guards-one/source/apps/backend/package.json b/lazer/cardano/guards-one/source/apps/backend/package.json new file mode 100644 index 00000000..ce3b7a4a --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/package.json @@ -0,0 +1,11 @@ +{ + "name": "@anaconda/backend", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@anaconda/cardano": "workspace:*", + "@anaconda/core": "workspace:*", + "dotenv": "^17.2.3" + } +} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/collector.ts b/lazer/cardano/guards-one/source/apps/backend/src/collector.ts new file mode 100644 index 00000000..fa8273fc --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/collector.ts @@ -0,0 +1,27 @@ +import type { OracleSnapshot } from "@anaconda/core"; +import { AuditStore } from "./storage.js"; + +export class PythCollector { + private readonly snapshots = new Map(); + + constructor(private readonly auditStore?: AuditStore) {} + + publish(snapshot: OracleSnapshot) { + this.snapshots.set(snapshot.assetId, snapshot); + this.auditStore?.recordSnapshot(snapshot); + this.auditStore?.recordEvent({ + eventId: `snapshot:${snapshot.snapshotId}`, + category: "snapshot", + payload: { + assetId: snapshot.assetId, + snapshotId: snapshot.snapshotId, + observedAtUs: snapshot.observedAtUs, + }, + createdAtUs: snapshot.observedAtUs, + }); + } + + current(): Record { + return Object.fromEntries(this.snapshots.entries()); + } +} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/dashboard.ts b/lazer/cardano/guards-one/source/apps/backend/src/dashboard.ts new file mode 100644 index 00000000..4cbfadff --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/dashboard.ts @@ -0,0 +1,399 @@ +import { + baseCapabilities, + computePositionLiquidValue, + evaluateRiskLadder, + type OracleSnapshot, + type PolicyConfig, + type RouteSpec, + type TreasuryState, +} from "@anaconda/core"; +import type { TickResult } from "./keeper.js"; +import type { AuditEvent } from "./storage.js"; + +export interface DashboardSeriesPoint { + label: string; + stage: TreasuryState["stage"]; + valueFiat: number; +} + +export interface DashboardDemoFrame { + label: string; + title: string; + copy: string; + stage: TreasuryState["stage"]; + valueFiat: number; + stableRatio: number; + reason: string; +} + +export interface DashboardPayload { + generatedAtUs: number; + source: string; + workspace: { + name: string; + label: string; + chain: string; + stage: string; + threshold: string; + members: string; + hotWallet: string; + governanceWallet: string; + totalBalance: string; + primaryAsset: string; + stableAsset: string; + vaultId: string; + }; + topbarChips: Array<{ + label: string; + value: string; + tone: string; + }>; + heroMetrics: Array<{ + label: string; + value: string; + copy: string; + chip?: string; + }>; + dashboardCards: Array<{ + label: string; + value: string; + copy: string; + chip: string; + }>; + chainCards: Array<{ + chain: string; + title: string; + copy: string; + chip: string; + }>; + riskLadder: Array<{ + stage: string; + title: string; + copy: string; + tone?: string; + }>; + executionTimeline: Array<{ + title: string; + copy: string; + status: string; + }>; + auditTrail: Array<{ + title: string; + copy: string; + stamp: string; + }>; + accounts: Array<{ + label: string; + address: string; + balance: string; + fiatValue: string; + weight: string; + role: string; + bucket: string; + }>; + portfolioSeries: Array<{ + label: string; + stage: string; + value: number; + displayValue: string; + }>; + demoFrames: Array<{ + label: string; + title: string; + copy: string; + stage: string; + balance: string; + stableRatio: string; + reason: string; + }>; +} + +function formatUsd(value: number): string { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: value >= 1000 ? 0 : 2, + }).format(value); +} + +function formatPercent(value: number): string { + return `${(value * 100).toFixed(1)}%`; +} + +function formatAgeUs(nowUs: number, thenUs: number): string { + const seconds = Math.max(0, Math.round((nowUs - thenUs) / 1_000_000)); + return `${seconds}s`; +} + +function formatAmount(value: number): string { + return new Intl.NumberFormat("en-US", { + maximumFractionDigits: value >= 1000 ? 0 : 2, + }).format(value); +} + +function shortAddress(value: string): string { + if (value.length <= 14) { + return value; + } + + return `${value.slice(0, 8)}...${value.slice(-6)}`; +} + +function stageLabel(stage: TreasuryState["stage"]): string { + switch (stage) { + case "normal": + return "Normal"; + case "watch": + return "Watch"; + case "partial_derisk": + return "Partial De-Risk"; + case "full_exit": + return "Full Stable Exit"; + case "frozen": + return "Frozen"; + } +} + +function stageChip(stage: TreasuryState["stage"]): string { + switch (stage) { + case "normal": + return "ok"; + case "watch": + return "warn"; + case "partial_derisk": + return "warn"; + case "full_exit": + return "danger"; + case "frozen": + return "danger"; + } +} + +function formatAuditCopy(event: AuditEvent): string { + switch (event.category) { + case "snapshot": { + const assetId = typeof event.payload.assetId === "string" ? event.payload.assetId : "asset"; + const snapshotId = + typeof event.payload.snapshotId === "string" ? event.payload.snapshotId : "unknown snapshot"; + return `Observed ${assetId.toUpperCase()} from ${snapshotId}.`; + } + case "intent": { + const kind = typeof event.payload.kind === "string" ? event.payload.kind.replaceAll("_", " ") : "intent"; + const stage = typeof event.payload.stage === "string" ? event.payload.stage.replaceAll("_", " ") : "n/a"; + return `Authorized ${kind} at stage ${stage}.`; + } + case "execution": { + const sold = typeof event.payload.soldAmount === "number" ? formatAmount(event.payload.soldAmount) : "n/a"; + const bought = typeof event.payload.boughtAmount === "number" ? formatAmount(event.payload.boughtAmount) : "n/a"; + const stage = typeof event.payload.stage === "string" ? event.payload.stage.replaceAll("_", " ") : "n/a"; + return `Settled ${stage}. Sold ${sold} and bought ${bought}.`; + } + case "rejection": { + const code = typeof event.payload.code === "string" ? event.payload.code : "unknown"; + const keeperId = typeof event.payload.keeperId === "string" ? event.payload.keeperId : "keeper"; + return `Rejected with ${code} for ${keeperId}.`; + } + } +} + +export function buildDashboardPayload(input: { + treasury: TreasuryState; + policy: PolicyConfig; + routes: RouteSpec[]; + snapshots: Record; + operations: TickResult[]; + events: AuditEvent[]; + nowUs: number; + portfolioSeries: DashboardSeriesPoint[]; + demoFrames: DashboardDemoFrame[]; +}): DashboardPayload { + const assessment = evaluateRiskLadder( + input.treasury, + input.policy, + input.snapshots, + input.routes, + input.nowUs, + ); + const latestSnapshot = input.snapshots[input.policy.primaryAssetId]; + const route = input.routes.find((candidate) => + input.policy.approvedRouteIds.includes(candidate.routeId), + ); + const topReason = assessment.reasons[0]?.message ?? "Policy is currently inside the safe band."; + const liveChains = Object.values(baseCapabilities); + const riskPosition = input.treasury.positions.find((position) => position.role === "risk"); + const stablePosition = input.treasury.positions.find((position) => position.role === "stable"); + const accounts = input.treasury.positions.map((position) => { + const snapshot = input.snapshots[position.assetId]; + const liquidValue = computePositionLiquidValue(position, snapshot, input.policy); + const weight = + assessment.metrics.totalLiquidValueFiat === 0 + ? 0 + : liquidValue / assessment.metrics.totalLiquidValueFiat; + const wallet = + position.bucket === "hot" ? input.treasury.executionHotWallet : input.treasury.governanceWallet; + + return { + label: `${position.symbol} ${position.role === "risk" ? "risk bucket" : "stable reserve"}`, + address: shortAddress(wallet), + balance: `${formatAmount(position.amount)} ${position.symbol}`, + fiatValue: formatUsd(liquidValue), + weight: formatPercent(weight), + role: position.role === "risk" ? "Risk asset" : "Stable reserve", + bucket: position.bucket === "hot" ? "Execution hot" : "Governance cold", + }; + }); + + return { + generatedAtUs: input.nowUs, + source: "backend-demo", + workspace: { + name: input.treasury.name, + label: "guards.one live desk", + chain: "Cardano preprod", + stage: stageLabel(input.treasury.stage), + threshold: `${input.treasury.governanceSigners.length} governance signers`, + members: `${input.treasury.riskManagers.length} risk manager · ${input.treasury.keepers.length} keepers`, + hotWallet: shortAddress(input.treasury.executionHotWallet), + governanceWallet: shortAddress(input.treasury.governanceWallet), + totalBalance: formatUsd(assessment.metrics.totalLiquidValueFiat), + primaryAsset: riskPosition?.symbol ?? input.policy.primaryAssetId.toUpperCase(), + stableAsset: stablePosition?.symbol ?? input.policy.stableAssetId.toUpperCase(), + vaultId: input.treasury.vaultId, + }, + topbarChips: [ + { + label: "Network status", + value: "Cardano preprod live", + tone: "live", + }, + { + label: "Approved route", + value: route ? `${route.venue} · ${route.toAssetId.toUpperCase()}` : "No route", + tone: route?.live ? "neutral" : "warn", + }, + { + label: "Execution wallet", + value: shortAddress(input.treasury.executionHotWallet), + tone: "neutral", + }, + ], + heroMetrics: [ + { + label: "Current stage", + value: stageLabel(input.treasury.stage), + copy: "Final policy state after the simulated breach and recovery run.", + chip: stageChip(input.treasury.stage), + }, + { + label: "Treasury liquid value", + value: formatUsd(assessment.metrics.totalLiquidValueFiat), + copy: "Valued from the latest Pyth price and haircut-aware liquid value math.", + }, + { + label: "Stable protection", + value: formatPercent(assessment.metrics.stableRatio), + copy: "Current treasury share parked in the approved stable reserve.", + }, + { + label: "Oracle freshness", + value: latestSnapshot + ? formatAgeUs(input.nowUs, latestSnapshot.feedUpdateTimestampUs) + : "missing", + copy: "Age of the primary feed update relative to the latest policy evaluation.", + }, + ], + dashboardCards: [ + { + label: "Protected floor", + value: formatUsd(input.policy.portfolioFloorFiat), + copy: "Minimum fiat-equivalent value the policy tries to keep defended at all times.", + chip: "ok", + }, + { + label: "Emergency floor", + value: formatUsd(input.policy.emergencyPortfolioFloorFiat), + copy: "Crossing this floor escalates the vault into a full stable exit or freeze path.", + chip: "danger", + }, + { + label: "Primary reason", + value: topReason, + copy: "Top reason emitted by the risk engine for the current snapshot set.", + chip: assessment.reasons.length > 0 ? "warn" : "ok", + }, + { + label: "Execution policy", + value: route + ? `${route.chainId.toUpperCase()} · ${route.venue} · ${route.toAssetId.toUpperCase()}` + : "No approved route", + copy: "Only allowlisted routes can spend from the execution hot wallet.", + chip: route?.live ? "ok" : "warn", + }, + ], + chainCards: liveChains.map((capability) => ({ + chain: capability.chainId.toUpperCase(), + title: capability.live ? "Live execution surface" : "Scaffolded adapter", + copy: capability.notes.join(" "), + chip: capability.live ? "live" : "scaffold", + })), + riskLadder: [ + { + stage: "Normal", + title: "Operate with full permissions", + copy: "Fresh oracle, healthy confidence, and no forced action path active.", + }, + { + stage: "Watch", + title: "Increase monitoring", + copy: "The drawdown or fiat floor approaches the first trigger band.", + tone: "partial", + }, + { + stage: "Partial De-Risk", + title: "Sell only what restores the safe floor", + copy: "The keeper sells a bounded slice of the risky bucket into the approved stable route.", + tone: "partial", + }, + { + stage: "Full Stable Exit", + title: "Move the vault fully defensive", + copy: "A deeper breach exits the risk bucket and keeps the hot wallet on one stable rail.", + tone: "full", + }, + { + stage: "Auto Re-entry", + title: "Re-risk only after hysteresis clears", + copy: "Recovery must clear a separate band and cooldown before exposure comes back.", + tone: "reentry", + }, + ], + executionTimeline: input.operations.map((operation, index) => ({ + title: `Step ${index + 1} · ${stageLabel(operation.stage)}`, + copy: operation.rejected + ? `Execution rejected with ${operation.rejected}.` + : `Intent ${operation.intentId ?? "n/a"} anchored and settled in tx ${operation.txHash ?? "n/a"}.`, + status: operation.rejected ? "rejected" : "executed", + })), + auditTrail: input.events.slice(-6).map((event) => ({ + title: `${event.category.toUpperCase()} · ${event.eventId}`, + copy: formatAuditCopy(event), + stamp: formatAgeUs(input.nowUs, event.createdAtUs), + })), + accounts, + portfolioSeries: input.portfolioSeries.map((point) => ({ + label: point.label, + stage: point.stage, + value: point.valueFiat, + displayValue: formatUsd(point.valueFiat), + })), + demoFrames: input.demoFrames.map((frame) => ({ + label: frame.label, + title: frame.title, + copy: frame.copy, + stage: frame.stage, + balance: formatUsd(frame.valueFiat), + stableRatio: formatPercent(frame.stableRatio), + reason: frame.reason, + })), + }; +} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/demo-state.ts b/lazer/cardano/guards-one/source/apps/backend/src/demo-state.ts new file mode 100644 index 00000000..811afebd --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/demo-state.ts @@ -0,0 +1,290 @@ +import { + evaluateRiskLadder, + type TreasuryState, +} from "@anaconda/core"; +import type { TickResult } from "./keeper.js"; +import { + buildDashboardPayload, + type DashboardDemoFrame, + type DashboardPayload, + type DashboardSeriesPoint, +} from "./dashboard.js"; +import { PythCollector } from "./collector.js"; +import { buildDemoScenario, sampleWitness } from "./fixtures.js"; +import { CardanoKeeperService } from "./keeper.js"; +import { AuditStore } from "./storage.js"; +import { buildSnapshots } from "@anaconda/core"; + +export interface DemoState { + payload: DashboardPayload; + operations: { + partial: TickResult; + fullExit: TickResult; + reentry: TickResult; + }; + counts: ReturnType; + events: ReturnType; +} + +function publishAll( + collector: PythCollector, + snapshots: Record[string]>, +) { + for (const snapshot of Object.values(snapshots)) { + collector.publish(snapshot); + } +} + +function reasonForFrame(result: ReturnType, fallback: string): string { + return result.reasons[0]?.message ?? fallback; +} + +function point(label: string, stage: TreasuryState["stage"], valueFiat: number): DashboardSeriesPoint { + return { + label, + stage, + valueFiat, + }; +} + +function frame(input: { + label: string; + title: string; + copy: string; + stage: TreasuryState["stage"]; + valueFiat: number; + stableRatio: number; + reason: string; +}): DashboardDemoFrame { + return input; +} + +export function createDemoState(): DemoState { + const auditStore = new AuditStore(); + const collector = new PythCollector(auditStore); + const keeper = new CardanoKeeperService(sampleWitness.pythPolicyId, auditStore); + const scenario = buildDemoScenario(); + + publishAll( + collector, + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-watch", + price: 75, + emaPrice: 80, + feedUpdateTimestampUs: 180_000_000, + observedAtUs: 180_000_000, + }, + usdm: { + snapshotId: "snapshot-usdm-watch", + feedUpdateTimestampUs: 180_000_000, + observedAtUs: 180_000_000, + }, + }), + ); + let treasury = scenario.treasury; + + const baselineAssessment = evaluateRiskLadder( + treasury, + scenario.policy, + collector.current(), + scenario.routes, + 180_000_000, + ); + + publishAll( + collector, + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-partial", + price: 72, + emaPrice: 80, + feedUpdateTimestampUs: 200_000_000, + observedAtUs: 200_000_000, + }, + usdm: { + snapshotId: "snapshot-usdm-partial", + feedUpdateTimestampUs: 200_000_000, + observedAtUs: 200_000_000, + }, + }), + ); + + const partial = keeper.tick({ + treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: collector.current(), + nowUs: 200_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + treasury = partial.treasury; + + const partialAssessment = evaluateRiskLadder( + treasury, + scenario.policy, + collector.current(), + scenario.routes, + 200_000_000, + ); + + publishAll( + collector, + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-crash", + price: 39, + emaPrice: 80, + feedUpdateTimestampUs: 240_000_000, + observedAtUs: 240_000_000, + }, + usdm: { + snapshotId: "snapshot-usdm-2", + feedUpdateTimestampUs: 240_000_000, + observedAtUs: 240_000_000, + }, + }), + ); + + const fullExit = keeper.tick({ + treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: collector.current(), + nowUs: 260_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + treasury = fullExit.treasury; + + const fullExitAssessment = evaluateRiskLadder( + treasury, + scenario.policy, + collector.current(), + scenario.routes, + 260_000_000, + ); + + publishAll( + collector, + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-recovery", + price: 79, + emaPrice: 80, + confidence: 0.2, + feedUpdateTimestampUs: 340_000_000, + observedAtUs: 340_000_000, + }, + usdm: { + snapshotId: "snapshot-usdm-3", + feedUpdateTimestampUs: 340_000_000, + observedAtUs: 340_000_000, + }, + }), + ); + + const reentry = keeper.tick({ + treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: collector.current(), + nowUs: 360_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + + const reentryAssessment = evaluateRiskLadder( + reentry.treasury, + scenario.policy, + collector.current(), + scenario.routes, + 360_000_000, + ); + + const events = auditStore.listEvents(); + const portfolioSeries: DashboardSeriesPoint[] = [ + point("Watch", "watch", baselineAssessment.metrics.totalLiquidValueFiat), + point("Partial", partial.treasury.stage, partialAssessment.metrics.totalLiquidValueFiat), + point("Exit", fullExit.treasury.stage, fullExitAssessment.metrics.totalLiquidValueFiat), + point("Recovery", reentry.treasury.stage, reentryAssessment.metrics.totalLiquidValueFiat), + ]; + const demoFrames: DashboardDemoFrame[] = [ + frame({ + label: "01", + title: "Watchlist breach detected", + copy: + "ADA slips under its EMA while Pyth freshness and confidence remain healthy enough to authorize a policy action.", + stage: "watch", + valueFiat: baselineAssessment.metrics.totalLiquidValueFiat, + stableRatio: baselineAssessment.metrics.stableRatio, + reason: reasonForFrame( + baselineAssessment, + "Drawdown approaches the first policy band.", + ), + }), + frame({ + label: "02", + title: "Partial de-risk executes", + copy: + "The keeper emits an intent, swaps only the bounded amount needed, and restores the defended stable floor.", + stage: partial.treasury.stage, + valueFiat: partialAssessment.metrics.totalLiquidValueFiat, + stableRatio: partialAssessment.metrics.stableRatio, + reason: reasonForFrame( + partialAssessment, + "Partial stable target restored.", + ), + }), + frame({ + label: "03", + title: "Full stable exit after the second leg down", + copy: + "A deeper price break and thinner asset cushion push the vault into a full defensive configuration on the approved stable route.", + stage: fullExit.treasury.stage, + valueFiat: fullExitAssessment.metrics.totalLiquidValueFiat, + stableRatio: fullExitAssessment.metrics.stableRatio, + reason: reasonForFrame( + fullExitAssessment, + "Emergency floor forced the full stable path.", + ), + }), + frame({ + label: "04", + title: "Auto re-entry restores exposure", + copy: + "Recovery clears the hysteresis band, cooldown expires, and the treasury re-enters risk according to the configured target ratio.", + stage: reentry.treasury.stage, + valueFiat: reentryAssessment.metrics.totalLiquidValueFiat, + stableRatio: reentryAssessment.metrics.stableRatio, + reason: reasonForFrame( + reentryAssessment, + "Recovery cleared the re-entry guardrails.", + ), + }), + ]; + + const payload = buildDashboardPayload({ + treasury: reentry.treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: collector.current(), + operations: [partial, fullExit, reentry], + events, + nowUs: 360_000_000, + portfolioSeries, + demoFrames, + }); + + return { + payload, + operations: { + partial, + fullExit, + reentry, + }, + counts: auditStore.counts(), + events, + }; +} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/env.ts b/lazer/cardano/guards-one/source/apps/backend/src/env.ts new file mode 100644 index 00000000..dd363cd8 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/env.ts @@ -0,0 +1,56 @@ +import path from "node:path"; +import { config as loadDotenv } from "dotenv"; + +loadDotenv({ path: path.resolve(process.cwd(), ".env"), quiet: true }); + +function readString(name: string, fallback = ""): string { + const value = process.env[name]; + return typeof value === "string" && value.length > 0 ? value : fallback; +} + +function readNumber(name: string, fallback: number): number { + const value = process.env[name]; + if (!value) { + return fallback; + } + + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +} + +export const runtimeEnv = { + appPublicName: readString("APP_PUBLIC_NAME", "guards.one"), + appInternalName: readString("APP_INTERNAL_NAME", "anaconda"), + port: readNumber("PORT", 4310), + pythApiKey: readString("PYTH_API_KEY"), + pythPreprodPolicyId: readString( + "PYTH_PREPROD_POLICY_ID", + "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6", + ), + pythApiBaseUrl: readString("PYTH_API_BASE_URL", "https://api.pyth.network"), + pythStreamChannel: readString("PYTH_STREAM_CHANNEL", "fixed_rate@200ms"), + pythPrimaryFeedId: readString("PYTH_PRIMARY_FEED_ID", "pyth-ada-usd"), + cardanoNetwork: readString("CARDANO_NETWORK", "preprod"), + cardanoProvider: readString("CARDANO_PROVIDER", "blockfrost"), + cardanoProviderUrl: readString( + "CARDANO_PROVIDER_URL", + "https://cardano-preprod.blockfrost.io/api/v0", + ), + cardanoBlockfrostProjectId: readString("CARDANO_BLOCKFROST_PROJECT_ID"), + cardanoPythStateReference: readString("CARDANO_PYTH_STATE_REFERENCE", "pyth-state-ref"), + cardanoExecutionRouteId: readString( + "CARDANO_EXECUTION_ROUTE_ID", + "cardano-minswap-ada-usdm", + ), + cardanoExecutionHotWalletAddress: readString("CARDANO_EXECUTION_HOT_WALLET_ADDRESS"), + cardanoExecutionHotWalletSkeyPath: readString( + "CARDANO_EXECUTION_HOT_WALLET_SKEY_PATH", + "./secrets/execution-hot.skey", + ), + cardanoGovernanceWalletAddress: readString("CARDANO_GOVERNANCE_WALLET_ADDRESS"), + cardanoGovernanceSkeyPath: readString( + "CARDANO_GOVERNANCE_SKEY_PATH", + "./secrets/governance.skey", + ), + auditDbPath: readString("AUDIT_DB_PATH", "./data/guards-one.sqlite"), +} as const; diff --git a/lazer/cardano/guards-one/source/apps/backend/src/export-ui-data.ts b/lazer/cardano/guards-one/source/apps/backend/src/export-ui-data.ts new file mode 100644 index 00000000..b66b3792 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/export-ui-data.ts @@ -0,0 +1,16 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { createDemoState } from "./demo-state.js"; +import "./env.js"; + +const outputDir = path.resolve(process.cwd(), "apps/ui/data"); +const outputFile = path.join(outputDir, "demo-state.json"); + +await mkdir(outputDir, { recursive: true }); +await writeFile( + outputFile, + `${JSON.stringify(createDemoState().payload, null, 2)}\n`, + "utf8", +); + +console.log(`Wrote ${outputFile}`); diff --git a/lazer/cardano/guards-one/source/apps/backend/src/fixtures.ts b/lazer/cardano/guards-one/source/apps/backend/src/fixtures.ts new file mode 100644 index 00000000..20890f38 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/fixtures.ts @@ -0,0 +1,33 @@ +import { + buildSnapshots, + samplePolicy, + sampleRoutes, + sampleTreasury, +} from "@anaconda/core"; +import { runtimeEnv } from "./env.js"; + +export const sampleWitness = { + pythPolicyId: runtimeEnv.pythPreprodPolicyId, + pythStateReference: runtimeEnv.cardanoPythStateReference, + signedUpdateHex: "0xfeedbeef", +}; + +export function buildDemoScenario() { + return { + treasury: structuredClone(sampleTreasury), + policy: structuredClone(samplePolicy), + routes: structuredClone(sampleRoutes), + snapshots: buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-live", + feedUpdateTimestampUs: 180_000_000, + observedAtUs: 180_000_000, + }, + usdm: { + snapshotId: "snapshot-usdm-live", + feedUpdateTimestampUs: 180_000_000, + observedAtUs: 180_000_000, + }, + }), + }; +} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/keeper.ts b/lazer/cardano/guards-one/source/apps/backend/src/keeper.ts new file mode 100644 index 00000000..7f741cf6 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/keeper.ts @@ -0,0 +1,135 @@ +import { randomUUID } from "node:crypto"; +import { + type ExecutionResult, + type PolicyConfig, + type RouteSpec, + type TreasuryState, +} from "@anaconda/core"; +import { + CardanoExecutionError, + PolicyVaultSimulator, + createCardanoConnector, + type CardanoPythWitness, +} from "@anaconda/cardano"; +import { AuditStore } from "./storage.js"; + +export interface TickInput { + treasury: TreasuryState; + policy: PolicyConfig; + routes: RouteSpec[]; + snapshots: ReturnType; + nowUs: number; + keeperId: string; + witness: CardanoPythWitness; +} + +export interface TickResult { + treasury: TreasuryState; + intentId?: string; + txHash?: string; + stage: TreasuryState["stage"]; + rejected?: string; +} + +export class CardanoKeeperService { + private readonly simulator: PolicyVaultSimulator; + + constructor( + pythPolicyId: string, + private readonly auditStore: AuditStore, + ) { + this.simulator = new PolicyVaultSimulator(pythPolicyId); + } + + tick(input: TickInput): TickResult { + const connector = createCardanoConnector(input.routes); + + try { + const authorization = this.simulator.authorizeExecution({ + treasury: input.treasury, + policy: input.policy, + snapshots: input.snapshots, + routes: input.routes, + nowUs: input.nowUs, + keeperId: input.keeperId, + witness: input.witness, + }); + + this.auditStore.recordIntent(authorization.intent); + this.auditStore.recordEvent({ + eventId: `intent:${authorization.intent.intentId}`, + category: "intent", + payload: { + stage: authorization.intent.stage, + kind: authorization.intent.kind, + reasonHash: authorization.intent.reasonHash, + }, + createdAtUs: authorization.intent.createdAtUs, + }); + + const simulated = connector.simulateRoute( + authorization.intent, + authorization.assessment.metrics.priceMap, + ); + const result: ExecutionResult = { + intentId: authorization.intent.intentId, + vaultId: authorization.intent.vaultId, + chainId: "cardano", + sourceAssetId: simulated.sourceAssetId, + destinationAssetId: simulated.destinationAssetId, + soldAmount: simulated.soldAmount, + boughtAmount: simulated.boughtAmount, + averagePrice: simulated.averagePrice, + txHash: `tx-${randomUUID()}`, + executedAtUs: input.nowUs + 1_000, + routeId: simulated.routeId, + }; + + const completion = this.simulator.completeExecution({ + treasury: authorization.treasury, + policy: input.policy, + intent: authorization.intent, + result, + }); + + this.auditStore.recordExecution(result); + this.auditStore.recordEvent({ + eventId: `execution:${result.txHash}`, + category: "execution", + payload: { + intentId: result.intentId, + txHash: result.txHash, + soldAmount: result.soldAmount, + boughtAmount: result.boughtAmount, + stage: completion.treasury.stage, + }, + createdAtUs: result.executedAtUs, + }); + + return { + treasury: completion.treasury, + intentId: authorization.intent.intentId, + txHash: result.txHash, + stage: completion.treasury.stage, + }; + } catch (error) { + const message = + error instanceof CardanoExecutionError ? error.code : "UNKNOWN_ERROR"; + this.auditStore.recordEvent({ + eventId: `rejection:${randomUUID()}`, + category: "rejection", + payload: { + code: message, + keeperId: input.keeperId, + }, + createdAtUs: input.nowUs, + }); + + return { + treasury: input.treasury, + stage: input.treasury.stage, + rejected: message, + }; + } + } +} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/preview-server.ts b/lazer/cardano/guards-one/source/apps/backend/src/preview-server.ts new file mode 100644 index 00000000..9a0a6c30 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/preview-server.ts @@ -0,0 +1,84 @@ +import { createReadStream, existsSync } from "node:fs"; +import { stat } from "node:fs/promises"; +import { createServer } from "node:http"; +import path from "node:path"; +import { createDemoState } from "./demo-state.js"; +import { runtimeEnv } from "./env.js"; + +const port = runtimeEnv.port; +const uiRoot = path.resolve(process.cwd(), "apps/ui"); +const docsRoot = path.resolve(process.cwd(), "docs"); +const allowedDocs = new Set([ + "functional-v4.md", + "roadmap.md", + "landing-frontend-spec.md", +]); + +const contentTypes: Record = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".md": "text/markdown; charset=utf-8", +}; + +function resolveUiPath(requestPath: string): string { + const safePath = requestPath === "/" ? "/index.html" : requestPath; + const relativePath = safePath.replace(/^\/+/, ""); + const resolvedPath = path.resolve(uiRoot, relativePath); + const relativeToRoot = path.relative(uiRoot, resolvedPath); + + if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) { + return path.resolve(uiRoot, "index.html"); + } + + return resolvedPath; +} + +function resolveDocsPath(requestPath: string): string | undefined { + const fileName = requestPath.replace(/^\/docs\/+/, ""); + if (!allowedDocs.has(fileName)) { + return undefined; + } + + return path.resolve(docsRoot, fileName); +} + +const server = createServer(async (request, response) => { + const hostHeader = + typeof request.headers.host === "string" && request.headers.host.length > 0 + ? request.headers.host + : "localhost"; + const url = new URL(request.url ?? "/", `http://${hostHeader}`); + + if (url.pathname === "/api/demo-state") { + response.writeHead(200, { "content-type": "application/json; charset=utf-8" }); + response.end(JSON.stringify(createDemoState().payload, null, 2)); + return; + } + + const filePath = url.pathname.startsWith("/docs/") + ? resolveDocsPath(url.pathname) + : resolveUiPath(url.pathname); + if (!filePath || !existsSync(filePath)) { + response.writeHead(404, { "content-type": "text/plain; charset=utf-8" }); + response.end("Not found"); + return; + } + + const fileStats = await stat(filePath); + if (!fileStats.isFile()) { + response.writeHead(403, { "content-type": "text/plain; charset=utf-8" }); + response.end("Forbidden"); + return; + } + + response.writeHead(200, { + "content-type": contentTypes[path.extname(filePath)] ?? "application/octet-stream", + }); + createReadStream(filePath).pipe(response); +}); + +server.listen(port, () => { + console.log(`Preview server running at http://localhost:${port}`); +}); diff --git a/lazer/cardano/guards-one/source/apps/backend/src/risk-engine.ts b/lazer/cardano/guards-one/source/apps/backend/src/risk-engine.ts new file mode 100644 index 00000000..c014c069 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/risk-engine.ts @@ -0,0 +1,20 @@ +import { + evaluateRiskLadder, + type OracleSnapshot, + type PolicyConfig, + type RiskAssessment, + type RouteSpec, + type TreasuryState, +} from "@anaconda/core"; + +export class RiskEngine { + evaluate( + treasury: TreasuryState, + policy: PolicyConfig, + snapshots: Record, + routes: RouteSpec[], + nowUs: number, + ): RiskAssessment { + return evaluateRiskLadder(treasury, policy, snapshots, routes, nowUs); + } +} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/simulate.ts b/lazer/cardano/guards-one/source/apps/backend/src/simulate.ts new file mode 100644 index 00000000..09aa04dd --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/simulate.ts @@ -0,0 +1,4 @@ +import "./env.js"; +import { createDemoState } from "./demo-state.js"; + +console.log(JSON.stringify(createDemoState(), null, 2)); diff --git a/lazer/cardano/guards-one/source/apps/backend/src/storage.ts b/lazer/cardano/guards-one/source/apps/backend/src/storage.ts new file mode 100644 index 00000000..19ce6c40 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/src/storage.ts @@ -0,0 +1,131 @@ +import { DatabaseSync } from "node:sqlite"; +import type { ExecutionIntent, ExecutionResult, OracleSnapshot } from "@anaconda/core"; + +export interface AuditEvent { + eventId: string; + category: "snapshot" | "intent" | "execution" | "rejection"; + payload: Record; + createdAtUs: number; +} + +export class AuditStore { + private readonly db: DatabaseSync; + + constructor(location = ":memory:") { + this.db = new DatabaseSync(location); + this.db.exec(` + CREATE TABLE IF NOT EXISTS snapshots ( + snapshot_id TEXT PRIMARY KEY, + asset_id TEXT NOT NULL, + payload TEXT NOT NULL, + observed_at_us INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS intents ( + intent_id TEXT PRIMARY KEY, + vault_id TEXT NOT NULL, + payload TEXT NOT NULL, + created_at_us INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS executions ( + tx_hash TEXT PRIMARY KEY, + intent_id TEXT NOT NULL, + payload TEXT NOT NULL, + executed_at_us INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS audit_events ( + event_id TEXT PRIMARY KEY, + category TEXT NOT NULL, + payload TEXT NOT NULL, + created_at_us INTEGER NOT NULL + ); + `); + } + + recordSnapshot(snapshot: OracleSnapshot) { + this.db + .prepare(` + INSERT OR REPLACE INTO snapshots (snapshot_id, asset_id, payload, observed_at_us) + VALUES (?, ?, ?, ?) + `) + .run( + snapshot.snapshotId, + snapshot.assetId, + JSON.stringify(snapshot), + snapshot.observedAtUs, + ); + } + + recordIntent(intent: ExecutionIntent) { + this.db + .prepare(` + INSERT OR REPLACE INTO intents (intent_id, vault_id, payload, created_at_us) + VALUES (?, ?, ?, ?) + `) + .run(intent.intentId, intent.vaultId, JSON.stringify(intent), intent.createdAtUs); + } + + recordExecution(result: ExecutionResult) { + this.db + .prepare(` + INSERT OR REPLACE INTO executions (tx_hash, intent_id, payload, executed_at_us) + VALUES (?, ?, ?, ?) + `) + .run(result.txHash, result.intentId, JSON.stringify(result), result.executedAtUs); + } + + recordEvent(event: AuditEvent) { + this.db + .prepare(` + INSERT OR REPLACE INTO audit_events (event_id, category, payload, created_at_us) + VALUES (?, ?, ?, ?) + `) + .run( + event.eventId, + event.category, + JSON.stringify(event.payload), + event.createdAtUs, + ); + } + + listEvents(): AuditEvent[] { + const rows = this.db + .prepare( + `SELECT event_id, category, payload, created_at_us FROM audit_events ORDER BY created_at_us, event_id`, + ) + .all() as Array<{ + event_id: string; + category: AuditEvent["category"]; + payload: string; + created_at_us: number; + }>; + + return rows.map((row) => ({ + eventId: row.event_id, + category: row.category, + payload: JSON.parse(row.payload) as Record, + createdAtUs: row.created_at_us, + })); + } + + counts() { + const [snapshots] = this.db + .prepare(`SELECT COUNT(*) AS count FROM snapshots`) + .all() as Array<{ count: number }>; + const [intents] = this.db + .prepare(`SELECT COUNT(*) AS count FROM intents`) + .all() as Array<{ count: number }>; + const [executions] = this.db + .prepare(`SELECT COUNT(*) AS count FROM executions`) + .all() as Array<{ count: number }>; + const [events] = this.db + .prepare(`SELECT COUNT(*) AS count FROM audit_events`) + .all() as Array<{ count: number }>; + + return { + snapshots: snapshots?.count ?? 0, + intents: intents?.count ?? 0, + executions: executions?.count ?? 0, + events: events?.count ?? 0, + }; + } +} diff --git a/lazer/cardano/guards-one/source/apps/backend/tests/dashboard.test.ts b/lazer/cardano/guards-one/source/apps/backend/tests/dashboard.test.ts new file mode 100644 index 00000000..c1792d8d --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/tests/dashboard.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { createDemoState } from "../src/demo-state.js"; + +describe("dashboard payload", () => { + it("builds a UI payload from the simulated backend state", () => { + const state = createDemoState(); + + expect(state.payload.source).toBe("backend-demo"); + expect(state.payload.workspace.name).toContain("Treasury"); + expect(state.payload.topbarChips).toHaveLength(3); + expect(state.payload.heroMetrics).toHaveLength(4); + expect(state.payload.dashboardCards).toHaveLength(4); + expect(state.payload.chainCards).toHaveLength(3); + expect(state.payload.accounts).toHaveLength(2); + expect(state.payload.portfolioSeries).toHaveLength(4); + expect(state.payload.demoFrames).toHaveLength(4); + expect(state.payload.executionTimeline).toHaveLength(3); + expect(state.payload.auditTrail.length).toBeGreaterThan(0); + expect(state.operations.reentry.stage).toBe("normal"); + }); +}); diff --git a/lazer/cardano/guards-one/source/apps/backend/tests/e2e.test.ts b/lazer/cardano/guards-one/source/apps/backend/tests/e2e.test.ts new file mode 100644 index 00000000..d911b220 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/tests/e2e.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; +import { buildSnapshots } from "@anaconda/core"; +import { PythCollector } from "../src/collector.js"; +import { createDemoState } from "../src/demo-state.js"; +import { buildDemoScenario, sampleWitness } from "../src/fixtures.js"; +import { CardanoKeeperService } from "../src/keeper.js"; +import { AuditStore } from "../src/storage.js"; + +describe("backend e2e flow", () => { + it("runs partial de-risk, full exit, reentry and records an audit trail", () => { + const state = createDemoState(); + const { partial, fullExit, reentry } = state.operations; + + expect(partial.stage).toBe("partial_derisk"); + expect(partial.txHash).toBeDefined(); + expect(fullExit.stage).toBe("full_exit"); + expect(fullExit.txHash).toBeDefined(); + expect(reentry.stage).toBe("normal"); + expect(reentry.txHash).toBeDefined(); + expect(state.counts.snapshots).toBeGreaterThanOrEqual(6); + expect(state.counts.intents).toBe(3); + expect(state.counts.executions).toBe(3); + expect(state.counts.events).toBeGreaterThanOrEqual(9); + }); + + it("records a rejection when oracle freshness is invalid", () => { + const scenario = buildDemoScenario(); + const auditStore = new AuditStore(); + const collector = new PythCollector(auditStore); + const keeper = new CardanoKeeperService(sampleWitness.pythPolicyId, auditStore); + + for (const snapshot of Object.values( + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-stale", + feedUpdateTimestampUs: 1_000_000, + observedAtUs: 1_000_000, + }, + }), + )) { + collector.publish(snapshot); + } + + const result = keeper.tick({ + treasury: scenario.treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: collector.current(), + nowUs: 500_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + + expect(result.rejected).toBe("STALE_FEED"); + expect(auditStore.counts().events).toBeGreaterThanOrEqual(3); + }); + + it("records a rejection when no approved route exists for execution", () => { + const scenario = buildDemoScenario(); + const auditStore = new AuditStore(); + const collector = new PythCollector(auditStore); + const keeper = new CardanoKeeperService(sampleWitness.pythPolicyId, auditStore); + + for (const snapshot of Object.values(scenario.snapshots)) { + collector.publish(snapshot); + } + + const result = keeper.tick({ + treasury: scenario.treasury, + policy: { + ...scenario.policy, + approvedRouteIds: [], + }, + routes: scenario.routes, + snapshots: collector.current(), + nowUs: 200_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + + expect(result.rejected).toBe("NO_EXECUTABLE_INTENT"); + }); +}); diff --git a/lazer/cardano/guards-one/source/apps/backend/tests/storage.test.ts b/lazer/cardano/guards-one/source/apps/backend/tests/storage.test.ts new file mode 100644 index 00000000..47c037bb --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/backend/tests/storage.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { buildSnapshots } from "@anaconda/core"; +import { PythCollector } from "../src/collector.js"; +import { AuditStore } from "../src/storage.js"; + +describe("audit store and collector", () => { + it("records snapshots and returns event counts", () => { + const store = new AuditStore(); + const collector = new PythCollector(store); + const snapshots = buildSnapshots(); + + collector.publish(snapshots.ada!); + collector.publish(snapshots.usdm!); + + const current = collector.current(); + expect(current.ada!.snapshotId).toBe("snapshot-ada"); + expect(store.counts().snapshots).toBe(2); + expect(store.counts().events).toBe(2); + expect(store.listEvents()[0]?.category).toBe("snapshot"); + }); + + it("overwrites the latest snapshot per asset in collector state", () => { + const collector = new PythCollector(); + collector.publish(buildSnapshots().ada!); + collector.publish( + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-2", + observedAtUs: 2_000_000, + feedUpdateTimestampUs: 2_000_000, + }, + }).ada!, + ); + + expect(collector.current().ada!.snapshotId).toBe("snapshot-ada-2"); + }); + + it("orders audit events deterministically when timestamps tie", () => { + const store = new AuditStore(); + + store.recordEvent({ + eventId: "event-b", + category: "snapshot", + payload: { order: 2 }, + createdAtUs: 1_000_000, + }); + store.recordEvent({ + eventId: "event-a", + category: "snapshot", + payload: { order: 1 }, + createdAtUs: 1_000_000, + }); + + expect(store.listEvents().map((event) => event.eventId)).toEqual(["event-a", "event-b"]); + }); +}); diff --git a/lazer/cardano/guards-one/source/apps/ui/app.js b/lazer/cardano/guards-one/source/apps/ui/app.js new file mode 100644 index 00000000..117f9c75 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/ui/app.js @@ -0,0 +1,617 @@ +const fallbackState = { + source: "static-fallback", + workspace: { + name: "Anaconda Treasury Demo", + label: "guards.one live desk", + chain: "Cardano preprod", + stage: "Normal", + threshold: "3 governance signers", + members: "1 risk manager · 2 keepers", + hotWallet: "addr_test...hot_1", + governanceWallet: "addr_test...gov_1", + totalBalance: "$6,646", + primaryAsset: "ADA", + stableAsset: "USDM", + vaultId: "vault-anaconda-demo", + }, + topbarChips: [ + { label: "Network status", value: "Cardano preprod live", tone: "live" }, + { label: "Approved route", value: "Minswap · USDM", tone: "neutral" }, + { label: "Execution wallet", value: "addr_tes...hot_1", tone: "neutral" }, + ], + heroMetrics: [ + { + label: "Current stage", + value: "Normal", + copy: "Final policy state after the simulated breach and recovery run.", + chip: "ok", + }, + { + label: "Treasury liquid value", + value: "$6,646", + copy: "Valued from the latest Pyth price and haircut-aware liquid value math.", + }, + { + label: "Stable protection", + value: "36.0%", + copy: "Current treasury share parked in the approved stable reserve.", + }, + { + label: "Oracle freshness", + value: "20s", + copy: "Age of the primary feed update relative to the latest policy evaluation.", + }, + ], + dashboardCards: [ + { + label: "Protected floor", + value: "$4,500", + copy: "Minimum fiat-equivalent value the policy tries to keep defended at all times.", + chip: "ok", + }, + { + label: "Emergency floor", + value: "$3,400", + copy: "Crossing this floor escalates the vault into a full stable exit or freeze path.", + chip: "danger", + }, + { + label: "Primary reason", + value: "Cooldown prevents an automatic state transition", + copy: "Top reason emitted by the risk engine for the current snapshot set.", + chip: "warn", + }, + { + label: "Execution policy", + value: "CARDANO · Minswap · USDM", + copy: "Only allowlisted routes can spend from the execution hot wallet.", + chip: "ok", + }, + ], + chainCards: [ + { + chain: "CARDANO", + title: "Live execution surface", + copy: "Cardano is the only live execution target in the MVP. Execution uses a two-step authorize-and-swap flow.", + chip: "live", + }, + { + chain: "SVM", + title: "Scaffolded adapter", + copy: "Scaffolding only in the MVP. Designed to share the same policy and role model.", + chip: "scaffold", + }, + { + chain: "EVM", + title: "Scaffolded adapter", + copy: "Scaffolding only in the MVP. Connectors remain simulation-first until phase 2.", + chip: "scaffold", + }, + ], + riskLadder: [ + { + stage: "Normal", + title: "Operate with full permissions", + copy: "Fresh oracle, healthy confidence, and no forced action path active.", + }, + { + stage: "Watch", + title: "Increase monitoring", + copy: "The drawdown or fiat floor approaches the first trigger band.", + tone: "partial", + }, + { + stage: "Partial De-Risk", + title: "Sell only what restores the safe floor", + copy: "The keeper sells a bounded slice of the risky bucket into the approved stable route.", + tone: "partial", + }, + { + stage: "Full Stable Exit", + title: "Move the vault fully defensive", + copy: "A deeper breach exits the risk bucket and keeps the hot wallet on one stable rail.", + tone: "full", + }, + { + stage: "Auto Re-entry", + title: "Re-risk only after hysteresis clears", + copy: "Recovery must clear a separate band and cooldown before exposure comes back.", + tone: "reentry", + }, + ], + executionTimeline: [ + { + title: "Step 1 · Partial De-Risk", + copy: "Intent intent-1 anchored and settled in tx tx-1.", + status: "executed", + }, + { + title: "Step 2 · Full Stable Exit", + copy: "Intent intent-2 anchored and settled in tx tx-2.", + status: "executed", + }, + { + title: "Step 3 · Normal", + copy: "Intent intent-3 anchored and settled in tx tx-3.", + status: "executed", + }, + ], + auditTrail: [ + { + title: "EXECUTION · tx-915d", + copy: "Settled normal. Sold 4,440 and bought 5,553.", + stamp: "0s", + }, + ], + accounts: [ + { + label: "ADA risk bucket", + address: "addr_tes...hot_1", + balance: "5,553 ADA", + fiatValue: "$4,266", + weight: "64.2%", + role: "Risk asset", + bucket: "Execution hot", + }, + { + label: "USDM stable reserve", + address: "addr_tes...hot_1", + balance: "2,393 USDM", + fiatValue: "$2,393", + weight: "35.8%", + role: "Stable reserve", + bucket: "Execution hot", + }, + ], + portfolioSeries: [ + { label: "Watch", stage: "watch", value: 8400, displayValue: "$8,400" }, + { label: "Partial", stage: "partial_derisk", value: 7310, displayValue: "$7,310" }, + { label: "Exit", stage: "full_exit", value: 4580, displayValue: "$4,580" }, + { label: "Recovery", stage: "normal", value: 6646, displayValue: "$6,646" }, + ], + demoFrames: [ + { + label: "01", + title: "Watchlist breach detected", + copy: "ADA slips under its EMA while Pyth freshness and confidence remain healthy enough to authorize a policy action.", + stage: "watch", + balance: "$8,400", + stableRatio: "17.9%", + reason: "ADA liquid value fell below its protected floor", + }, + { + label: "02", + title: "Partial de-risk executes", + copy: "The keeper emits an intent, swaps only the bounded amount needed, and restores the defended stable floor.", + stage: "partial_derisk", + balance: "$7,310", + stableRatio: "44.0%", + reason: "Partial stable target restored.", + }, + { + label: "03", + title: "Full stable exit after the second leg down", + copy: "A deeper price break and thinner asset cushion push the vault into a full defensive configuration on the approved stable route.", + stage: "full_exit", + balance: "$4,580", + stableRatio: "100.0%", + reason: "Emergency floor forced the full stable path.", + }, + { + label: "04", + title: "Auto re-entry restores exposure", + copy: "Recovery clears the hysteresis band, cooldown expires, and the treasury re-enters risk according to the configured target ratio.", + stage: "normal", + balance: "$6,646", + stableRatio: "36.0%", + reason: "Recovery cleared the re-entry guardrails.", + }, + ], +}; + +const workspaceCard = document.querySelector("#workspace-card"); +const topbarChips = document.querySelector("#topbar-chips"); +const heroMetrics = document.querySelector("#hero-metrics"); +const dashboardCards = document.querySelector("#dashboard-cards"); +const chainList = document.querySelector("#chain-list"); +const ladderList = document.querySelector("#risk-ladder-list"); +const executionTimeline = document.querySelector("#execution-timeline"); +const auditTrail = document.querySelector("#audit-trail"); +const accountsBody = document.querySelector("#accounts-body"); +const overviewStage = document.querySelector("#overview-stage"); +const overviewBalance = document.querySelector("#overview-balance"); +const overviewCopy = document.querySelector("#overview-copy"); +const demoTitle = document.querySelector("#demo-title"); +const demoStage = document.querySelector("#demo-stage"); +const demoCopy = document.querySelector("#demo-copy"); +const frameStrip = document.querySelector("#frame-strip"); +const portfolioChart = document.querySelector("#portfolio-chart"); +const frameBalance = document.querySelector("#frame-balance"); +const frameStableRatio = document.querySelector("#frame-stable-ratio"); +const frameReason = document.querySelector("#frame-reason"); +const replayButton = document.querySelector("#replay-button"); + +const allowedTones = new Set(["ok", "warn", "danger", "live", "neutral", "executed", "rejected"]); +const allowedStages = new Set(["normal", "watch", "partial_derisk", "full_exit", "frozen"]); +const allowedLadderTones = new Set(["", "partial", "full", "reentry"]); +const stageFramesToLadderCount = [2, 3, 4, 5]; +let replayTimer = null; +let currentState = fallbackState; +let activeFrameIndex = Math.max(0, fallbackState.demoFrames.length - 1); + +async function loadState() { + const candidates = ["/api/demo-state", "./data/demo-state.json"]; + + for (const url of candidates) { + try { + const response = await fetch(url); + if (response.ok) { + return await response.json(); + } + } catch { + // Try the next source. + } + } + + return fallbackState; +} + +function escapeHTML(value) { + if (value === null || value === undefined) { + return ""; + } + + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function safeToken(value, allowed, fallback) { + return allowed.has(value) ? value : fallback; +} + +function toneForStage(stage) { + switch (safeToken(stage, allowedStages, "watch")) { + case "normal": + return "ok"; + case "watch": + return "warn"; + case "partial_derisk": + return "warn"; + case "full_exit": + return "danger"; + case "frozen": + return "danger"; + default: + return "neutral"; + } +} + +function humanizeStage(stage) { + return String(stage).replaceAll("_", " "); +} + +function renderWorkspace(workspace) { + return ` +
+
G1
+
+

${escapeHTML(workspace.label)}

+ ${escapeHTML(workspace.name)} +
+
+
+ Total balance + ${escapeHTML(workspace.totalBalance)} +
+
+
+ Stage + ${escapeHTML(workspace.stage)} +
+
+ Chain + ${escapeHTML(workspace.chain)} +
+
+ Primary + ${escapeHTML(workspace.primaryAsset)} +
+
+ Stable + ${escapeHTML(workspace.stableAsset)} +
+
+
+

${escapeHTML(workspace.threshold)}

+

${escapeHTML(workspace.members)}

+

Hot wallet ${escapeHTML(workspace.hotWallet)}

+

Vault ${escapeHTML(workspace.vaultId)}

+
+ `; +} + +function renderTopbarChip({ label, value, tone }) { + const safeTone = safeToken(tone, allowedTones, "neutral"); + return ` +
+ ${escapeHTML(label)} + ${escapeHTML(value)} +
+ `; +} + +function renderMetric({ label, value, copy, chip }) { + const safeChip = safeToken(chip ?? "neutral", allowedTones, "neutral"); + return ` +
+
+ ${escapeHTML(label)} + ${chip ? `` : ""} +
+ ${escapeHTML(value)} +

${escapeHTML(copy)}

+
+ `; +} + +function renderPolicyCard({ label, value, copy, chip }) { + const safeChip = safeToken(chip, allowedTones, "neutral"); + return ` +
+
+ ${escapeHTML(label)} + +
+ ${escapeHTML(value)} +

${escapeHTML(copy)}

+
+ `; +} + +function renderChain({ chain, title, copy, chip }) { + const safeChip = chip === "live" ? "live" : "warn"; + return ` +
+
+ ${escapeHTML(chain)} + ${escapeHTML(chip === "live" ? "Live" : "Scaffold")} +
+ ${escapeHTML(title)} +

${escapeHTML(copy)}

+
+ `; +} + +function renderLadder({ stage, title, copy, tone = "" }, index) { + const safeTone = safeToken(tone ?? "", allowedLadderTones, ""); + const toneClass = safeTone ? ` ladder-${safeTone}` : ""; + return ` +
+ ${escapeHTML(stage)} + ${escapeHTML(title)} +

${escapeHTML(copy)}

+
+ `; +} + +function renderTimelineItem({ title, copy, status }, index) { + const safeStatus = status === "rejected" ? "rejected" : "executed"; + return ` +
+
+ ${escapeHTML(title)} + ${escapeHTML(safeStatus)} +
+

${escapeHTML(copy)}

+
+ `; +} + +function renderAuditItem({ title, copy, stamp }) { + return ` +
+
+ ${escapeHTML(title)} + ${escapeHTML(stamp)} ago +
+

${escapeHTML(copy)}

+
+ `; +} + +function renderAccountRow({ label, address, balance, fiatValue, weight, role, bucket }) { + return ` + + + + + ${escapeHTML(balance)} + ${escapeHTML(fiatValue)} + ${escapeHTML(weight)} + + `; +} + +function renderFramePill(frame, index, activeIndex) { + const safeTone = toneForStage(frame.stage); + const activeClass = index === activeIndex ? " active" : ""; + return ` + + `; +} + +function renderChart(series, activeIndex) { + if (!Array.isArray(series) || series.length === 0) { + return ""; + } + + const width = 640; + const height = 280; + const padX = 36; + const padTop = 18; + const padBottom = 44; + const values = series.map((point) => Number(point.value) || 0); + const min = Math.min(...values); + const max = Math.max(...values); + const span = Math.max(1, max - min); + const innerWidth = width - padX * 2; + const innerHeight = height - padTop - padBottom; + + const x = (index) => padX + (innerWidth * index) / Math.max(1, series.length - 1); + const y = (value) => padTop + (1 - (value - min) / span) * innerHeight; + + const linePath = series + .map((point, index) => `${index === 0 ? "M" : "L"} ${x(index).toFixed(2)} ${y(point.value).toFixed(2)}`) + .join(" "); + const areaPath = `${linePath} L ${x(series.length - 1).toFixed(2)} ${(height - padBottom).toFixed(2)} L ${x(0).toFixed(2)} ${(height - padBottom).toFixed(2)} Z`; + + const grid = Array.from({ length: 3 }, (_, index) => { + const yPos = padTop + (innerHeight * index) / 2; + return ``; + }).join(""); + + const points = series + .map((point, index) => { + const tone = toneForStage(point.stage); + const activeClass = index === activeIndex ? " active" : ""; + return ` + + + ${escapeHTML(point.label)} + + `; + }) + .join(""); + + const activeValue = series[activeIndex]?.displayValue ?? ""; + const activeX = x(activeIndex); + const activeY = y(series[activeIndex]?.value ?? 0); + + return ` + + + + + + + ${grid} + + + ${points} + + + ${escapeHTML(activeValue)} + + `; +} + +function setStagePill(element, label, stage) { + const tone = toneForStage(stage); + element.textContent = label; + element.className = `status-pill tone-${tone}`; +} + +function applyFrame(state, index) { + const frames = Array.isArray(state.demoFrames) ? state.demoFrames : []; + if (frames.length === 0) { + return; + } + + activeFrameIndex = Math.max(0, Math.min(index, frames.length - 1)); + const frame = frames[activeFrameIndex]; + demoTitle.textContent = frame.title; + demoCopy.textContent = frame.copy; + setStagePill(demoStage, humanizeStage(frame.stage), frame.stage); + frameBalance.textContent = frame.balance; + frameStableRatio.textContent = frame.stableRatio; + frameReason.textContent = frame.reason; + frameStrip.innerHTML = frames.map((item, frameIndex) => renderFramePill(item, frameIndex, activeFrameIndex)).join(""); + portfolioChart.innerHTML = renderChart(state.portfolioSeries ?? [], activeFrameIndex); + + const timelineItems = executionTimeline.querySelectorAll(".timeline-item"); + timelineItems.forEach((item, timelineIndex) => { + item.classList.toggle("is-complete", timelineIndex < activeFrameIndex - 1); + item.classList.toggle("is-active", timelineIndex === activeFrameIndex - 1); + }); + + const ladderCards = ladderList.querySelectorAll(".ladder-card"); + const activeCount = stageFramesToLadderCount[Math.min(activeFrameIndex, stageFramesToLadderCount.length - 1)] ?? ladderCards.length; + ladderCards.forEach((item, ladderIndex) => { + item.classList.toggle("is-active", ladderIndex < activeCount); + }); +} + +function startReplay() { + if (replayTimer) { + clearInterval(replayTimer); + replayTimer = null; + } + + applyFrame(currentState, 0); + replayTimer = window.setInterval(() => { + const nextIndex = activeFrameIndex + 1; + if (nextIndex >= currentState.demoFrames.length) { + clearInterval(replayTimer); + replayTimer = null; + return; + } + + applyFrame(currentState, nextIndex); + }, 1400); +} + +function renderState(state) { + currentState = state; + workspaceCard.innerHTML = renderWorkspace(state.workspace); + topbarChips.innerHTML = state.topbarChips.map(renderTopbarChip).join(""); + heroMetrics.innerHTML = state.heroMetrics.map(renderMetric).join(""); + dashboardCards.innerHTML = state.dashboardCards.map(renderPolicyCard).join(""); + chainList.innerHTML = state.chainCards.map(renderChain).join(""); + ladderList.innerHTML = state.riskLadder.map(renderLadder).join(""); + executionTimeline.innerHTML = state.executionTimeline.map(renderTimelineItem).join(""); + auditTrail.innerHTML = (state.auditTrail ?? []).map(renderAuditItem).join(""); + accountsBody.innerHTML = state.accounts.map(renderAccountRow).join(""); + + overviewBalance.textContent = state.workspace.totalBalance; + overviewCopy.textContent = state.dashboardCards[2]?.value ?? "Policy is currently inside the safe band."; + setStagePill(overviewStage, state.workspace.stage, state.demoFrames.at(-1)?.stage ?? "normal"); + applyFrame(state, Math.max(0, state.demoFrames.length - 1)); +} + +frameStrip.addEventListener("click", (event) => { + const target = event.target.closest("[data-frame-index]"); + if (!(target instanceof HTMLElement)) { + return; + } + + const index = Number(target.dataset.frameIndex); + if (!Number.isFinite(index)) { + return; + } + + if (replayTimer) { + clearInterval(replayTimer); + replayTimer = null; + } + + applyFrame(currentState, index); +}); + +replayButton.addEventListener("click", () => { + startReplay(); +}); + +loadState().then((state) => { + renderState(state); +}); diff --git a/lazer/cardano/guards-one/source/apps/ui/data/demo-state.json b/lazer/cardano/guards-one/source/apps/ui/data/demo-state.json new file mode 100644 index 00000000..726f5b53 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/ui/data/demo-state.json @@ -0,0 +1,268 @@ +{ + "generatedAtUs": 360000000, + "source": "backend-demo", + "workspace": { + "name": "Anaconda Treasury Demo", + "label": "guards.one live desk", + "chain": "Cardano preprod", + "stage": "Normal", + "threshold": "3 governance signers", + "members": "1 risk manager · 2 keepers", + "hotWallet": "addr_tes..._hot_1", + "governanceWallet": "addr_tes..._gov_1", + "totalBalance": "$6,646", + "primaryAsset": "ADA", + "stableAsset": "USDM", + "vaultId": "vault-anaconda-demo" + }, + "topbarChips": [ + { + "label": "Network status", + "value": "Cardano preprod live", + "tone": "live" + }, + { + "label": "Approved route", + "value": "Minswap · USDM", + "tone": "neutral" + }, + { + "label": "Execution wallet", + "value": "addr_tes..._hot_1", + "tone": "neutral" + } + ], + "heroMetrics": [ + { + "label": "Current stage", + "value": "Normal", + "copy": "Final policy state after the simulated breach and recovery run.", + "chip": "ok" + }, + { + "label": "Treasury liquid value", + "value": "$6,646", + "copy": "Valued from the latest Pyth price and haircut-aware liquid value math." + }, + { + "label": "Stable protection", + "value": "36.0%", + "copy": "Current treasury share parked in the approved stable reserve." + }, + { + "label": "Oracle freshness", + "value": "20s", + "copy": "Age of the primary feed update relative to the latest policy evaluation." + } + ], + "dashboardCards": [ + { + "label": "Protected floor", + "value": "$4,500", + "copy": "Minimum fiat-equivalent value the policy tries to keep defended at all times.", + "chip": "ok" + }, + { + "label": "Emergency floor", + "value": "$3,400", + "copy": "Crossing this floor escalates the vault into a full stable exit or freeze path.", + "chip": "danger" + }, + { + "label": "Primary reason", + "value": "Cooldown prevents an automatic state transition", + "copy": "Top reason emitted by the risk engine for the current snapshot set.", + "chip": "warn" + }, + { + "label": "Execution policy", + "value": "CARDANO · Minswap · USDM", + "copy": "Only allowlisted routes can spend from the execution hot wallet.", + "chip": "ok" + } + ], + "chainCards": [ + { + "chain": "CARDANO", + "title": "Live execution surface", + "copy": "Cardano is the only live execution target in the MVP. Execution uses a two-step authorize-and-swap flow.", + "chip": "live" + }, + { + "chain": "SVM", + "title": "Scaffolded adapter", + "copy": "Scaffolding only in the MVP. Designed to share the same policy and role model.", + "chip": "scaffold" + }, + { + "chain": "EVM", + "title": "Scaffolded adapter", + "copy": "Scaffolding only in the MVP. Connectors remain simulation-first until phase 2.", + "chip": "scaffold" + } + ], + "riskLadder": [ + { + "stage": "Normal", + "title": "Operate with full permissions", + "copy": "Fresh oracle, healthy confidence, and no forced action path active." + }, + { + "stage": "Watch", + "title": "Increase monitoring", + "copy": "The drawdown or fiat floor approaches the first trigger band.", + "tone": "partial" + }, + { + "stage": "Partial De-Risk", + "title": "Sell only what restores the safe floor", + "copy": "The keeper sells a bounded slice of the risky bucket into the approved stable route.", + "tone": "partial" + }, + { + "stage": "Full Stable Exit", + "title": "Move the vault fully defensive", + "copy": "A deeper breach exits the risk bucket and keeps the hot wallet on one stable rail.", + "tone": "full" + }, + { + "stage": "Auto Re-entry", + "title": "Re-risk only after hysteresis clears", + "copy": "Recovery must clear a separate band and cooldown before exposure comes back.", + "tone": "reentry" + } + ], + "executionTimeline": [ + { + "title": "Step 1 · Partial De-Risk", + "copy": "Intent 815ede7a-6cf0-49fe-939f-262dfa1b6a4c anchored and settled in tx tx-8eb5d881-9efe-4ae5-9cb8-11d7a5d06424.", + "status": "executed" + }, + { + "title": "Step 2 · Full Stable Exit", + "copy": "Intent 27535a6c-6d30-4a06-9e8b-655c620913c8 anchored and settled in tx tx-b713b68d-6d8e-42d3-8ec6-f20e66c0a61f.", + "status": "executed" + }, + { + "title": "Step 3 · Normal", + "copy": "Intent 8205bd55-2e1e-452d-b0ca-5c1889e371d4 anchored and settled in tx tx-41e2236d-9cd7-4f9b-958b-b9de83246025.", + "status": "executed" + } + ], + "auditTrail": [ + { + "title": "INTENT · intent:27535a6c-6d30-4a06-9e8b-655c620913c8", + "copy": "Authorized derisk swap at stage full exit.", + "stamp": "100s" + }, + { + "title": "EXECUTION · execution:tx-b713b68d-6d8e-42d3-8ec6-f20e66c0a61f", + "copy": "Settled full exit. Sold 5,466 and bought 2,106.", + "stamp": "100s" + }, + { + "title": "SNAPSHOT · snapshot:snapshot-ada-recovery", + "copy": "Observed ADA from snapshot-ada-recovery.", + "stamp": "20s" + }, + { + "title": "SNAPSHOT · snapshot:snapshot-usdm-3", + "copy": "Observed USDM from snapshot-usdm-3.", + "stamp": "20s" + }, + { + "title": "INTENT · intent:8205bd55-2e1e-452d-b0ca-5c1889e371d4", + "copy": "Authorized reentry swap at stage normal.", + "stamp": "0s" + }, + { + "title": "EXECUTION · execution:tx-41e2236d-9cd7-4f9b-958b-b9de83246025", + "copy": "Settled normal. Sold 4,440 and bought 5,553.", + "stamp": "0s" + } + ], + "accounts": [ + { + "label": "ADA risk bucket", + "address": "addr_tes..._hot_1", + "balance": "5,553 ADA", + "fiatValue": "$4,255", + "weight": "64.0%", + "role": "Risk asset", + "bucket": "Execution hot" + }, + { + "label": "USDM stable reserve", + "address": "addr_tes..._hot_1", + "balance": "2,391 USDM", + "fiatValue": "$2,391", + "weight": "36.0%", + "role": "Stable reserve", + "bucket": "Execution hot" + } + ], + "portfolioSeries": [ + { + "label": "Watch", + "stage": "watch", + "value": 8775, + "displayValue": "$8,775" + }, + { + "label": "Partial", + "stage": "partial_derisk", + "value": 8542.754226799423, + "displayValue": "$8,543" + }, + { + "label": "Exit", + "stage": "full_exit", + "value": 6831.30402061, + "displayValue": "$6,831" + }, + { + "label": "Recovery", + "stage": "normal", + "value": 6646.407945987443, + "displayValue": "$6,646" + } + ], + "demoFrames": [ + { + "label": "01", + "title": "Watchlist breach detected", + "copy": "ADA slips under its EMA while Pyth freshness and confidence remain healthy enough to authorize a policy action.", + "stage": "watch", + "balance": "$8,775", + "stableRatio": "17.1%", + "reason": "Primary asset drawdown crossed watch threshold" + }, + { + "label": "02", + "title": "Partial de-risk executes", + "copy": "The keeper emits an intent, swaps only the bounded amount needed, and restores the defended stable floor.", + "stage": "partial_derisk", + "balance": "$8,543", + "stableRatio": "55.3%", + "reason": "Primary asset drawdown crossed watch threshold" + }, + { + "label": "03", + "title": "Full stable exit after the second leg down", + "copy": "A deeper price break and thinner asset cushion push the vault into a full defensive configuration on the approved stable route.", + "stage": "full_exit", + "balance": "$6,831", + "stableRatio": "100.0%", + "reason": "Primary asset drawdown crossed watch threshold" + }, + { + "label": "04", + "title": "Auto re-entry restores exposure", + "copy": "Recovery clears the hysteresis band, cooldown expires, and the treasury re-enters risk according to the configured target ratio.", + "stage": "normal", + "balance": "$6,646", + "stableRatio": "36.0%", + "reason": "Cooldown prevents an automatic state transition" + } + ] +} diff --git a/lazer/cardano/guards-one/source/apps/ui/index.html b/lazer/cardano/guards-one/source/apps/ui/index.html new file mode 100644 index 00000000..51321e13 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/ui/index.html @@ -0,0 +1,203 @@ + + + + + + + guards.one + + + + + + +
+ + +
+
+
+

Cardano live · Pyth-backed controls

+

Treasury Control Center

+
+
+
+ +
+
+
+
+
+

Overview

+

Liquid treasury value

+
+ +
+ +
+
+

Current total balance

+
+

+
+
+ + Open runbook + Read spec +
+
+ +
+
+ +
+
+
+

Simulation

+

Demo frame

+
+ +
+ +

+
+
+ +
+
+
+ Frame balance + +
+
+ Stable ratio + +
+
+ Trigger + +
+
+
+
+ +
+
+
+
+

Accounts

+

Hot and cold treasury buckets

+
+
+
+ + + + + + + + + + +
AccountBalanceFiat valueWeight
+
+
+ +
+
+
+
+

Policy

+

Guardrails

+
+
+
+
+ +
+
+
+

Multichain

+

Execution surfaces

+
+
+
+
+
+
+ +
+
+
+
+

Risk ladder

+

Escalation path

+
+
+
+
+ +
+
+
+

Execution

+

Runbook timeline

+
+
+
+
+
+ +
+
+
+

Audit log

+

Deterministic replay trail

+
+
+
+
+
+
+
+ + + + diff --git a/lazer/cardano/guards-one/source/apps/ui/styles.css b/lazer/cardano/guards-one/source/apps/ui/styles.css new file mode 100644 index 00000000..46c703e9 --- /dev/null +++ b/lazer/cardano/guards-one/source/apps/ui/styles.css @@ -0,0 +1,881 @@ +:root { + color-scheme: dark; + --bg: #17181d; + --bg-soft: #1c1e25; + --panel: #212329; + --panel-2: #272932; + --panel-3: #1b1d24; + --line: #31343d; + --line-soft: #292c36; + --text: #f4f2ec; + --muted: #9b9ca4; + --muted-strong: #c6c8ce; + --green: #22c55e; + --yellow: #f0bf5f; + --red: #ef6f6c; + --white-chip: #f6f3ee; + --shadow: 0 20px 56px rgba(0, 0, 0, 0.28); +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; + background: + radial-gradient(circle at top right, rgba(255, 255, 255, 0.06), transparent 30%), + radial-gradient(circle at top left, rgba(255, 255, 255, 0.03), transparent 20%), + linear-gradient(180deg, #18191f 0%, #15161b 100%); + color: var(--text); + font-family: "Manrope", system-ui, sans-serif; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent 22%); + opacity: 0.4; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +a { + transition: 160ms ease; +} + +button { + font: inherit; +} + +.app-shell { + display: grid; + grid-template-columns: 308px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + position: sticky; + top: 0; + align-self: start; + min-height: 100vh; + padding: 28px 20px; + border-right: 1px solid var(--line-soft); + background: rgba(22, 24, 31, 0.9); + backdrop-filter: blur(18px); + display: grid; + gap: 18px; +} + +.brand { + display: flex; + align-items: center; + gap: 14px; + padding: 4px 10px 16px; +} + +.brand-mark { + width: 40px; + height: 40px; + border-radius: 14px; + background: linear-gradient(180deg, #fcfaf6, #d8d5cd); + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06); +} + +.brand-mark span { + width: 18px; + height: 18px; + border: 2px solid #1a1b20; + border-radius: 5px; +} + +.brand-copy { + display: grid; + gap: 2px; +} + +.brand-copy strong { + font-size: 1.15rem; + letter-spacing: -0.04em; +} + +.brand-copy small, +.side-label, +.workspace-kicker, +.panel-eyebrow, +.eyebrow, +.audit-stamp, +.frame-label, +.ladder-stage, +.metric-top span, +.card-top span, +.summary-block span, +.account-cell span, +.workspace-balance-row span, +.workspace-meta-grid span, +.workspace-foot p, +th { + font-family: "IBM Plex Mono", monospace; +} + +.brand-copy small, +.side-label, +.workspace-kicker, +.panel-eyebrow, +.eyebrow, +.audit-stamp, +.frame-label, +.ladder-stage, +.metric-top span, +.card-top span, +.summary-block span, +.account-cell span, +.workspace-balance-row span, +.workspace-meta-grid span, +.workspace-foot p { + color: var(--muted); + font-size: 0.72rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.panel, +.panel-soft { + background: linear-gradient(180deg, rgba(35, 37, 45, 0.96), rgba(29, 31, 38, 0.96)); + border: 1px solid var(--line); + border-radius: 24px; + box-shadow: var(--shadow); +} + +.panel-soft { + padding: 18px; + box-shadow: none; +} + +.workspace-card { + display: grid; + gap: 16px; +} + +.workspace-head { + display: flex; + align-items: center; + gap: 14px; +} + +.workspace-avatar { + width: 52px; + height: 52px; + border-radius: 16px; + background: linear-gradient(180deg, #f8f5ef, #d5d2cb); + color: #17181d; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 800; + letter-spacing: -0.04em; +} + +.workspace-head strong, +.workspace-balance-row strong, +.workspace-meta-grid strong, +.workspace-balance, +.balance-value, +.metric-card strong, +.policy-card strong, +.chain-card strong, +.ladder-card strong, +.summary-block strong, +.account-cell strong, +.timeline-item strong, +.audit-item strong, +.topbar h1, +.balance-panel h2, +.chart-panel h2, +.panel h2 { + letter-spacing: -0.04em; +} + +.workspace-balance-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 0 10px; + border-top: 1px solid var(--line-soft); + border-bottom: 1px solid var(--line-soft); +} + +.workspace-balance-row strong { + font-size: 1.8rem; +} + +.workspace-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.workspace-meta-grid article { + padding: 12px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--line-soft); + display: grid; + gap: 6px; +} + +.workspace-foot { + display: grid; + gap: 8px; +} + +.workspace-foot p { + margin: 0; +} + +.sidebar-nav { + display: grid; + gap: 10px; +} + +.nav-item { + min-height: 52px; + padding: 0 16px; + border-radius: 18px; + border: 1px solid transparent; + display: flex; + align-items: center; + color: var(--muted-strong); + background: transparent; + font-weight: 600; +} + +.nav-item:hover, +.nav-item:focus-visible, +.nav-item.active { + background: var(--panel-3); + border-color: var(--line); + color: var(--text); +} + +.sidebar-foot { + align-self: end; +} + +.doc-links { + display: grid; + gap: 10px; +} + +.doc-links a { + padding: 12px 14px; + border-radius: 14px; + border: 1px solid var(--line-soft); + background: rgba(255, 255, 255, 0.03); + color: var(--muted-strong); +} + +.doc-links a:hover, +.doc-links a:focus-visible { + color: var(--text); + border-color: #484b56; +} + +.workspace { + padding: 24px 28px 36px; +} + +.topbar { + display: flex; + justify-content: space-between; + align-items: start; + gap: 18px; + margin-bottom: 22px; +} + +.eyebrow { + margin: 0 0 10px; +} + +.topbar h1 { + margin: 0; + font-size: clamp(2.15rem, 3.8vw, 3.5rem); +} + +.topbar-chips { + display: flex; + flex-wrap: wrap; + justify-content: end; + gap: 12px; +} + +.status-chip, +.status-pill, +.chip { + display: inline-flex; + align-items: center; + gap: 10px; + border-radius: 16px; + border: 1px solid var(--line); +} + +.status-chip { + min-height: 52px; + padding: 0 18px; + background: var(--panel-3); +} + +.status-chip strong { + font-size: 1rem; +} + +.status-chip span { + color: var(--muted); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.status-pill, +.chip { + min-height: 34px; + padding: 0 12px; + background: rgba(255, 255, 255, 0.04); + font-size: 0.84rem; + font-weight: 700; +} + +.status-pill.subtle { + background: var(--panel-3); +} + +.tone-live::before, +.status-pill::before, +.mini-dot { + content: ""; + width: 8px; + height: 8px; + border-radius: 999px; + background: currentColor; + display: inline-block; +} + +.tone-live { + color: var(--green); +} + +.tone-neutral { + color: var(--muted-strong); +} + +.tone-ok { + color: var(--green); +} + +.tone-warn { + color: var(--yellow); +} + +.tone-danger { + color: var(--red); +} + +.content { + display: grid; + gap: 20px; +} + +.overview-grid, +.content-grid, +.split-grid { + display: grid; + gap: 20px; +} + +.overview-grid { + grid-template-columns: 1.15fr 1fr; +} + +.content-grid { + grid-template-columns: 1.2fr 0.88fr; +} + +.split-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.side-stack { + display: grid; + gap: 20px; +} + +.panel { + padding: 24px; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: start; + gap: 18px; + margin-bottom: 22px; +} + +.panel-header.compact { + margin-bottom: 18px; +} + +.panel-header h2, +.balance-panel h2, +.chart-panel h2 { + margin: 0; + font-size: clamp(1.35rem, 2.4vw, 2rem); +} + +.panel-copy, +.balance-copy, +.metric-card p, +.policy-card p, +.chain-card p, +.ladder-card p, +.timeline-item p, +.audit-item p { + margin: 0; + color: var(--muted); + line-height: 1.6; +} + +.balance-row { + display: flex; + justify-content: space-between; + align-items: end; + gap: 20px; + padding-bottom: 22px; + border-bottom: 1px solid var(--line-soft); +} + +.balance-label { + margin: 0 0 8px; + color: var(--muted); +} + +.balance-value { + font-size: clamp(2.8rem, 5vw, 4.6rem); + font-weight: 800; + line-height: 0.95; +} + +.action-row { + display: flex; + flex-wrap: wrap; + justify-content: end; + gap: 12px; +} + +.button { + min-height: 48px; + padding: 0 18px; + border-radius: 16px; + border: 1px solid transparent; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; + cursor: pointer; +} + +.button-primary { + background: var(--white-chip); + color: #17181d; +} + +.button-secondary { + background: rgba(255, 255, 255, 0.03); + border-color: var(--line); + color: var(--text); +} + +.button:hover, +.button:focus-visible { + transform: translateY(-1px); + border-color: #555966; +} + +.metric-grid, +.policy-grid, +.chain-list, +.ladder-list, +.timeline-list, +.audit-list { + display: grid; + gap: 14px; +} + +.metric-grid { + margin-top: 22px; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.metric-card, +.policy-card, +.chain-card, +.ladder-card, +.timeline-item, +.audit-item { + padding: 18px; + border-radius: 18px; + border: 1px solid var(--line-soft); + background: rgba(255, 255, 255, 0.03); +} + +.metric-card { + min-height: 142px; + display: grid; + align-content: space-between; + gap: 10px; +} + +.metric-card strong { + font-size: 1.55rem; +} + +.metric-top, +.card-top { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.policy-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.policy-card, +.chain-card, +.ladder-card, +.timeline-item, +.audit-item { + display: grid; + gap: 10px; +} + +.policy-card strong, +.chain-card strong, +.ladder-card strong, +.timeline-item strong, +.audit-item strong { + font-size: 1.06rem; +} + +.frame-strip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 18px; +} + +.frame-pill { + width: 100%; + padding: 14px; + border-radius: 18px; + border: 1px solid var(--line-soft); + background: rgba(255, 255, 255, 0.03); + color: inherit; + text-align: left; + display: grid; + gap: 6px; + cursor: pointer; +} + +.frame-pill strong { + font-size: 0.96rem; + letter-spacing: -0.03em; +} + +.frame-pill small { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.frame-pill.active, +.frame-pill:hover, +.frame-pill:focus-visible { + border-color: #5a5e69; + background: rgba(255, 255, 255, 0.06); +} + +.chart-shell { + min-height: 280px; + border-radius: 22px; + border: 1px solid var(--line-soft); + background: + radial-gradient(circle at top center, rgba(255, 255, 255, 0.05), transparent 36%), + linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01)); + padding: 8px; + overflow: hidden; +} + +#portfolio-chart { + width: 100%; + height: 280px; + display: block; +} + +.chart-grid-line { + stroke: rgba(255, 255, 255, 0.1); + stroke-width: 1; +} + +.chart-area { + fill: url(#chartAreaGradient); +} + +.chart-line { + fill: none; + stroke: rgba(255, 255, 255, 0.9); + stroke-width: 2.6; + stroke-linecap: round; + stroke-linejoin: round; +} + +.chart-point { + stroke: #17181d; + stroke-width: 3; +} + +.chart-point.tone-ok, +.chart-point.active.tone-ok { + fill: var(--green); +} + +.chart-point.tone-warn, +.chart-point.active.tone-warn { + fill: var(--yellow); +} + +.chart-point.tone-danger, +.chart-point.active.tone-danger { + fill: var(--red); +} + +.chart-label, +.chart-value { + fill: var(--muted-strong); + font-family: "IBM Plex Mono", monospace; +} + +.chart-label { + font-size: 11px; +} + +.chart-bubble rect { + fill: rgba(255, 255, 255, 0.08); + stroke: rgba(255, 255, 255, 0.16); +} + +.chart-value { + font-size: 12px; +} + +.demo-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + margin-top: 18px; +} + +.summary-block { + padding: 16px; + border-radius: 18px; + border: 1px solid var(--line-soft); + background: rgba(255, 255, 255, 0.03); + display: grid; + gap: 10px; +} + +.summary-block strong { + font-size: 1.18rem; +} + +.summary-reason strong { + font-size: 0.96rem; + line-height: 1.5; +} + +.table-wrap { + overflow-x: auto; +} + +.table-wrap table { + width: 100%; + border-collapse: collapse; +} + +th, +td { + text-align: left; + padding: 14px 10px; + border-bottom: 1px solid var(--line-soft); +} + +th { + color: var(--muted); + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.account-cell { + display: grid; + gap: 6px; +} + +.account-cell strong { + font-size: 1rem; +} + +.ladder-list { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.ladder-card { + min-height: 188px; + align-content: start; + opacity: 0.62; +} + +.ladder-card.is-active { + opacity: 1; + border-color: #565a67; + background: rgba(255, 255, 255, 0.05); +} + +.ladder-card.ladder-partial { + border-top: 1px solid rgba(240, 191, 95, 0.45); +} + +.ladder-card.ladder-full { + border-top: 1px solid rgba(239, 111, 108, 0.45); +} + +.ladder-card.ladder-reentry { + border-top: 1px solid rgba(34, 197, 94, 0.45); +} + +.timeline-item, +.audit-item { + position: relative; +} + +.timeline-item.is-active, +.audit-item:hover { + border-color: #565a67; + background: rgba(255, 255, 255, 0.05); +} + +.timeline-item.is-complete { + opacity: 0.72; +} + +.chip { + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.chip-live { + color: var(--green); +} + +.chip-warn { + color: var(--yellow); +} + +@media (max-width: 1400px) { + .overview-grid, + .content-grid, + .split-grid { + grid-template-columns: 1fr; + } + + .ladder-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 1080px) { + .app-shell { + grid-template-columns: 1fr; + } + + .sidebar { + position: static; + min-height: auto; + border-right: 0; + border-bottom: 1px solid var(--line-soft); + } + + .metric-grid, + .policy-grid, + .frame-strip, + .demo-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 720px) { + .workspace { + padding: 18px; + } + + .sidebar { + padding: 18px; + } + + .topbar, + .balance-row, + .panel-header { + flex-direction: column; + align-items: stretch; + } + + .topbar-chips, + .action-row { + justify-content: stretch; + } + + .metric-grid, + .policy-grid, + .frame-strip, + .demo-summary, + .ladder-list, + .workspace-meta-grid { + grid-template-columns: 1fr; + } + + .status-chip, + .button, + .nav-item { + width: 100%; + } + + .balance-value { + font-size: 2.8rem; + } +} diff --git a/lazer/cardano/guards-one/source/docs/functional-v4.md b/lazer/cardano/guards-one/source/docs/functional-v4.md new file mode 100644 index 00000000..68542777 --- /dev/null +++ b/lazer/cardano/guards-one/source/docs/functional-v4.md @@ -0,0 +1,113 @@ +# guards.one Functional Spec + +## Summary +guards.one is a treasury policy enforcement system for Cardano with a multichain-native core. The MVP keeps Cardano live and uses Pyth as the oracle evidence layer for every automated execution. The product shifts from a simple mode toggle into a risk ladder that can perform partial de-risk, full stable exit, and oracle-aware auto re-entry. + +## Product Goal +The system helps DAOs, communities, and treasury operators protect fiat-denominated value in volatile markets. It does not try to forecast markets or maximize yield. It enforces policy: if exposure becomes too large, too stale, or too uncertain, the treasury transitions into a safer state and optionally executes a swap into a single approved stable asset. + +## Core Principles +- Oracle data is not decorative. Price, confidence, and freshness are inputs to execution. +- The business logic is chain-agnostic. Cardano is only the first live connector. +- Risk is measured both as percentage change and as liquid fiat value. +- The treasury should react to floors, not just drawdown. +- The system must remain explainable to a judge, a governance signer, and an auditor. + +## Risk Model +The policy engine evaluates the treasury against three classes of thresholds. + +### 1. Market drawdown +Compares the latest Pyth price against the EMA price (`ema_price` in the Pyth feed, `emaPrice` in the internal model) and computes a drawdown in bps. This is the first trigger for Watch and Partial DeRisk. + +### 2. Liquid value floor +Computes the liquid value of each protected asset using: + +`liquid_value = amount * pyth_price * (1 - haircut_bps/10000)` + +If a protected asset or the full portfolio falls below a fiat/stable floor, the engine moves to Partial DeRisk or Full Stable Exit. + +### 3. Oracle quality +If the update is stale or the confidence ratio is too wide, the treasury should stop trusting the market view and reduce exposure aggressively. + +## Risk Ladder +The ladder is the main product abstraction. + +1. `Normal` +The treasury is healthy. Governance can act and the keeper watches the oracle. + +2. `Watch` +The treasury is approaching a threshold. No forced swap yet. + +3. `Partial DeRisk` +The treasury sells part of the risky asset into the approved stable asset. + +4. `Full Stable Exit` +The treasury fully exits the risky asset into the approved stable asset. + +5. `Frozen` +If the oracle update is stale or the confidence ratio is too wide, automatic execution is blocked until the market view is trustworthy again. + +6. `Auto Re-entry` +If the market recovers into the re-entry band and the cooldown has elapsed, the treasury can restore exposure. + +## Swap Policy +The MVP uses one approved stable token and one preapproved route on Cardano. The execution flow is intentionally simple. + +### Inputs +- Source asset +- Destination stable asset +- Route id +- Max sell amount +- Minimum buy amount +- Expiry +- Risk stage +- Reason hash + +### Execution Behavior +- `Partial DeRisk` only sells enough to restore the safe floor or reduce exposure to the target band. +- `Full Stable Exit` sells the remaining risky exposure. +- Re-entry is oracle-aware and hysteresis-based. The system must not immediately re-open after a single recovered tick. + +## On-chain Responsibilities +- Validate Pyth evidence in the same transaction. +- Verify freshness, confidence, and policy guards. +- Enforce the current stage of the vault. +- Emit or anchor an `ExecutionIntent`. +- Reject unauthorized signers, stale updates, or invalid routes. + +## Off-chain Responsibilities +- Collect and cache oracle updates. +- Evaluate policies and compute breach events. +- Build the execution transaction. +- Route the swap through the approved execution wallet. +- Persist an auditable log of snapshots, intents, and outcomes. + +## Multichain Model +The core code must not depend on Cardano-specific types. Instead, it exposes a common model that other chains can consume later. + +### Shared abstractions +- `PolicyConfig` +- `OracleSnapshot` +- `RiskStage` +- `ExecutionIntent` +- `ExecutionResult` +- `TreasuryConnector` +- `RouteSpec` + +### Current status +- Cardano: live MVP target. +- SVM: scaffolded from day one, no live deployment in this hackathon scope. +- EVM: scaffolded from day one, no live deployment in this hackathon scope. + +## Non-goals +- Full perps engine +- Cross-chain vault migration +- Multi-DEX optimization +- Yield farming or portfolio maximization + +## Acceptance Criteria +- A treasury can be created with a policy and roles. +- The dashboard can explain the latest oracle state and the current risk stage. +- A breach can trigger partial de-risk and full stable exit. +- A recovery condition can trigger auto re-entry. +- The product can explain how the same logic will later run on SVM and EVM. diff --git a/lazer/cardano/guards-one/source/docs/landing-frontend-spec.md b/lazer/cardano/guards-one/source/docs/landing-frontend-spec.md new file mode 100644 index 00000000..c79755c6 --- /dev/null +++ b/lazer/cardano/guards-one/source/docs/landing-frontend-spec.md @@ -0,0 +1,100 @@ +# guards.one Landing and Frontend Spec + +## Goal +Create a static but high-signal frontend shell that communicates the product in under one minute: +- What guards.one is +- Why Pyth is required +- How the risk ladder works +- What is live today versus planned later + +## Information Architecture +1. App-shell overview +2. Simulation and replay panel +3. Accounts table +4. Policy guardrails +5. Risk ladder +6. Execution timeline +7. Audit log +8. Documentation links + +## Visual Direction +- Dark, high-contrast treasury shell inspired by `squads.xyz` +- Flat charcoal panels with restrained borders and premium spacing +- White action pills and muted operator chrome +- Minimal accent colors only for state, risk, and execution tone +- Mobile-friendly layout with stacked panels and responsive sidebar collapse + +## Key Messages +- guards.one is a policy enforcement layer, not just an alert board. +- Cardano is the live MVP chain. +- The business logic is multichain-native from the start. +- The system uses price, confidence, freshness, and fiat floors. +- Partial de-risk and full stable exit are the core execution actions. +- Hedge and perps are roadmap items, not MVP scope. + +## Required UI Blocks + +### App Shell +- Branded sidebar with workspace card +- Primary nav for overview, accounts, policy, risk, execution, and audit +- Topbar chips for network, route, and wallet status +- Main overview panel with total liquid treasury value and action buttons + +### Simulation Panel +- Demo replay control +- Per-frame copy for watch, partial de-risk, full exit, and recovery +- SVG chart driven by backend series data +- Summary boxes for frame balance, stable ratio, and trigger reason + +### Accounts Table +- Hot bucket and stable reserve rows +- Address, balance, fiat value, and weight +- Clear distinction between risk and stable roles + +### Policy Cards +- Protected floor +- Emergency floor +- Primary reason +- Current execution route / policy + +### Risk Ladder +- Normal +- Watch +- Partial DeRisk +- Full Stable Exit +- Auto Re-entry + +Each stage must explain the trigger and the resulting action. + +### Execution Timeline +Show the simulated runbook steps emitted by the backend demo state: +- partial de-risk execution +- full stable exit +- re-entry execution + +### Multichain Positioning +Explain that Cardano is the live deployment target while SVM and EVM are first-class future connectors. + +### Audit Log +- Recent snapshots, intents, and execution events +- Human-readable summaries generated from the backend event log + +## Interaction Notes +- The page stays build-free, but it is not static in presentation. +- A small JS file renders backend demo data into the shell and powers replay controls. +- The UI should prefer `/api/demo-state` and fall back to a committed demo JSON export. +- No build step required. +- All content should render without network access except the font import, which is optional. + +## Copy Constraints +- Keep explanations concrete. +- Mention `Pyth` as the oracle evidence layer. +- Avoid generic “AI agent” or “dashboard app” language. +- Make it clear that hedge/perps are planned later, not part of the live MVP. + +## Acceptance Criteria +- The landing reads clearly on desktop and mobile. +- The visual hierarchy is obvious. +- The docs match the product direction in the functional spec. +- A reviewer can understand the product and the roadmap without talking to the team. +- The replay demo clearly shows the breach lifecycle without needing a live blockchain connection. diff --git a/lazer/cardano/guards-one/source/docs/roadmap.md b/lazer/cardano/guards-one/source/docs/roadmap.md new file mode 100644 index 00000000..fd8b1a8e --- /dev/null +++ b/lazer/cardano/guards-one/source/docs/roadmap.md @@ -0,0 +1,39 @@ +# guards.one Roadmap + +## Phase 1 +Deliver a credible hackathon MVP on Cardano with a polished dashboard, a documented policy model, and at least one real oracle-driven execution path. + +### Deliverables +- Functional spec v4 +- Static landing/dashboard shell +- Cardano execution path for partial de-risk and full stable exit +- Audit trail for oracle evidence and execution result + +## Phase 2 +Expand the policy model and UX so treasury operators can manage more than one protected asset and more than one threshold family. + +### Deliverables +- Multiple asset floors +- Better re-entry controls +- Historical snapshots and replay +- Visual policy editor + +## Phase 3 +Introduce hedge-aware policy branches without making them part of the Cardano MVP. + +### Deliverables +- Hedge engine types and interfaces +- Long/short policy hooks +- Roadmap for perps and structured protection products + +## Phase 4 +Turn the core into a genuine multichain operating layer. + +### Deliverables +- SVM connector with live execution +- EVM connector with live execution +- Shared policy engine used by all adapters +- Per-chain capability matrix in the UI + +## Strategic Direction +The product should remain conservative in the near term and expand in scope only after the core policy enforcement loop is trustworthy. The next big feature after the hackathon is not a more complex swap route; it is a broader treasury control plane that can execute across chains without rewriting the business logic. diff --git a/lazer/cardano/guards-one/source/package.json b/lazer/cardano/guards-one/source/package.json new file mode 100644 index 00000000..3f486036 --- /dev/null +++ b/lazer/cardano/guards-one/source/package.json @@ -0,0 +1,18 @@ +{ + "name": "guards-one", + "private": true, + "type": "module", + "scripts": { + "test": "vitest run", + "typecheck": "tsc --noEmit", + "simulate": "tsx apps/backend/src/simulate.ts", + "export:ui-data": "tsx apps/backend/src/export-ui-data.ts", + "preview": "tsx apps/backend/src/preview-server.ts" + }, + "devDependencies": { + "@types/node": "^24.0.0", + "tsx": "^4.20.3", + "typescript": "^5.8.3", + "vitest": "^3.2.4" + } +} diff --git a/lazer/cardano/guards-one/source/packages/cardano/package.json b/lazer/cardano/guards-one/source/packages/cardano/package.json new file mode 100644 index 00000000..8608453d --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/cardano/package.json @@ -0,0 +1,12 @@ +{ + "name": "@anaconda/cardano", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@anaconda/core": "workspace:*" + } +} diff --git a/lazer/cardano/guards-one/source/packages/cardano/src/index.ts b/lazer/cardano/guards-one/source/packages/cardano/src/index.ts new file mode 100644 index 00000000..bd3efca6 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/cardano/src/index.ts @@ -0,0 +1,2 @@ +export * from "./policy-vault.js"; +export * from "./types.js"; diff --git a/lazer/cardano/guards-one/source/packages/cardano/src/policy-vault.ts b/lazer/cardano/guards-one/source/packages/cardano/src/policy-vault.ts new file mode 100644 index 00000000..8e8e97f6 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/cardano/src/policy-vault.ts @@ -0,0 +1,379 @@ +import { + baseCapabilities, + evaluateRiskLadder, + type ExecutionIntent, + type ExecutionResult, + type PolicyConfig, + type RouteSpec, + type TreasuryState, +} from "@anaconda/core"; +import type { + AuthorizeExecutionParams, + AuthorizeExecutionResult, + CardanoPolicyVaultArtifact, + CardanoTxEnvelope, + CompleteExecutionParams, + CompleteExecutionResult, +} from "./types.js"; + +export class CardanoExecutionError extends Error { + code: string; + + constructor(code: string, message: string) { + super(message); + this.code = code; + } +} + +export const policyVaultArtifact: CardanoPolicyVaultArtifact = { + name: "PolicyVault", + datumFields: [ + "vault_id", + "stage", + "last_transition_us", + "governance_signers", + "keepers", + "approved_route_ids", + "stable_asset_id", + "primary_asset_id", + "current_intent_id", + ], + redeemers: [ + "AuthorizeExecution", + "CompleteExecution", + "UpdatePolicy", + "Resume", + "EmergencyWithdraw", + ], + invariants: [ + "Pyth witness must be present in the same transaction for authorize execution", + "Only approved keepers can authorize automatic execution", + "Only approved routes and assets can be used by execution results", + "Cooldown and guardrails are enforced before stage transitions", + ], +}; + +function buildTxEnvelope( + kind: CardanoTxEnvelope["kind"], + reasonHash: string, + startUs: number, + validityEndUs: number, + referenceInputs: string[], + withdrawals: CardanoTxEnvelope["withdrawals"], + spendingTargets: string[], + metadata: Record = {}, +): CardanoTxEnvelope { + return { + kind, + validityStartUs: startUs, + validityEndUs, + referenceInputs, + withdrawals, + spendingTargets, + metadata: { + reason_hash: reasonHash, + tx_kind: kind, + ...metadata, + }, + }; +} + +function deriveValidityEndUs( + startUs: number, + expiryUs: number, + maxValidityDurationUs: number, +): number { + return Math.max(startUs, Math.min(expiryUs, startUs + maxValidityDurationUs)); +} + +function updatePositionAmount( + treasury: TreasuryState, + assetId: string, + delta: number, +): TreasuryState["positions"] { + let touched = false; + const positions = treasury.positions.map((position) => { + if (position.assetId !== assetId) { + return position; + } + + touched = true; + const nextAmount = Number((position.amount + delta).toFixed(8)); + if (nextAmount < -1e-9) { + throw new CardanoExecutionError( + "NEGATIVE_BALANCE", + `Asset ${assetId} would go negative after execution`, + ); + } + + return { + ...position, + amount: Math.max(0, nextAmount), + }; + }); + + if (!touched) { + throw new CardanoExecutionError("ASSET_NOT_FOUND", `Asset ${assetId} is not in treasury`); + } + + return positions; +} + +export class PolicyVaultSimulator { + readonly pythPolicyId: string; + readonly capabilities = baseCapabilities.cardano; + + constructor(pythPolicyId: string) { + this.pythPolicyId = pythPolicyId; + } + + authorizeExecution(params: AuthorizeExecutionParams): AuthorizeExecutionResult { + const { treasury, policy, snapshots, routes, nowUs, keeperId, witness } = params; + + if (treasury.currentIntentId) { + throw new CardanoExecutionError( + "INTENT_ALREADY_IN_FLIGHT", + "Treasury already has an in-flight execution intent", + ); + } + + if (!treasury.keepers.includes(keeperId)) { + throw new CardanoExecutionError( + "KEEPER_NOT_AUTHORIZED", + `Keeper ${keeperId} is not allowed to authorize automatic execution`, + ); + } + + if (witness.pythPolicyId !== this.pythPolicyId) { + throw new CardanoExecutionError( + "PYTH_POLICY_MISMATCH", + "The provided Pyth witness does not match the configured Cardano deployment", + ); + } + + const assessment = evaluateRiskLadder(treasury, policy, snapshots, routes, nowUs); + if (assessment.shouldFreeze) { + const reason = assessment.reasons[0]; + throw new CardanoExecutionError( + reason?.code?.toUpperCase() ?? "FREEZE_REQUIRED", + reason?.message ?? "Oracle guardrails require a freeze instead of execution", + ); + } + + if (assessment.cooldownRemainingUs > 0) { + throw new CardanoExecutionError( + "COOLDOWN_ACTIVE", + "Cooldown is still active for this vault", + ); + } + + const intent = assessment.intent; + if (!intent) { + throw new CardanoExecutionError( + "NO_EXECUTABLE_INTENT", + "Current policy evaluation did not produce an automatic execution intent", + ); + } + + if (!policy.approvedRouteIds.includes(intent.routeId)) { + throw new CardanoExecutionError( + "ROUTE_NOT_APPROVED", + `Route ${intent.routeId} is not approved by governance`, + ); + } + + if ( + intent.destinationAssetId !== policy.stableAssetId && + intent.destinationAssetId !== policy.primaryAssetId + ) { + throw new CardanoExecutionError( + "ASSET_NOT_APPROVED", + `Destination asset ${intent.destinationAssetId} is not approved`, + ); + } + + const nextTreasury: TreasuryState = { + ...treasury, + stage: assessment.nextStage, + lastTransitionUs: nowUs, + currentIntentId: intent.intentId, + }; + + const tx = buildTxEnvelope( + "authorize_execution", + intent.reasonHash, + nowUs, + deriveValidityEndUs(nowUs, intent.expiryUs, policy.maxStaleUs), + [witness.pythStateReference], + [ + { + policyId: witness.pythPolicyId, + amount: 0, + redeemer: witness.signedUpdateHex, + }, + ], + ["policy-vault-utxo"], + ); + + return { + treasury: nextTreasury, + assessment, + intent, + tx, + }; + } + + completeExecution(params: CompleteExecutionParams): CompleteExecutionResult { + const { treasury, policy, intent, result } = params; + + if (result.vaultId !== treasury.vaultId || result.vaultId !== intent.vaultId) { + throw new CardanoExecutionError( + "RESULT_VAULT_MISMATCH", + "Execution result does not belong to this vault", + ); + } + + if (result.chainId !== treasury.chainId || result.chainId !== intent.chainId) { + throw new CardanoExecutionError( + "RESULT_CHAIN_MISMATCH", + "Execution result does not belong to this chain", + ); + } + + if (treasury.currentIntentId !== intent.intentId) { + throw new CardanoExecutionError( + "INTENT_MISMATCH", + "Treasury does not hold the provided execution intent", + ); + } + + if (result.intentId !== intent.intentId) { + throw new CardanoExecutionError( + "RESULT_INTENT_MISMATCH", + "Execution result does not match the current intent", + ); + } + + if (!policy.approvedRouteIds.includes(result.routeId)) { + throw new CardanoExecutionError( + "ROUTE_NOT_APPROVED", + `Route ${result.routeId} is not approved by governance`, + ); + } + + if (result.routeId !== intent.routeId) { + throw new CardanoExecutionError( + "ROUTE_MISMATCH", + "Execution result route does not match the authorized intent", + ); + } + + if ( + result.sourceAssetId !== intent.sourceAssetId || + result.destinationAssetId !== intent.destinationAssetId + ) { + throw new CardanoExecutionError( + "ASSET_NOT_APPROVED", + "Execution result asset pair differs from the approved intent", + ); + } + + if (result.executedAtUs < intent.createdAtUs || result.executedAtUs > intent.expiryUs) { + throw new CardanoExecutionError( + "RESULT_OUT_OF_WINDOW", + "Execution result falls outside the authorized intent window", + ); + } + + if (result.soldAmount - intent.maxSellAmount > 1e-8) { + throw new CardanoExecutionError( + "MAX_SELL_EXCEEDED", + "Execution result sold more than the authorized intent", + ); + } + + if (result.boughtAmount + 1e-8 < intent.minBuyAmount) { + throw new CardanoExecutionError( + "MIN_BUY_NOT_MET", + "Execution result under-delivered against the minimum buy amount", + ); + } + + let nextPositions = updatePositionAmount(treasury, result.sourceAssetId, -result.soldAmount); + nextPositions = updatePositionAmount( + { ...treasury, positions: nextPositions }, + result.destinationAssetId, + result.boughtAmount, + ); + + const nextTreasury: TreasuryState = { + ...treasury, + positions: nextPositions, + stage: intent.kind === "reentry_swap" ? "normal" : intent.stage, + currentIntentId: undefined, + lastTransitionUs: result.executedAtUs, + }; + + return { + treasury: nextTreasury, + tx: buildTxEnvelope( + "complete_execution", + intent.reasonHash, + result.executedAtUs, + deriveValidityEndUs(result.executedAtUs, intent.expiryUs, policy.maxStaleUs), + [], + [], + ["policy-vault-utxo", "execution-hot-wallet"], + { + intent_id: intent.intentId, + tx_hash: result.txHash, + }, + ), + }; + } +} + +export function createCardanoConnector(routes: RouteSpec[]) { + return { + chainId: "cardano" as const, + capabilities: baseCapabilities.cardano, + describeExecutionConstraints() { + return [ + "Cardano executes live in the MVP via a two-step authorize and swap flow.", + "Only approved routes can consume the execution hot bucket.", + "Oracle verification must be present in the same authorization transaction.", + ]; + }, + simulateRoute(intent: ExecutionIntent, priceMap: Record) { + const route = routes.find((candidate) => candidate.routeId === intent.routeId); + if (!route) { + throw new CardanoExecutionError( + "ROUTE_NOT_FOUND", + `Missing route definition for ${intent.routeId}`, + ); + } + + const sourcePrice = priceMap[intent.sourceAssetId]; + const destinationPrice = priceMap[intent.destinationAssetId]; + if (!sourcePrice || !destinationPrice) { + throw new CardanoExecutionError( + "PRICE_MAP_INCOMPLETE", + "Price map is missing one or more assets for route simulation", + ); + } + + const soldAmount = intent.maxSellAmount; + const grossDestination = soldAmount * sourcePrice / destinationPrice; + const boughtAmount = grossDestination * (1 - route.maxSlippageBps / 10_000); + + return { + sourceAssetId: intent.sourceAssetId, + destinationAssetId: intent.destinationAssetId, + soldAmount: Number(soldAmount.toFixed(8)), + boughtAmount: Number(boughtAmount.toFixed(8)), + averagePrice: Number((boughtAmount / soldAmount).toFixed(8)), + routeId: route.routeId, + }; + }, + }; +} diff --git a/lazer/cardano/guards-one/source/packages/cardano/src/types.ts b/lazer/cardano/guards-one/source/packages/cardano/src/types.ts new file mode 100644 index 00000000..d7a81943 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/cardano/src/types.ts @@ -0,0 +1,65 @@ +import type { + ExecutionIntent, + ExecutionResult, + OracleSnapshot, + PolicyConfig, + RiskAssessment, + RouteSpec, + TreasuryState, +} from "@anaconda/core"; + +export interface CardanoPythWitness { + pythPolicyId: string; + pythStateReference: string; + signedUpdateHex: string; +} + +export interface CardanoTxEnvelope { + kind: "authorize_execution" | "complete_execution"; + validityStartUs: number; + validityEndUs: number; + referenceInputs: string[]; + withdrawals: Array<{ + policyId: string; + amount: number; + redeemer: string; + }>; + spendingTargets: string[]; + metadata: Record; +} + +export interface AuthorizeExecutionParams { + treasury: TreasuryState; + policy: PolicyConfig; + snapshots: Record; + routes: RouteSpec[]; + nowUs: number; + keeperId: string; + witness: CardanoPythWitness; +} + +export interface AuthorizeExecutionResult { + treasury: TreasuryState; + assessment: RiskAssessment; + intent: ExecutionIntent; + tx: CardanoTxEnvelope; +} + +export interface CompleteExecutionParams { + treasury: TreasuryState; + policy: PolicyConfig; + intent: ExecutionIntent; + result: ExecutionResult; +} + +export interface CompleteExecutionResult { + treasury: TreasuryState; + tx: CardanoTxEnvelope; +} + +export interface CardanoPolicyVaultArtifact { + name: string; + datumFields: string[]; + redeemers: string[]; + invariants: string[]; +} diff --git a/lazer/cardano/guards-one/source/packages/cardano/tests/policy-vault.test.ts b/lazer/cardano/guards-one/source/packages/cardano/tests/policy-vault.test.ts new file mode 100644 index 00000000..c1d1b187 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/cardano/tests/policy-vault.test.ts @@ -0,0 +1,421 @@ +import { describe, expect, it } from "vitest"; +import { + buildSnapshots, + samplePolicy, + sampleRoutes, + sampleTreasury, +} from "@anaconda/core"; +import { + CardanoExecutionError, + PolicyVaultSimulator, + createCardanoConnector, +} from "../src/index.js"; + +const simulator = new PolicyVaultSimulator( + "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6", +); + +const witness = { + pythPolicyId: "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6", + pythStateReference: "pyth-state-ref", + signedUpdateHex: "0xfeedbeef", +}; + +function expectCardanoError(action: () => void, expectedCode: string) { + try { + action(); + throw new Error(`Expected CardanoExecutionError(${expectedCode})`); + } catch (error) { + expect(error).toBeInstanceOf(CardanoExecutionError); + expect((error as CardanoExecutionError).code).toBe(expectedCode); + } +} + +describe("cardano policy vault simulator", () => { + it("authorizes a valid partial de-risk execution", () => { + const result = simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }); + + expect(result.intent.stage).toBe("partial_derisk"); + expect(result.treasury.stage).toBe("partial_derisk"); + expect(result.tx.withdrawals).toHaveLength(1); + expect(result.tx.referenceInputs).toEqual([witness.pythStateReference]); + expect(result.tx.metadata.reason_hash).toBe(result.intent.reasonHash); + expect(result.tx.validityEndUs).toBe(result.intent.expiryUs); + }); + + it("rejects stale snapshots", () => { + expectCardanoError( + () => + simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots({ + ada: { feedUpdateTimestampUs: 1_000_000, observedAtUs: 1_000_000 }, + }), + routes: sampleRoutes, + nowUs: 500_000_000, + keeperId: "keeper-1", + witness, + }), + "STALE_FEED", + ); + }); + + it("rejects wide confidence intervals", () => { + expectCardanoError( + () => + simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots({ + ada: { confidence: 0.04, exponent: 0, price: 1, emaPrice: 1 }, + }), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }), + "CONFIDENCE_GUARDRAIL", + ); + }); + + it("rejects execution during cooldown", () => { + expectCardanoError( + () => + simulator.authorizeExecution({ + treasury: { + ...sampleTreasury, + lastTransitionUs: 90_000_000, + }, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }), + "COOLDOWN_ACTIVE", + ); + }); + + it("rejects Pyth witness mismatches", () => { + expectCardanoError( + () => + simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness: { + ...witness, + pythPolicyId: "bad-policy-id", + }, + }), + "PYTH_POLICY_MISMATCH", + ); + }); + + it("rejects unauthorized keepers", () => { + expectCardanoError( + () => + simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-x", + witness, + }), + "KEEPER_NOT_AUTHORIZED", + ); + }); + + it("rejects authorization while another intent is in flight", () => { + expectCardanoError( + () => + simulator.authorizeExecution({ + treasury: { + ...sampleTreasury, + currentIntentId: "intent-existing", + }, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }), + "INTENT_ALREADY_IN_FLIGHT", + ); + }); + + it("completes an authorized execution and updates treasury balances", () => { + const authorization = simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }); + const connector = createCardanoConnector(sampleRoutes); + const simulated = connector.simulateRoute( + authorization.intent, + authorization.assessment.metrics.priceMap, + ); + + const completion = simulator.completeExecution({ + treasury: authorization.treasury, + policy: samplePolicy, + intent: authorization.intent, + result: { + intentId: authorization.intent.intentId, + vaultId: sampleTreasury.vaultId, + chainId: "cardano", + sourceAssetId: simulated.sourceAssetId, + destinationAssetId: simulated.destinationAssetId, + soldAmount: simulated.soldAmount, + boughtAmount: simulated.boughtAmount, + averagePrice: simulated.averagePrice, + txHash: "tx-good", + executedAtUs: 100_001_000, + routeId: simulated.routeId, + }, + }); + + expect(completion.treasury.currentIntentId).toBeUndefined(); + expect( + completion.treasury.positions.find((position) => position.assetId === "ada")?.amount, + ).toBeLessThan(10_000); + expect( + completion.treasury.positions.find((position) => position.assetId === "usdm")?.amount, + ).toBeGreaterThan(1_500); + expect(completion.tx.metadata.reason_hash).toBe(authorization.intent.reasonHash); + expect(simulated.averagePrice).toBeCloseTo( + simulated.boughtAmount / simulated.soldAmount, + 8, + ); + }); + + it("rejects executions that do not meet the minimum buy amount", () => { + const authorization = simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }); + + expectCardanoError( + () => + simulator.completeExecution({ + treasury: authorization.treasury, + policy: samplePolicy, + intent: authorization.intent, + result: { + intentId: authorization.intent.intentId, + vaultId: sampleTreasury.vaultId, + chainId: "cardano", + sourceAssetId: authorization.intent.sourceAssetId, + destinationAssetId: authorization.intent.destinationAssetId, + soldAmount: authorization.intent.maxSellAmount, + boughtAmount: authorization.intent.minBuyAmount - 10, + averagePrice: 1, + txHash: "tx-short", + executedAtUs: 101_000_000, + routeId: authorization.intent.routeId, + }, + }), + "MIN_BUY_NOT_MET", + ); + }); + + it("rejects unapproved route/asset pairs at completion time", () => { + const authorized = simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }); + + expectCardanoError( + () => + simulator.completeExecution({ + treasury: authorized.treasury, + policy: samplePolicy, + intent: authorized.intent, + result: { + intentId: authorized.intent.intentId, + vaultId: sampleTreasury.vaultId, + chainId: "cardano", + sourceAssetId: "ada", + destinationAssetId: "evil", + soldAmount: authorized.intent.maxSellAmount, + boughtAmount: authorized.intent.minBuyAmount + 1, + averagePrice: 1, + txHash: "tx-bad", + executedAtUs: 101_000_000, + routeId: authorized.intent.routeId, + }, + }), + "ASSET_NOT_APPROVED", + ); + }); + + it("rejects route mismatches even when the route is approved", () => { + const authorized = simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }); + + expectCardanoError( + () => + simulator.completeExecution({ + treasury: authorized.treasury, + policy: samplePolicy, + intent: authorized.intent, + result: { + intentId: authorized.intent.intentId, + vaultId: sampleTreasury.vaultId, + chainId: "cardano", + sourceAssetId: authorized.intent.sourceAssetId, + destinationAssetId: authorized.intent.destinationAssetId, + soldAmount: authorized.intent.maxSellAmount, + boughtAmount: authorized.intent.minBuyAmount + 1, + averagePrice: 1, + txHash: "tx-other-route", + executedAtUs: 101_000_000, + routeId: "cardano-minswap-usdm-ada", + }, + }), + "ROUTE_MISMATCH", + ); + }); + + it("rejects executions outside the intent window", () => { + const authorized = simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }); + + expectCardanoError( + () => + simulator.completeExecution({ + treasury: authorized.treasury, + policy: samplePolicy, + intent: authorized.intent, + result: { + intentId: authorized.intent.intentId, + vaultId: sampleTreasury.vaultId, + chainId: "cardano", + sourceAssetId: authorized.intent.sourceAssetId, + destinationAssetId: authorized.intent.destinationAssetId, + soldAmount: authorized.intent.maxSellAmount, + boughtAmount: authorized.intent.minBuyAmount + 1, + averagePrice: 1, + txHash: "tx-late", + executedAtUs: authorized.intent.expiryUs + 1, + routeId: authorized.intent.routeId, + }, + }), + "RESULT_OUT_OF_WINDOW", + ); + }); + + it("rejects cross-vault or cross-chain execution results", () => { + const authorized = simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }); + + expectCardanoError( + () => + simulator.completeExecution({ + treasury: authorized.treasury, + policy: samplePolicy, + intent: authorized.intent, + result: { + intentId: authorized.intent.intentId, + vaultId: "vault-elsewhere", + chainId: "evm", + sourceAssetId: authorized.intent.sourceAssetId, + destinationAssetId: authorized.intent.destinationAssetId, + soldAmount: authorized.intent.maxSellAmount, + boughtAmount: authorized.intent.minBuyAmount + 1, + averagePrice: 1, + txHash: "tx-wrong-domain", + executedAtUs: 101_000_000, + routeId: authorized.intent.routeId, + }, + }), + "RESULT_VAULT_MISMATCH", + ); + }); + + it("rejects results that sell more than the authorized intent", () => { + const authorized = simulator.authorizeExecution({ + treasury: sampleTreasury, + policy: samplePolicy, + snapshots: buildSnapshots(), + routes: sampleRoutes, + nowUs: 100_000_000, + keeperId: "keeper-1", + witness, + }); + + expectCardanoError( + () => + simulator.completeExecution({ + treasury: authorized.treasury, + policy: samplePolicy, + intent: authorized.intent, + result: { + intentId: authorized.intent.intentId, + vaultId: sampleTreasury.vaultId, + chainId: "cardano", + sourceAssetId: authorized.intent.sourceAssetId, + destinationAssetId: authorized.intent.destinationAssetId, + soldAmount: authorized.intent.maxSellAmount + 1, + boughtAmount: authorized.intent.minBuyAmount + 1, + averagePrice: 1, + txHash: "tx-oversold", + executedAtUs: 101_000_000, + routeId: authorized.intent.routeId, + }, + }), + "MAX_SELL_EXCEEDED", + ); + }); +}); diff --git a/lazer/cardano/guards-one/source/packages/core/package.json b/lazer/cardano/guards-one/source/packages/core/package.json new file mode 100644 index 00000000..71501bd0 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/core/package.json @@ -0,0 +1,9 @@ +{ + "name": "@anaconda/core", + "version": "0.0.0", + "private": true, + "type": "module", + "exports": { + ".": "./src/index.ts" + } +} diff --git a/lazer/cardano/guards-one/source/packages/core/src/capabilities.ts b/lazer/cardano/guards-one/source/packages/core/src/capabilities.ts new file mode 100644 index 00000000..b54f9f9c --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/core/src/capabilities.ts @@ -0,0 +1,50 @@ +import type { ChainCapabilities, ChainId, Role } from "./types.js"; + +export const roleModel: Role[] = [ + "governance", + "risk_manager", + "keeper", + "viewer", +]; + +export const baseCapabilities: Record = { + cardano: { + chainId: "cardano", + live: true, + supportsOracleVerifiedExecution: true, + supportsHotColdBuckets: true, + supportsAutoSwap: true, + supportsAutoReentry: true, + supportsLiveDeployments: true, + notes: [ + "Cardano is the only live execution target in the MVP.", + "Execution uses a two-step authorize-and-swap flow.", + ], + }, + svm: { + chainId: "svm", + live: false, + supportsOracleVerifiedExecution: true, + supportsHotColdBuckets: true, + supportsAutoSwap: true, + supportsAutoReentry: true, + supportsLiveDeployments: false, + notes: [ + "Scaffolding only in the MVP.", + "Designed to share the same policy and role model.", + ], + }, + evm: { + chainId: "evm", + live: false, + supportsOracleVerifiedExecution: true, + supportsHotColdBuckets: true, + supportsAutoSwap: true, + supportsAutoReentry: true, + supportsLiveDeployments: false, + notes: [ + "Scaffolding only in the MVP.", + "Connectors remain simulation-first until phase 2.", + ], + }, +}; diff --git a/lazer/cardano/guards-one/source/packages/core/src/engine.ts b/lazer/cardano/guards-one/source/packages/core/src/engine.ts new file mode 100644 index 00000000..98b8f6a8 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/core/src/engine.ts @@ -0,0 +1,488 @@ +import { randomUUID } from "node:crypto"; +import { stableHash } from "./hash.js"; +import { + confidenceBps, + isSnapshotStale, + STAGE_SEVERITY, + stageMax, + summarizePortfolio, + toDecimalPrice, +} from "./math.js"; +import type { + EvaluationReason, + ExecutionIntent, + PolicyConfig, + RiskAssessment, + RiskStage, + RouteSpec, + TreasuryPosition, + TreasuryState, + OracleSnapshot, +} from "./types.js"; + +function buildReason( + code: string, + severity: RiskStage, + message: string, + details?: Record, +): EvaluationReason { + return { code, severity, message, details }; +} + +function clampAmount(amount: number): number { + if (!Number.isFinite(amount) || amount < 0) { + return 0; + } + + return Number(amount.toFixed(8)); +} + +function getPrimaryRiskPosition( + treasury: TreasuryState, + policy: PolicyConfig, +): TreasuryPosition | undefined { + return treasury.positions.find((position) => position.assetId === policy.primaryAssetId); +} + +function getStablePosition( + treasury: TreasuryState, + policy: PolicyConfig, +): TreasuryPosition | undefined { + return treasury.positions.find((position) => position.assetId === policy.stableAssetId); +} + +function chooseRoute( + treasury: TreasuryState, + policy: PolicyConfig, + routes: RouteSpec[], + fromAssetId: string, + toAssetId: string, +): RouteSpec | undefined { + return routes.find((candidate) => + policy.approvedRouteIds.includes(candidate.routeId) && + candidate.chainId === treasury.chainId && + candidate.fromAssetId === fromAssetId && + candidate.toAssetId === toAssetId && + candidate.live, + ); +} + +function buildIntent( + kind: ExecutionIntent["kind"], + stage: RiskStage, + sellAmount: number, + expectedBuyAmount: number, + treasury: TreasuryState, + policy: PolicyConfig, + snapshots: Record, + route: RouteSpec, + nowUs: number, + reasons: EvaluationReason[], +): ExecutionIntent | undefined { + if (sellAmount <= 0 || expectedBuyAmount <= 0) { + return undefined; + } + + const sourceAssetId = + kind === "reentry_swap" ? policy.stableAssetId : policy.primaryAssetId; + const destinationAssetId = + kind === "reentry_swap" ? policy.primaryAssetId : policy.stableAssetId; + const snapshotIds = Object.values(snapshots) + .sort((left, right) => left.snapshotId.localeCompare(right.snapshotId)) + .map((snapshot) => snapshot.snapshotId); + + return { + intentId: randomUUID(), + vaultId: treasury.vaultId, + chainId: treasury.chainId, + kind, + stage, + sourceAssetId, + destinationAssetId, + routeId: route.routeId, + maxSellAmount: clampAmount(sellAmount), + minBuyAmount: clampAmount( + expectedBuyAmount * (1 - route.maxSlippageBps / 10_000), + ), + expiryUs: nowUs + policy.maxStaleUs, + reasonHash: stableHash({ + stage, + reasons, + snapshotIds, + sellAmount, + expectedBuyAmount, + }), + snapshotIds, + createdAtUs: nowUs, + }; +} + +function buildDeriskIntent( + targetStage: RiskStage, + treasury: TreasuryState, + policy: PolicyConfig, + snapshots: Record, + routes: RouteSpec[], + nowUs: number, + reasons: EvaluationReason[], +): ExecutionIntent | undefined { + const riskPosition = getPrimaryRiskPosition(treasury, policy); + const stablePosition = getStablePosition(treasury, policy); + const riskSnapshot = snapshots[policy.primaryAssetId]; + const stableSnapshot = snapshots[policy.stableAssetId]; + + if (!riskPosition || !stablePosition || !riskSnapshot || !stableSnapshot) { + return undefined; + } + + const metrics = summarizePortfolio(treasury.positions, snapshots, policy); + const route = chooseRoute( + treasury, + policy, + routes, + policy.primaryAssetId, + policy.stableAssetId, + ); + if (!route) { + return undefined; + } + const stablePrice = stableSnapshot ? toDecimalPrice(stableSnapshot) : 1; + const riskPrice = toDecimalPrice(riskSnapshot) * (1 - policy.haircutBps / 10_000); + const targetStableValue = + targetStage === "full_exit" + ? metrics.totalLiquidValueFiat + : Math.max( + policy.portfolioFloorFiat, + metrics.totalLiquidValueFiat * policy.partialStableTargetRatio, + ); + const stableGapFiat = Math.max(0, targetStableValue - metrics.stableLiquidValueFiat); + const sellAmount = + targetStage === "full_exit" + ? riskPosition.amount + : Math.min(riskPosition.amount, stableGapFiat / Math.max(riskPrice, 1e-9)); + const expectedBuyAmount = + sellAmount * toDecimalPrice(riskSnapshot) / Math.max(stablePrice, 1e-9); + + return buildIntent( + "derisk_swap", + targetStage, + sellAmount, + expectedBuyAmount, + treasury, + policy, + snapshots, + route, + nowUs, + reasons, + ); +} + +function buildReentryIntent( + treasury: TreasuryState, + policy: PolicyConfig, + snapshots: Record, + routes: RouteSpec[], + nowUs: number, + reasons: EvaluationReason[], +): ExecutionIntent | undefined { + const riskPosition = getPrimaryRiskPosition(treasury, policy); + const stablePosition = getStablePosition(treasury, policy); + const riskSnapshot = snapshots[policy.primaryAssetId]; + const stableSnapshot = snapshots[policy.stableAssetId]; + + if (!riskPosition || !stablePosition || !riskSnapshot || !stableSnapshot) { + return undefined; + } + + const metrics = summarizePortfolio(treasury.positions, snapshots, policy); + const route = chooseRoute( + treasury, + policy, + routes, + policy.stableAssetId, + policy.primaryAssetId, + ); + if (!route) { + return undefined; + } + const stablePrice = stableSnapshot ? toDecimalPrice(stableSnapshot) : 1; + const riskPrice = toDecimalPrice(riskSnapshot); + const targetRiskValue = metrics.totalLiquidValueFiat * policy.reentryRiskTargetRatio; + const riskGapFiat = Math.max(0, targetRiskValue - metrics.riskLiquidValueFiat); + const sellAmount = Math.min( + stablePosition.amount, + riskGapFiat / Math.max(stablePrice, 1e-9), + ); + const expectedBuyAmount = sellAmount * stablePrice / Math.max(riskPrice, 1e-9); + + return buildIntent( + "reentry_swap", + "normal", + sellAmount, + expectedBuyAmount, + treasury, + policy, + snapshots, + route, + nowUs, + reasons, + ); +} + +export function evaluateRiskLadder( + treasury: TreasuryState, + policy: PolicyConfig, + snapshots: Record, + routes: RouteSpec[], + nowUs: number, +): RiskAssessment { + const reasons: EvaluationReason[] = []; + const primarySnapshot = snapshots[policy.primaryAssetId]; + const metrics = summarizePortfolio(treasury.positions, snapshots, policy); + const cooldownRemainingUs = Math.max( + 0, + policy.cooldownUs - (nowUs - treasury.lastTransitionUs), + ); + + if (!primarySnapshot) { + return { + nowUs, + currentStage: treasury.stage, + nextStage: treasury.stage, + metrics, + reasons: [ + buildReason( + "missing_primary_snapshot", + "frozen", + "Primary oracle snapshot is unavailable", + ), + ], + cooldownRemainingUs, + shouldFreeze: true, + }; + } + + if (isSnapshotStale(primarySnapshot, nowUs, policy.maxStaleUs)) { + reasons.push( + buildReason("stale_feed", "frozen", "Primary feed update is stale", { + age_us: nowUs - primarySnapshot.feedUpdateTimestampUs, + }), + ); + } + + const relativeConfidence = confidenceBps(primarySnapshot); + if (relativeConfidence > policy.maxConfidenceBps) { + reasons.push( + buildReason( + "confidence_guardrail", + "frozen", + "Confidence interval is wider than the configured guardrail", + { confidence_bps: Number(relativeConfidence.toFixed(2)) }, + ), + ); + } + + if (reasons.some((reason) => reason.severity === "frozen")) { + return { + nowUs, + currentStage: treasury.stage, + nextStage: "frozen", + metrics, + reasons, + cooldownRemainingUs, + shouldFreeze: true, + }; + } + + let targetStage: RiskStage = "normal"; + if (metrics.drawdownBps >= policy.watchDrawdownBps) { + targetStage = stageMax(targetStage, "watch"); + reasons.push( + buildReason( + "drawdown_watch", + "watch", + "Primary asset drawdown crossed watch threshold", + { drawdown_bps: Number(metrics.drawdownBps.toFixed(2)) }, + ), + ); + } + + if (metrics.drawdownBps >= policy.partialDrawdownBps) { + targetStage = stageMax(targetStage, "partial_derisk"); + reasons.push( + buildReason( + "drawdown_partial", + "partial_derisk", + "Primary asset drawdown crossed partial de-risk threshold", + { drawdown_bps: Number(metrics.drawdownBps.toFixed(2)) }, + ), + ); + } + + if (metrics.drawdownBps >= policy.fullExitDrawdownBps) { + targetStage = stageMax(targetStage, "full_exit"); + reasons.push( + buildReason( + "drawdown_full_exit", + "full_exit", + "Primary asset drawdown crossed full exit threshold", + { drawdown_bps: Number(metrics.drawdownBps.toFixed(2)) }, + ), + ); + } + + if (metrics.totalLiquidValueFiat <= policy.portfolioFloorFiat) { + targetStage = stageMax(targetStage, "partial_derisk"); + reasons.push( + buildReason( + "portfolio_floor_breach", + "partial_derisk", + "Portfolio liquid value fell below the configured floor", + { + total_liquid_fiat: Number(metrics.totalLiquidValueFiat.toFixed(2)), + floor_fiat: policy.portfolioFloorFiat, + }, + ), + ); + } + + if (metrics.totalLiquidValueFiat <= policy.emergencyPortfolioFloorFiat) { + targetStage = stageMax(targetStage, "full_exit"); + reasons.push( + buildReason( + "portfolio_emergency_floor_breach", + "full_exit", + "Portfolio liquid value fell below the emergency floor", + { + total_liquid_fiat: Number(metrics.totalLiquidValueFiat.toFixed(2)), + floor_fiat: policy.emergencyPortfolioFloorFiat, + }, + ), + ); + } + + for (const assetRule of policy.assetRules.filter((rule) => rule.enabled)) { + const liquidValue = metrics.assetLiquidValues[assetRule.assetId] ?? 0; + const hasExposure = treasury.positions.some( + (position) => position.assetId === assetRule.assetId && position.amount > 0, + ); + if (!hasExposure) { + continue; + } + + if (liquidValue <= assetRule.protectedFloorFiat) { + targetStage = stageMax(targetStage, "partial_derisk"); + reasons.push( + buildReason( + "asset_floor_breach", + "partial_derisk", + `${assetRule.symbol} liquid value fell below its protected floor`, + { + asset_id: assetRule.assetId, + liquid_value_fiat: Number(liquidValue.toFixed(2)), + floor_fiat: assetRule.protectedFloorFiat, + }, + ), + ); + } + if (liquidValue <= assetRule.emergencyExitFloorFiat) { + targetStage = stageMax(targetStage, "full_exit"); + reasons.push( + buildReason( + "asset_emergency_floor_breach", + "full_exit", + `${assetRule.symbol} liquid value fell below its emergency floor`, + { + asset_id: assetRule.assetId, + liquid_value_fiat: Number(liquidValue.toFixed(2)), + floor_fiat: assetRule.emergencyExitFloorFiat, + }, + ), + ); + } + } + + const reentryAllowed = + (treasury.stage === "partial_derisk" || treasury.stage === "full_exit") && + metrics.drawdownBps <= policy.reentryDrawdownBps && + metrics.totalLiquidValueFiat > policy.portfolioFloorFiat && + metrics.stableLiquidValueFiat > policy.portfolioFloorFiat; + + let nextStage = targetStage; + let intent: ExecutionIntent | undefined; + + if (cooldownRemainingUs > 0 && targetStage !== "frozen") { + reasons.push( + buildReason( + "cooldown_active", + treasury.stage, + "Cooldown prevents an automatic state transition", + { cooldown_remaining_us: cooldownRemainingUs }, + ), + ); + nextStage = treasury.stage; + } else if (reentryAllowed && targetStage === "normal") { + reasons.push( + buildReason( + "reentry_window", + "normal", + "Recovery conditions satisfied the automatic re-entry band", + { drawdown_bps: Number(metrics.drawdownBps.toFixed(2)) }, + ), + ); + nextStage = "normal"; + intent = buildReentryIntent( + treasury, + policy, + snapshots, + routes, + nowUs, + reasons, + ); + if (!intent) { + reasons.push( + buildReason( + "reentry_route_unavailable", + treasury.stage, + "Recovery conditions are satisfied but no approved route is available for re-entry", + ), + ); + nextStage = treasury.stage; + } + } else if (STAGE_SEVERITY[targetStage] > STAGE_SEVERITY[treasury.stage]) { + nextStage = targetStage; + if (targetStage === "partial_derisk" || targetStage === "full_exit") { + intent = buildDeriskIntent( + targetStage, + treasury, + policy, + snapshots, + routes, + nowUs, + reasons, + ); + if (!intent) { + reasons.push( + buildReason( + "execution_route_unavailable", + targetStage, + "Risk stage changed but no approved route is available for execution", + ), + ); + } + } + } else { + nextStage = treasury.stage; + } + + return { + nowUs, + currentStage: treasury.stage, + nextStage, + metrics, + reasons, + intent, + cooldownRemainingUs, + shouldFreeze: false, + }; +} diff --git a/lazer/cardano/guards-one/source/packages/core/src/fixtures.ts b/lazer/cardano/guards-one/source/packages/core/src/fixtures.ts new file mode 100644 index 00000000..7b5a4ea0 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/core/src/fixtures.ts @@ -0,0 +1,130 @@ +import type { + OracleSnapshot, + PolicyConfig, + RouteSpec, + TreasuryState, +} from "./types.js"; + +export const samplePolicy: PolicyConfig = { + policyId: "policy-treasury-guard-v4", + primaryAssetId: "ada", + primaryFeedId: "pyth-ada-usd", + stableAssetId: "usdm", + approvedRouteIds: [ + "cardano-minswap-ada-usdm", + "cardano-minswap-usdm-ada", + ], + haircutBps: 300, + maxStaleUs: 120_000_000, + maxConfidenceBps: 150, + cooldownUs: 30_000_000, + watchDrawdownBps: 500, + partialDrawdownBps: 900, + fullExitDrawdownBps: 1_500, + reentryDrawdownBps: 250, + portfolioFloorFiat: 4_500, + emergencyPortfolioFloorFiat: 3_400, + partialStableTargetRatio: 0.55, + reentryRiskTargetRatio: 0.65, + assetRules: [ + { + assetId: "ada", + symbol: "ADA", + feedId: "pyth-ada-usd", + protectedFloorFiat: 2_700, + emergencyExitFloorFiat: 2_050, + enabled: true, + }, + ], +}; + +export const sampleRoutes: RouteSpec[] = [ + { + routeId: "cardano-minswap-ada-usdm", + venue: "Minswap", + chainId: "cardano", + fromAssetId: "ada", + toAssetId: "usdm", + maxSlippageBps: 120, + live: true, + notes: "Primary approved route for Cardano execution bucket", + }, + { + routeId: "cardano-minswap-usdm-ada", + venue: "Minswap", + chainId: "cardano", + fromAssetId: "usdm", + toAssetId: "ada", + maxSlippageBps: 120, + live: true, + notes: "Re-entry route for the same pair", + }, +]; + +export const sampleTreasury: TreasuryState = { + vaultId: "vault-anaconda-demo", + name: "Anaconda Treasury Demo", + chainId: "cardano", + stage: "normal", + positions: [ + { + assetId: "ada", + symbol: "ADA", + amount: 10_000, + decimals: 6, + role: "risk", + feedId: "pyth-ada-usd", + bucket: "hot", + }, + { + assetId: "usdm", + symbol: "USDM", + amount: 1_500, + decimals: 6, + role: "stable", + feedId: "stable-usdm-usd", + bucket: "hot", + }, + ], + governanceSigners: ["gov-1", "gov-2", "gov-3"], + riskManagers: ["risk-1"], + keepers: ["keeper-1", "keeper-2"], + viewers: ["viewer-1"], + safeAddresses: ["addr_test_safe_1"], + executionHotWallet: "addr_test_hot_1", + governanceWallet: "addr_test_gov_1", + lastTransitionUs: 0, +}; + +export function buildSnapshots( + overrides?: Partial>>, +): Record { + return { + ada: { + snapshotId: "snapshot-ada", + feedId: "pyth-ada-usd", + assetId: "ada", + symbol: "ADA/USD", + price: 72, + emaPrice: 80, + confidence: 0.4, + exponent: -2, + feedUpdateTimestampUs: 1_000_000, + observedAtUs: 1_000_000, + ...overrides?.ada, + }, + usdm: { + snapshotId: "snapshot-usdm", + feedId: "stable-usdm-usd", + assetId: "usdm", + symbol: "USDM/USD", + price: 1_000_000, + emaPrice: 1_000_000, + confidence: 500, + exponent: -6, + feedUpdateTimestampUs: 1_000_000, + observedAtUs: 1_000_000, + ...overrides?.usdm, + }, + }; +} diff --git a/lazer/cardano/guards-one/source/packages/core/src/hash.ts b/lazer/cardano/guards-one/source/packages/core/src/hash.ts new file mode 100644 index 00000000..3ddfa345 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/core/src/hash.ts @@ -0,0 +1,24 @@ +import { createHash } from "node:crypto"; + +function stableValue(input: unknown): unknown { + if (Array.isArray(input)) { + return input.map(stableValue); + } + + if (input && typeof input === "object") { + return Object.keys(input as Record) + .sort() + .reduce>((accumulator, key) => { + accumulator[key] = stableValue((input as Record)[key]); + return accumulator; + }, {}); + } + + return input; +} + +export function stableHash(input: unknown): string { + return createHash("sha256") + .update(JSON.stringify(stableValue(input))) + .digest("hex"); +} diff --git a/lazer/cardano/guards-one/source/packages/core/src/index.ts b/lazer/cardano/guards-one/source/packages/core/src/index.ts new file mode 100644 index 00000000..12bfd1c6 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/core/src/index.ts @@ -0,0 +1,6 @@ +export * from "./capabilities.js"; +export * from "./engine.js"; +export * from "./fixtures.js"; +export * from "./hash.js"; +export * from "./math.js"; +export * from "./types.js"; diff --git a/lazer/cardano/guards-one/source/packages/core/src/math.ts b/lazer/cardano/guards-one/source/packages/core/src/math.ts new file mode 100644 index 00000000..39e76a19 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/core/src/math.ts @@ -0,0 +1,135 @@ +import type { + OracleSnapshot, + PolicyConfig, + PortfolioMetrics, + RiskStage, + TreasuryPosition, +} from "./types.js"; + +export const STAGE_SEVERITY = { + normal: 0, + watch: 1, + partial_derisk: 2, + full_exit: 3, + frozen: 4, +} as const; + +export function stageAtLeast( + current: RiskStage, + target: RiskStage, +): boolean { + return STAGE_SEVERITY[current] >= STAGE_SEVERITY[target]; +} + +export function stageMax(left: RiskStage, right: RiskStage): RiskStage { + return STAGE_SEVERITY[left] >= STAGE_SEVERITY[right] ? left : right; +} + +export function toDecimalPrice(snapshot: OracleSnapshot): number { + return snapshot.price * 10 ** snapshot.exponent; +} + +export function toDecimalEma(snapshot: OracleSnapshot): number { + return snapshot.emaPrice * 10 ** snapshot.exponent; +} + +export function confidenceBps(snapshot: OracleSnapshot): number { + const price = Math.abs(toDecimalPrice(snapshot)); + if (price === 0) { + return Number.POSITIVE_INFINITY; + } + + return (snapshot.confidence * 10 ** snapshot.exponent) / price * 10_000; +} + +export function isSnapshotStale( + snapshot: OracleSnapshot, + nowUs: number, + maxStaleUs: number, +): boolean { + return nowUs - snapshot.feedUpdateTimestampUs > maxStaleUs; +} + +export function computeDrawdownBps(snapshot: OracleSnapshot): number { + const price = toDecimalPrice(snapshot); + const ema = toDecimalEma(snapshot); + if (ema <= 0 || price >= ema) { + return 0; + } + + return ((ema - price) / ema) * 10_000; +} + +export function applyHaircut(value: number, haircutBps: number): number { + return value * (1 - haircutBps / 10_000); +} + +export function computePositionLiquidValue( + position: TreasuryPosition, + snapshot: OracleSnapshot | undefined, + policy: PolicyConfig, +): number { + if (position.role === "stable") { + if (!snapshot) { + return position.amount; + } + + return position.amount * toDecimalPrice(snapshot); + } + + if (!snapshot) { + return 0; + } + + const price = toDecimalPrice(snapshot); + return applyHaircut(position.amount * price, policy.haircutBps); +} + +export function summarizePortfolio( + positions: TreasuryPosition[], + snapshots: Record, + policy: PolicyConfig, +): PortfolioMetrics { + const assetLiquidValues: Record = {}; + const priceMap: Record = {}; + let totalLiquidValueFiat = 0; + let stableLiquidValueFiat = 0; + let riskLiquidValueFiat = 0; + let drawdownBps = 0; + + for (const position of positions) { + const snapshot = snapshots[position.assetId]; + const liquidValue = computePositionLiquidValue(position, snapshot, policy); + assetLiquidValues[position.assetId] = + (assetLiquidValues[position.assetId] ?? 0) + liquidValue; + + if (snapshot) { + priceMap[position.assetId] = toDecimalPrice(snapshot); + } else if (position.role === "stable") { + priceMap[position.assetId] = 1; + } + + totalLiquidValueFiat += liquidValue; + if (position.role === "stable") { + stableLiquidValueFiat += liquidValue; + } else { + riskLiquidValueFiat += liquidValue; + if (position.assetId === policy.primaryAssetId && snapshot) { + drawdownBps = computeDrawdownBps(snapshot); + } + } + } + + const stableRatio = + totalLiquidValueFiat === 0 ? 0 : stableLiquidValueFiat / totalLiquidValueFiat; + + return { + totalLiquidValueFiat, + stableLiquidValueFiat, + riskLiquidValueFiat, + stableRatio, + drawdownBps, + assetLiquidValues, + priceMap, + }; +} diff --git a/lazer/cardano/guards-one/source/packages/core/src/types.ts b/lazer/cardano/guards-one/source/packages/core/src/types.ts new file mode 100644 index 00000000..bec937f0 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/core/src/types.ts @@ -0,0 +1,183 @@ +export type ChainId = "cardano" | "svm" | "evm"; + +export type Role = "governance" | "risk_manager" | "keeper" | "viewer"; + +export type RiskStage = + | "normal" + | "watch" + | "partial_derisk" + | "full_exit" + | "frozen"; + +export type ExecutionKind = "derisk_swap" | "reentry_swap"; + +export interface RouteSpec { + routeId: string; + venue: string; + chainId: ChainId; + fromAssetId: string; + toAssetId: string; + maxSlippageBps: number; + live: boolean; + notes?: string; +} + +export interface AssetRiskRule { + assetId: string; + symbol: string; + feedId: string; + protectedFloorFiat: number; + emergencyExitFloorFiat: number; + enabled: boolean; +} + +export interface PolicyConfig { + policyId: string; + primaryAssetId: string; + primaryFeedId: string; + stableAssetId: string; + approvedRouteIds: string[]; + haircutBps: number; + maxStaleUs: number; + maxConfidenceBps: number; + cooldownUs: number; + watchDrawdownBps: number; + partialDrawdownBps: number; + fullExitDrawdownBps: number; + reentryDrawdownBps: number; + portfolioFloorFiat: number; + emergencyPortfolioFloorFiat: number; + partialStableTargetRatio: number; + reentryRiskTargetRatio: number; + assetRules: AssetRiskRule[]; +} + +export interface OracleSnapshot { + snapshotId: string; + feedId: string; + assetId: string; + symbol: string; + price: number; + emaPrice: number; + confidence: number; + exponent: number; + feedUpdateTimestampUs: number; + observedAtUs: number; + publisherCount?: number; + marketSession?: string; +} + +export interface TreasuryPosition { + assetId: string; + symbol: string; + amount: number; + decimals: number; + role: "risk" | "stable"; + feedId: string; + bucket: "cold" | "hot"; +} + +export interface TreasuryState { + vaultId: string; + name: string; + chainId: ChainId; + stage: RiskStage; + positions: TreasuryPosition[]; + governanceSigners: string[]; + riskManagers: string[]; + keepers: string[]; + viewers: string[]; + safeAddresses: string[]; + executionHotWallet: string; + governanceWallet: string; + lastTransitionUs: number; + currentIntentId?: string | undefined; +} + +export interface PortfolioMetrics { + totalLiquidValueFiat: number; + stableLiquidValueFiat: number; + riskLiquidValueFiat: number; + stableRatio: number; + drawdownBps: number; + assetLiquidValues: Record; + priceMap: Record; +} + +export interface EvaluationReason { + code: string; + severity: RiskStage; + message: string; + details?: Record | undefined; +} + +export interface ExecutionIntent { + intentId: string; + vaultId: string; + chainId: ChainId; + kind: ExecutionKind; + stage: RiskStage; + sourceAssetId: string; + destinationAssetId: string; + routeId: string; + maxSellAmount: number; + minBuyAmount: number; + expiryUs: number; + reasonHash: string; + snapshotIds: string[]; + createdAtUs: number; +} + +export interface ExecutionResult { + intentId: string; + vaultId: string; + chainId: ChainId; + sourceAssetId: string; + destinationAssetId: string; + soldAmount: number; + boughtAmount: number; + averagePrice: number; + txHash: string; + executedAtUs: number; + routeId: string; +} + +export interface RiskAssessment { + nowUs: number; + currentStage: RiskStage; + nextStage: RiskStage; + metrics: PortfolioMetrics; + reasons: EvaluationReason[]; + intent?: ExecutionIntent | undefined; + cooldownRemainingUs: number; + shouldFreeze: boolean; +} + +export interface ChainCapabilities { + chainId: ChainId; + live: boolean; + supportsOracleVerifiedExecution: boolean; + supportsHotColdBuckets: boolean; + supportsAutoSwap: boolean; + supportsAutoReentry: boolean; + supportsLiveDeployments: boolean; + notes: string[]; +} + +export interface TreasuryConnector { + chainId: ChainId; + capabilities: ChainCapabilities; + describeExecutionConstraints(): string[]; + simulateRoute( + intent: ExecutionIntent, + priceMap: Record, + ): Pick< + ExecutionResult, + | "sourceAssetId" + | "destinationAssetId" + | "soldAmount" + | "boughtAmount" + | "averagePrice" + | "routeId" + >; +} diff --git a/lazer/cardano/guards-one/source/packages/core/tests/engine.test.ts b/lazer/cardano/guards-one/source/packages/core/tests/engine.test.ts new file mode 100644 index 00000000..4293a931 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/core/tests/engine.test.ts @@ -0,0 +1,209 @@ +import { describe, expect, it } from "vitest"; +import { + buildSnapshots, + evaluateRiskLadder, + samplePolicy, + sampleRoutes, + sampleTreasury, +} from "../src/index.js"; + +describe("core risk ladder", () => { + it("computes partial de-risk when drawdown breaches the medium band", () => { + const assessment = evaluateRiskLadder( + sampleTreasury, + samplePolicy, + buildSnapshots(), + sampleRoutes, + 100_000_000, + ); + + expect(assessment.nextStage).toBe("partial_derisk"); + expect(assessment.intent?.kind).toBe("derisk_swap"); + expect(assessment.intent?.maxSellAmount).toBeGreaterThan(0); + }); + + it("promotes to full exit on emergency floor breach", () => { + const treasury = { + ...sampleTreasury, + positions: sampleTreasury.positions.map((position) => + position.assetId === "usdm" ? { ...position, amount: 300 } : position, + ), + }; + const snapshots = buildSnapshots({ + ada: { price: 42, emaPrice: 80, snapshotId: "snapshot-ada-2" }, + }); + + const assessment = evaluateRiskLadder( + treasury, + samplePolicy, + snapshots, + sampleRoutes, + 100_000_000, + ); + + expect(assessment.nextStage).toBe("full_exit"); + expect(assessment.intent?.stage).toBe("full_exit"); + }); + + it("freezes when the primary snapshot is stale", () => { + const assessment = evaluateRiskLadder( + sampleTreasury, + samplePolicy, + buildSnapshots({ + ada: { feedUpdateTimestampUs: 1_000_000, observedAtUs: 1_000_000 }, + }), + sampleRoutes, + 500_000_000, + ); + + expect(assessment.nextStage).toBe("frozen"); + expect(assessment.shouldFreeze).toBe(true); + expect(assessment.reasons[0]?.code).toBe("stale_feed"); + }); + + it("triggers a reentry swap after recovery and hysteresis", () => { + const treasury = { + ...sampleTreasury, + stage: "partial_derisk" as const, + lastTransitionUs: 0, + positions: sampleTreasury.positions.map((position) => + position.assetId === "ada" + ? { ...position, amount: 6_000 } + : { ...position, amount: 4_800 }, + ), + }; + const snapshots = buildSnapshots({ + ada: { + price: 79, + emaPrice: 80, + confidence: 0.2, + snapshotId: "snapshot-ada-reentry", + }, + }); + + const assessment = evaluateRiskLadder( + treasury, + samplePolicy, + snapshots, + sampleRoutes, + 90_000_000, + ); + + expect(assessment.nextStage).toBe("normal"); + expect(assessment.intent?.kind).toBe("reentry_swap"); + expect(assessment.intent?.destinationAssetId).toBe("ada"); + }); + + it("respects cooldown and blocks new automatic transitions", () => { + const treasury = { + ...sampleTreasury, + lastTransitionUs: 9_000_000, + }; + + const assessment = evaluateRiskLadder( + treasury, + samplePolicy, + buildSnapshots(), + sampleRoutes, + 10_000_000, + ); + + expect(assessment.nextStage).toBe("normal"); + expect(assessment.intent).toBeUndefined(); + expect(assessment.cooldownRemainingUs).toBeGreaterThan(0); + }); + + it("keeps the stage change but emits no intent when no approved route exists", () => { + const assessment = evaluateRiskLadder( + sampleTreasury, + { + ...samplePolicy, + approvedRouteIds: [], + }, + buildSnapshots(), + sampleRoutes, + 100_000_000, + ); + + expect(assessment.nextStage).toBe("partial_derisk"); + expect(assessment.intent).toBeUndefined(); + expect( + assessment.reasons.some((reason) => reason.code === "execution_route_unavailable"), + ).toBe(true); + }); + + it("does not select routes from another chain or disabled routes", () => { + const foreignRoutes = sampleRoutes.map((route, index) => ({ + ...route, + chainId: index === 0 ? "evm" as const : route.chainId, + live: index === 1 ? false : route.live, + })); + + const assessment = evaluateRiskLadder( + sampleTreasury, + samplePolicy, + buildSnapshots(), + foreignRoutes, + 100_000_000, + ); + + expect(assessment.nextStage).toBe("partial_derisk"); + expect(assessment.intent).toBeUndefined(); + expect( + assessment.reasons.some((reason) => reason.code === "execution_route_unavailable"), + ).toBe(true); + }); + + it("builds deterministic snapshot hashes regardless of input object order", () => { + const firstAssessment = evaluateRiskLadder( + sampleTreasury, + samplePolicy, + buildSnapshots(), + sampleRoutes, + 100_000_000, + ); + const reversedSnapshots = buildSnapshots(); + const secondAssessment = evaluateRiskLadder( + sampleTreasury, + samplePolicy, + { + usdm: reversedSnapshots.usdm!, + ada: reversedSnapshots.ada!, + }, + sampleRoutes, + 100_000_000, + ); + + expect(firstAssessment.intent?.snapshotIds).toEqual(secondAssessment.intent?.snapshotIds); + expect(firstAssessment.intent?.reasonHash).toBe(secondAssessment.intent?.reasonHash); + }); + + it("ignores asset floor breaches when the risky asset is fully exited", () => { + const treasury = { + ...sampleTreasury, + stage: "full_exit" as const, + positions: sampleTreasury.positions.map((position) => + position.assetId === "ada" + ? { ...position, amount: 0 } + : { ...position, amount: 7_200 }, + ), + }; + const assessment = evaluateRiskLadder( + treasury, + samplePolicy, + buildSnapshots({ + ada: { + price: 79, + emaPrice: 80, + confidence: 0.2, + snapshotId: "snapshot-ada-flat", + }, + }), + sampleRoutes, + 100_000_000, + ); + + expect(assessment.nextStage).toBe("normal"); + expect(assessment.intent?.kind).toBe("reentry_swap"); + }); +}); diff --git a/lazer/cardano/guards-one/source/packages/core/tests/math.test.ts b/lazer/cardano/guards-one/source/packages/core/tests/math.test.ts new file mode 100644 index 00000000..af572af1 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/core/tests/math.test.ts @@ -0,0 +1,71 @@ +import { describe, expect, it } from "vitest"; +import { + applyHaircut, + buildSnapshots, + computeDrawdownBps, + confidenceBps, + samplePolicy, + sampleTreasury, + summarizePortfolio, + toDecimalEma, + toDecimalPrice, +} from "../src/index.js"; + +describe("core math helpers", () => { + it("converts Pyth values into decimal price and ema", () => { + const snapshots = buildSnapshots(); + expect(toDecimalPrice(snapshots.ada!)).toBe(0.72); + expect(toDecimalEma(snapshots.ada!)).toBe(0.8); + }); + + it("computes relative confidence in bps", () => { + const snapshots = buildSnapshots(); + expect(confidenceBps(snapshots.ada!)).toBeCloseTo(55.5555, 2); + }); + + it("computes drawdown only when price is below ema", () => { + const snapshots = buildSnapshots(); + expect(computeDrawdownBps(snapshots.ada!)).toBeCloseTo(1000, 4); + expect( + computeDrawdownBps({ + ...snapshots.ada!, + price: 81, + }), + ).toBe(0); + }); + + it("applies the configured haircut", () => { + expect(applyHaircut(100, 300)).toBe(97); + }); + + it("summarizes liquid portfolio values and ratios", () => { + const metrics = summarizePortfolio( + sampleTreasury.positions, + buildSnapshots(), + samplePolicy, + ); + + expect(metrics.totalLiquidValueFiat).toBeCloseTo(8484, 2); + expect(metrics.riskLiquidValueFiat).toBeCloseTo(6984, 2); + expect(metrics.stableLiquidValueFiat).toBe(1500); + expect(metrics.stableRatio).toBeCloseTo(0.1768, 3); + }); + + it("values stable assets using the oracle price when a depeg is present", () => { + const metrics = summarizePortfolio( + sampleTreasury.positions, + buildSnapshots({ + usdm: { + price: 970_000, + emaPrice: 990_000, + snapshotId: "snapshot-usdm-depeg", + }, + }), + samplePolicy, + ); + + expect(metrics.stableLiquidValueFiat).toBe(1455); + expect(metrics.totalLiquidValueFiat).toBeCloseTo(8439, 2); + expect(metrics.stableRatio).toBeCloseTo(1455 / 8439, 4); + }); +}); diff --git a/lazer/cardano/guards-one/source/packages/evm/package.json b/lazer/cardano/guards-one/source/packages/evm/package.json new file mode 100644 index 00000000..ca74ee66 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/evm/package.json @@ -0,0 +1,14 @@ +{ + "name": "@anaconda/evm", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@anaconda/core": "workspace:*" + } +} diff --git a/lazer/cardano/guards-one/source/packages/evm/src/fixtures.ts b/lazer/cardano/guards-one/source/packages/evm/src/fixtures.ts new file mode 100644 index 00000000..fbdc1027 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/evm/src/fixtures.ts @@ -0,0 +1,23 @@ +import type { RouteSpec, TreasuryProfile } from './index.js'; + +export const evmFixtures = { + treasury: { + treasuryId: 'evm-demo-treasury', + chainId: 'evm', + vaultMode: 'watch', + governanceThreshold: 3, + approvedRiskOffAssets: ['USDC', 'USDT'], + protectedAssets: ['ETH', 'BTC'], + } satisfies TreasuryProfile, + route: { + fromAsset: 'ETH', + toAsset: 'USDC', + venue: 'simulated-evm-route', + maxSlippageBps: 100, + routeId: 'evm-route-01', + } satisfies RouteSpec, + simulationPrice: { + fromAssetPrice: 3200, + toAssetPrice: 1, + }, +} as const; diff --git a/lazer/cardano/guards-one/source/packages/evm/src/index.ts b/lazer/cardano/guards-one/source/packages/evm/src/index.ts new file mode 100644 index 00000000..10062b7d --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/evm/src/index.ts @@ -0,0 +1,151 @@ +import { + baseCapabilities, + roleModel, + type ChainCapabilities, + type ChainId as CoreChainId, + type RiskStage, + type Role as CoreRole, +} from '../../core/src/index.js'; +import { evmFixtures } from './fixtures.js'; + +export type Role = CoreRole; + +export type ChainId = Extract; + +export type VaultMode = RiskStage; + +export interface TreasuryProfile { + treasuryId: string; + chainId: ChainId; + vaultMode: VaultMode; + governanceThreshold: number; + approvedRiskOffAssets: readonly string[]; + protectedAssets: readonly string[]; +} + +export interface RouteSpec { + routeId: string; + fromAsset: string; + toAsset: string; + venue: string; + maxSlippageBps: number; +} + +export interface RouteSimulationInput { + route: RouteSpec; + amountIn: number; + fromAssetPrice: number; + toAssetPrice: number; + haircutBps?: number; +} + +export interface RouteSimulationResult { + chainId: ChainId; + routeId: string; + canExecute: boolean; + estimatedOutput: number; + estimatedValueUsd: number; + estimatedImpactBps: number; + executionNotes: readonly string[]; +} + +export interface ExecutionConstraints { + isLive: false; + executionMode: 'scaffold'; + requiresPreapproval: true; + routeSimulationOnly: true; + maxRouteAgeSeconds: number; + allowedRoles: readonly Role[]; + notes: readonly string[]; +} + +export interface TreasuryConnector { + chainId: ChainId; + roles: readonly Role[]; + capabilities: ChainCapabilities; + executionConstraints: ExecutionConstraints; + simulateRoute(input: RouteSimulationInput): RouteSimulationResult; + describeAvailability(): string; +} + +export const chainId: ChainId = 'evm'; + +export const capabilities: ChainCapabilities = baseCapabilities.evm; + +export const executionConstraints: ExecutionConstraints = { + isLive: false, + executionMode: 'scaffold', + requiresPreapproval: true, + routeSimulationOnly: true, + maxRouteAgeSeconds: 20, + allowedRoles: ['governance', 'risk_manager', 'keeper', 'viewer'], + notes: [ + 'EVM support is scaffolded for phase 2.', + 'Route simulation is deterministic and does not submit real transactions.', + 'Execution must be approved by governance before any live adapter is added.', + ], +}; + +export const roles: readonly Role[] = roleModel; + +export const createEvmConnector = (): TreasuryConnector => ({ + chainId, + roles, + capabilities, + executionConstraints, + simulateRoute: (input) => { + const validationNotes: string[] = []; + + if (!Number.isFinite(input.amountIn) || input.amountIn <= 0) { + validationNotes.push('Invalid amountIn: must be finite and > 0.'); + } + if (!Number.isFinite(input.fromAssetPrice) || input.fromAssetPrice <= 0) { + validationNotes.push('Invalid fromAssetPrice: must be finite and > 0.'); + } + if (!Number.isFinite(input.toAssetPrice) || input.toAssetPrice <= 0) { + validationNotes.push('Invalid toAssetPrice: must be finite and > 0.'); + } + + if (validationNotes.length > 0) { + return { + chainId, + routeId: input.route.routeId, + canExecute: false, + estimatedOutput: 0, + estimatedValueUsd: 0, + estimatedImpactBps: 0, + executionNotes: [ + 'Scaffold connector only.', + `Venue ${input.route.venue} is simulated, not live.`, + 'Route blocked due to invalid simulation inputs.', + ...validationNotes, + ], + }; + } + + const haircutBps = input.haircutBps ?? 150; + const grossValue = input.amountIn * input.fromAssetPrice; + const effectiveOutputValue = grossValue * (1 - haircutBps / 10_000); + const estimatedOutput = effectiveOutputValue / input.toAssetPrice; + const estimatedImpactBps = Math.min(input.route.maxSlippageBps, haircutBps); + const canExecute = estimatedOutput > 0 && input.route.maxSlippageBps <= 300; + + return { + chainId, + routeId: input.route.routeId, + canExecute, + estimatedOutput, + estimatedValueUsd: effectiveOutputValue, + estimatedImpactBps, + executionNotes: [ + 'Scaffold connector only.', + `Venue ${input.route.venue} is simulated, not live.`, + canExecute ? 'Route is eligible for future live adapter work.' : 'Route blocked by constraint or invalid sizing.', + ], + }; + }, + describeAvailability: () => + 'EVM connector is scaffold-only. It exposes policy, route simulation and role surfaces for future live integration.', +}); + +export const fixtures = evmFixtures; diff --git a/lazer/cardano/guards-one/source/packages/evm/tests/index.test.ts b/lazer/cardano/guards-one/source/packages/evm/tests/index.test.ts new file mode 100644 index 00000000..0d68a2ff --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/evm/tests/index.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { chainId, capabilities, createEvmConnector, executionConstraints, fixtures, roles } from '../src/index.js'; + +describe('@anaconda/evm', () => { + it('exposes a scaffold-only capability matrix', () => { + expect(chainId).toBe('evm'); + expect(capabilities.live).toBe(false); + expect(capabilities.supportsAutoSwap).toBe(true); + expect(executionConstraints.isLive).toBe(false); + expect(roles).toEqual(['governance', 'risk_manager', 'keeper', 'viewer']); + }); + + it('simulates routes using the fixture surface', () => { + const connector = createEvmConnector(); + const result = connector.simulateRoute({ + route: fixtures.route, + amountIn: 2, + fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, + toAssetPrice: fixtures.simulationPrice.toAssetPrice, + }); + + expect(result.chainId).toBe('evm'); + expect(result.canExecute).toBe(true); + expect(result.estimatedOutput).toBeGreaterThan(0); + expect(result.executionNotes[0]).toContain('Scaffold connector only.'); + }); + + it('blocks invalid or too-wide routes in simulation', () => { + const connector = createEvmConnector(); + const result = connector.simulateRoute({ + route: { + ...fixtures.route, + maxSlippageBps: 400, + }, + amountIn: 0, + fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, + toAssetPrice: fixtures.simulationPrice.toAssetPrice, + }); + + expect(result.canExecute).toBe(false); + }); + + it('rejects invalid pricing inputs', () => { + const connector = createEvmConnector(); + const result = connector.simulateRoute({ + route: fixtures.route, + amountIn: 2, + fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, + toAssetPrice: 0, + }); + + expect(result.canExecute).toBe(false); + expect(result.estimatedOutput).toBe(0); + expect(result.executionNotes.some((note) => note.includes('Invalid toAssetPrice'))).toBe(true); + }); +}); diff --git a/lazer/cardano/guards-one/source/packages/svm/package.json b/lazer/cardano/guards-one/source/packages/svm/package.json new file mode 100644 index 00000000..7f47cc4c --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/svm/package.json @@ -0,0 +1,14 @@ +{ + "name": "@anaconda/svm", + "version": "0.0.0", + "private": true, + "type": "module", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + }, + "dependencies": { + "@anaconda/core": "workspace:*" + } +} diff --git a/lazer/cardano/guards-one/source/packages/svm/src/fixtures.ts b/lazer/cardano/guards-one/source/packages/svm/src/fixtures.ts new file mode 100644 index 00000000..7ebaa56e --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/svm/src/fixtures.ts @@ -0,0 +1,23 @@ +import type { RouteSpec, TreasuryProfile } from './index.js'; + +export const svmFixtures = { + treasury: { + treasuryId: 'svm-demo-treasury', + chainId: 'svm', + vaultMode: 'watch', + governanceThreshold: 2, + approvedRiskOffAssets: ['USDC'], + protectedAssets: ['SOL', 'BTC'], + } satisfies TreasuryProfile, + route: { + fromAsset: 'SOL', + toAsset: 'USDC', + venue: 'simulated-svm-route', + maxSlippageBps: 75, + routeId: 'svm-route-01', + } satisfies RouteSpec, + simulationPrice: { + fromAssetPrice: 178.25, + toAssetPrice: 1, + }, +} as const; diff --git a/lazer/cardano/guards-one/source/packages/svm/src/index.ts b/lazer/cardano/guards-one/source/packages/svm/src/index.ts new file mode 100644 index 00000000..194cf3f7 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/svm/src/index.ts @@ -0,0 +1,151 @@ +import { + baseCapabilities, + roleModel, + type ChainCapabilities, + type ChainId as CoreChainId, + type RiskStage, + type Role as CoreRole, +} from '../../core/src/index.js'; +import { svmFixtures } from './fixtures.js'; + +export type Role = CoreRole; + +export type ChainId = Extract; + +export type VaultMode = RiskStage; + +export interface TreasuryProfile { + treasuryId: string; + chainId: ChainId; + vaultMode: VaultMode; + governanceThreshold: number; + approvedRiskOffAssets: readonly string[]; + protectedAssets: readonly string[]; +} + +export interface RouteSpec { + routeId: string; + fromAsset: string; + toAsset: string; + venue: string; + maxSlippageBps: number; +} + +export interface RouteSimulationInput { + route: RouteSpec; + amountIn: number; + fromAssetPrice: number; + toAssetPrice: number; + haircutBps?: number; +} + +export interface RouteSimulationResult { + chainId: ChainId; + routeId: string; + canExecute: boolean; + estimatedOutput: number; + estimatedValueUsd: number; + estimatedImpactBps: number; + executionNotes: readonly string[]; +} + +export interface ExecutionConstraints { + isLive: false; + executionMode: 'scaffold'; + requiresPreapproval: true; + routeSimulationOnly: true; + maxRouteAgeSeconds: number; + allowedRoles: readonly Role[]; + notes: readonly string[]; +} + +export interface TreasuryConnector { + chainId: ChainId; + roles: readonly Role[]; + capabilities: ChainCapabilities; + executionConstraints: ExecutionConstraints; + simulateRoute(input: RouteSimulationInput): RouteSimulationResult; + describeAvailability(): string; +} + +export const chainId: ChainId = 'svm'; + +export const capabilities: ChainCapabilities = baseCapabilities.svm; + +export const executionConstraints: ExecutionConstraints = { + isLive: false, + executionMode: 'scaffold', + requiresPreapproval: true, + routeSimulationOnly: true, + maxRouteAgeSeconds: 30, + allowedRoles: ['governance', 'risk_manager', 'keeper', 'viewer'], + notes: [ + 'SVM support is scaffolded for phase 2.', + 'Route simulation is deterministic and does not submit real transactions.', + 'Execution must be approved by governance before any live adapter is added.', + ], +}; + +export const roles: readonly Role[] = roleModel; + +export const createSvmConnector = (): TreasuryConnector => ({ + chainId, + roles, + capabilities, + executionConstraints, + simulateRoute: (input) => { + const validationNotes: string[] = []; + + if (!Number.isFinite(input.amountIn) || input.amountIn <= 0) { + validationNotes.push('Invalid amountIn: must be finite and > 0.'); + } + if (!Number.isFinite(input.fromAssetPrice) || input.fromAssetPrice <= 0) { + validationNotes.push('Invalid fromAssetPrice: must be finite and > 0.'); + } + if (!Number.isFinite(input.toAssetPrice) || input.toAssetPrice <= 0) { + validationNotes.push('Invalid toAssetPrice: must be finite and > 0.'); + } + + if (validationNotes.length > 0) { + return { + chainId, + routeId: input.route.routeId, + canExecute: false, + estimatedOutput: 0, + estimatedValueUsd: 0, + estimatedImpactBps: 0, + executionNotes: [ + 'Scaffold connector only.', + `Venue ${input.route.venue} is simulated, not live.`, + 'Route blocked due to invalid simulation inputs.', + ...validationNotes, + ], + }; + } + + const haircutBps = input.haircutBps ?? 125; + const grossValue = input.amountIn * input.fromAssetPrice; + const effectiveOutputValue = grossValue * (1 - haircutBps / 10_000); + const estimatedOutput = effectiveOutputValue / input.toAssetPrice; + const estimatedImpactBps = Math.min(input.route.maxSlippageBps, haircutBps); + const canExecute = estimatedOutput > 0 && input.route.maxSlippageBps <= 250; + + return { + chainId, + routeId: input.route.routeId, + canExecute, + estimatedOutput, + estimatedValueUsd: effectiveOutputValue, + estimatedImpactBps, + executionNotes: [ + 'Scaffold connector only.', + `Venue ${input.route.venue} is simulated, not live.`, + canExecute ? 'Route is eligible for future live adapter work.' : 'Route blocked by constraint or invalid sizing.', + ], + }; + }, + describeAvailability: () => + 'SVM connector is scaffold-only. It exposes policy, route simulation and role surfaces for future live integration.', +}); + +export const fixtures = svmFixtures; diff --git a/lazer/cardano/guards-one/source/packages/svm/tests/index.test.ts b/lazer/cardano/guards-one/source/packages/svm/tests/index.test.ts new file mode 100644 index 00000000..914d4598 --- /dev/null +++ b/lazer/cardano/guards-one/source/packages/svm/tests/index.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from 'vitest'; +import { chainId, capabilities, createSvmConnector, executionConstraints, fixtures, roles } from '../src/index.js'; + +describe('@anaconda/svm', () => { + it('exposes a scaffold-only capability matrix', () => { + expect(chainId).toBe('svm'); + expect(capabilities.live).toBe(false); + expect(capabilities.supportsAutoSwap).toBe(true); + expect(executionConstraints.isLive).toBe(false); + expect(roles).toEqual(['governance', 'risk_manager', 'keeper', 'viewer']); + }); + + it('simulates routes using the fixture surface', () => { + const connector = createSvmConnector(); + const result = connector.simulateRoute({ + route: fixtures.route, + amountIn: 10, + fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, + toAssetPrice: fixtures.simulationPrice.toAssetPrice, + }); + + expect(result.chainId).toBe('svm'); + expect(result.canExecute).toBe(true); + expect(result.estimatedOutput).toBeGreaterThan(0); + expect(result.executionNotes[0]).toContain('Scaffold connector only.'); + }); + + it('blocks invalid or too-wide routes in simulation', () => { + const connector = createSvmConnector(); + const result = connector.simulateRoute({ + route: { + ...fixtures.route, + maxSlippageBps: 400, + }, + amountIn: 0, + fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, + toAssetPrice: fixtures.simulationPrice.toAssetPrice, + }); + + expect(result.canExecute).toBe(false); + }); + + it('rejects invalid pricing inputs', () => { + const connector = createSvmConnector(); + const result = connector.simulateRoute({ + route: fixtures.route, + amountIn: 10, + fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, + toAssetPrice: 0, + }); + + expect(result.canExecute).toBe(false); + expect(result.estimatedOutput).toBe(0); + expect(result.executionNotes.some((note) => note.includes('Invalid toAssetPrice'))).toBe(true); + }); +}); diff --git a/lazer/cardano/guards-one/source/pnpm-lock.yaml b/lazer/cardano/guards-one/source/pnpm-lock.yaml new file mode 100644 index 00000000..820df2c3 --- /dev/null +++ b/lazer/cardano/guards-one/source/pnpm-lock.yaml @@ -0,0 +1,1090 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + devDependencies: + '@types/node': + specifier: ^24.0.0 + version: 24.12.0 + tsx: + specifier: ^4.20.3 + version: 4.21.0 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@24.12.0)(tsx@4.21.0) + + apps/backend: + dependencies: + '@anaconda/cardano': + specifier: workspace:* + version: link:../../packages/cardano + '@anaconda/core': + specifier: workspace:* + version: link:../../packages/core + dotenv: + specifier: ^17.2.3 + version: 17.3.1 + + packages/cardano: + dependencies: + '@anaconda/core': + specifier: workspace:* + version: link:../core + + packages/core: {} + + packages/evm: + dependencies: + '@anaconda/core': + specifier: workspace:* + version: link:../core + + packages/svm: + dependencies: + '@anaconda/core': + specifier: workspace:* + version: link:../core + +packages: + + '@esbuild/aix-ppc64@0.27.4': + resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.4': + resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.4': + resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.4': + resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.4': + resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.4': + resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.4': + resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.4': + resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.4': + resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.4': + resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.4': + resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.4': + resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.4': + resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.4': + resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.4': + resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.4': + resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.4': + resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.4': + resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.4': + resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.4': + resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.4': + resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.4': + resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.4': + resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.4': + resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.4': + resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.4': + resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@rollup/rollup-android-arm-eabi@4.60.0': + resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.0': + resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.0': + resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.0': + resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.0': + resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.0': + resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.0': + resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.0': + resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.0': + resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.0': + resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.0': + resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.0': + resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.0': + resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.0': + resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.0': + resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.0': + resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.0': + resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.0': + resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.0': + resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.0': + resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.0': + resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} + cpu: [x64] + os: [win32] + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/node@24.12.0': + resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} + + '@vitest/expect@3.2.4': + resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} + + '@vitest/mocker@3.2.4': + resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@3.2.4': + resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} + + '@vitest/runner@3.2.4': + resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} + + '@vitest/snapshot@3.2.4': + resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} + + '@vitest/spy@3.2.4': + resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} + + '@vitest/utils@3.2.4': + resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + cac@6.7.14: + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} + + chai@5.3.3: + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} + + check-error@2.1.3: + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + deep-eql@5.0.2: + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + + esbuild@0.27.4: + resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} + engines: {node: '>=18'} + hasBin: true + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + + js-tokens@9.0.1: + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + + loupe@3.2.1: + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + pathval@2.0.1: + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.3: + resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} + engines: {node: '>=12'} + + postcss@8.5.8: + resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} + engines: {node: ^10 || ^12 || >=14} + + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + + rollup@4.60.0: + resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + + strip-literal@3.1.0: + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@0.3.2: + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + + tinyglobby@0.2.15: + resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} + engines: {node: '>=12.0.0'} + + tinypool@1.1.1: + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} + + tinyrainbow@2.0.0: + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} + + tinyspy@4.0.4: + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} + + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + vite-node@3.2.4: + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + + vite@7.3.1: + resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + jiti: '>=1.21.0' + less: ^4.0.0 + lightningcss: ^1.21.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 + jiti: + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@3.2.4: + resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@types/debug': ^4.1.12 + '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 + '@vitest/browser': 3.2.4 + '@vitest/ui': 3.2.4 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@types/debug': + optional: true + '@types/node': + optional: true + '@vitest/browser': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + +snapshots: + + '@esbuild/aix-ppc64@0.27.4': + optional: true + + '@esbuild/android-arm64@0.27.4': + optional: true + + '@esbuild/android-arm@0.27.4': + optional: true + + '@esbuild/android-x64@0.27.4': + optional: true + + '@esbuild/darwin-arm64@0.27.4': + optional: true + + '@esbuild/darwin-x64@0.27.4': + optional: true + + '@esbuild/freebsd-arm64@0.27.4': + optional: true + + '@esbuild/freebsd-x64@0.27.4': + optional: true + + '@esbuild/linux-arm64@0.27.4': + optional: true + + '@esbuild/linux-arm@0.27.4': + optional: true + + '@esbuild/linux-ia32@0.27.4': + optional: true + + '@esbuild/linux-loong64@0.27.4': + optional: true + + '@esbuild/linux-mips64el@0.27.4': + optional: true + + '@esbuild/linux-ppc64@0.27.4': + optional: true + + '@esbuild/linux-riscv64@0.27.4': + optional: true + + '@esbuild/linux-s390x@0.27.4': + optional: true + + '@esbuild/linux-x64@0.27.4': + optional: true + + '@esbuild/netbsd-arm64@0.27.4': + optional: true + + '@esbuild/netbsd-x64@0.27.4': + optional: true + + '@esbuild/openbsd-arm64@0.27.4': + optional: true + + '@esbuild/openbsd-x64@0.27.4': + optional: true + + '@esbuild/openharmony-arm64@0.27.4': + optional: true + + '@esbuild/sunos-x64@0.27.4': + optional: true + + '@esbuild/win32-arm64@0.27.4': + optional: true + + '@esbuild/win32-ia32@0.27.4': + optional: true + + '@esbuild/win32-x64@0.27.4': + optional: true + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@rollup/rollup-android-arm-eabi@4.60.0': + optional: true + + '@rollup/rollup-android-arm64@4.60.0': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.0': + optional: true + + '@rollup/rollup-darwin-x64@4.60.0': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.0': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.0': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.0': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.0': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.0': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.0': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.0': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.0': + optional: true + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/estree@1.0.8': {} + + '@types/node@24.12.0': + dependencies: + undici-types: 7.16.0 + + '@vitest/expect@3.2.4': + dependencies: + '@types/chai': 5.2.3 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + tinyrainbow: 2.0.0 + + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.12.0)(tsx@4.21.0))': + dependencies: + '@vitest/spy': 3.2.4 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@24.12.0)(tsx@4.21.0) + + '@vitest/pretty-format@3.2.4': + dependencies: + tinyrainbow: 2.0.0 + + '@vitest/runner@3.2.4': + dependencies: + '@vitest/utils': 3.2.4 + pathe: 2.0.3 + strip-literal: 3.1.0 + + '@vitest/snapshot@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@3.2.4': + dependencies: + tinyspy: 4.0.4 + + '@vitest/utils@3.2.4': + dependencies: + '@vitest/pretty-format': 3.2.4 + loupe: 3.2.1 + tinyrainbow: 2.0.0 + + assertion-error@2.0.1: {} + + cac@6.7.14: {} + + chai@5.3.3: + dependencies: + assertion-error: 2.0.1 + check-error: 2.1.3 + deep-eql: 5.0.2 + loupe: 3.2.1 + pathval: 2.0.1 + + check-error@2.1.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + deep-eql@5.0.2: {} + + dotenv@17.3.1: {} + + es-module-lexer@1.7.0: {} + + esbuild@0.27.4: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.4 + '@esbuild/android-arm': 0.27.4 + '@esbuild/android-arm64': 0.27.4 + '@esbuild/android-x64': 0.27.4 + '@esbuild/darwin-arm64': 0.27.4 + '@esbuild/darwin-x64': 0.27.4 + '@esbuild/freebsd-arm64': 0.27.4 + '@esbuild/freebsd-x64': 0.27.4 + '@esbuild/linux-arm': 0.27.4 + '@esbuild/linux-arm64': 0.27.4 + '@esbuild/linux-ia32': 0.27.4 + '@esbuild/linux-loong64': 0.27.4 + '@esbuild/linux-mips64el': 0.27.4 + '@esbuild/linux-ppc64': 0.27.4 + '@esbuild/linux-riscv64': 0.27.4 + '@esbuild/linux-s390x': 0.27.4 + '@esbuild/linux-x64': 0.27.4 + '@esbuild/netbsd-arm64': 0.27.4 + '@esbuild/netbsd-x64': 0.27.4 + '@esbuild/openbsd-arm64': 0.27.4 + '@esbuild/openbsd-x64': 0.27.4 + '@esbuild/openharmony-arm64': 0.27.4 + '@esbuild/sunos-x64': 0.27.4 + '@esbuild/win32-arm64': 0.27.4 + '@esbuild/win32-ia32': 0.27.4 + '@esbuild/win32-x64': 0.27.4 + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + expect-type@1.3.0: {} + + fdir@6.5.0(picomatch@4.0.3): + optionalDependencies: + picomatch: 4.0.3 + + fsevents@2.3.3: + optional: true + + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + + js-tokens@9.0.1: {} + + loupe@3.2.1: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + pathe@2.0.3: {} + + pathval@2.0.1: {} + + picocolors@1.1.1: {} + + picomatch@4.0.3: {} + + postcss@8.5.8: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + resolve-pkg-maps@1.0.0: {} + + rollup@4.60.0: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.0 + '@rollup/rollup-android-arm64': 4.60.0 + '@rollup/rollup-darwin-arm64': 4.60.0 + '@rollup/rollup-darwin-x64': 4.60.0 + '@rollup/rollup-freebsd-arm64': 4.60.0 + '@rollup/rollup-freebsd-x64': 4.60.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.0 + '@rollup/rollup-linux-arm-musleabihf': 4.60.0 + '@rollup/rollup-linux-arm64-gnu': 4.60.0 + '@rollup/rollup-linux-arm64-musl': 4.60.0 + '@rollup/rollup-linux-loong64-gnu': 4.60.0 + '@rollup/rollup-linux-loong64-musl': 4.60.0 + '@rollup/rollup-linux-ppc64-gnu': 4.60.0 + '@rollup/rollup-linux-ppc64-musl': 4.60.0 + '@rollup/rollup-linux-riscv64-gnu': 4.60.0 + '@rollup/rollup-linux-riscv64-musl': 4.60.0 + '@rollup/rollup-linux-s390x-gnu': 4.60.0 + '@rollup/rollup-linux-x64-gnu': 4.60.0 + '@rollup/rollup-linux-x64-musl': 4.60.0 + '@rollup/rollup-openbsd-x64': 4.60.0 + '@rollup/rollup-openharmony-arm64': 4.60.0 + '@rollup/rollup-win32-arm64-msvc': 4.60.0 + '@rollup/rollup-win32-ia32-msvc': 4.60.0 + '@rollup/rollup-win32-x64-gnu': 4.60.0 + '@rollup/rollup-win32-x64-msvc': 4.60.0 + fsevents: 2.3.3 + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@3.10.0: {} + + strip-literal@3.1.0: + dependencies: + js-tokens: 9.0.1 + + tinybench@2.9.0: {} + + tinyexec@0.3.2: {} + + tinyglobby@0.2.15: + dependencies: + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + + tinypool@1.1.1: {} + + tinyrainbow@2.0.0: {} + + tinyspy@4.0.4: {} + + tsx@4.21.0: + dependencies: + esbuild: 0.27.4 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + + typescript@5.9.3: {} + + undici-types@7.16.0: {} + + vite-node@3.2.4(@types/node@24.12.0)(tsx@4.21.0): + dependencies: + cac: 6.7.14 + debug: 4.4.3 + es-module-lexer: 1.7.0 + pathe: 2.0.3 + vite: 7.3.1(@types/node@24.12.0)(tsx@4.21.0) + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + vite@7.3.1(@types/node@24.12.0)(tsx@4.21.0): + dependencies: + esbuild: 0.27.4 + fdir: 6.5.0(picomatch@4.0.3) + picomatch: 4.0.3 + postcss: 8.5.8 + rollup: 4.60.0 + tinyglobby: 0.2.15 + optionalDependencies: + '@types/node': 24.12.0 + fsevents: 2.3.3 + tsx: 4.21.0 + + vitest@3.2.4(@types/node@24.12.0)(tsx@4.21.0): + dependencies: + '@types/chai': 5.2.3 + '@vitest/expect': 3.2.4 + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.12.0)(tsx@4.21.0)) + '@vitest/pretty-format': 3.2.4 + '@vitest/runner': 3.2.4 + '@vitest/snapshot': 3.2.4 + '@vitest/spy': 3.2.4 + '@vitest/utils': 3.2.4 + chai: 5.3.3 + debug: 4.4.3 + expect-type: 1.3.0 + magic-string: 0.30.21 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 0.3.2 + tinyglobby: 0.2.15 + tinypool: 1.1.1 + tinyrainbow: 2.0.0 + vite: 7.3.1(@types/node@24.12.0)(tsx@4.21.0) + vite-node: 3.2.4(@types/node@24.12.0)(tsx@4.21.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 24.12.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - yaml + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 diff --git a/lazer/cardano/guards-one/source/pnpm-workspace.yaml b/lazer/cardano/guards-one/source/pnpm-workspace.yaml new file mode 100644 index 00000000..0e5a0737 --- /dev/null +++ b/lazer/cardano/guards-one/source/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - "packages/*" + - "apps/*" diff --git a/lazer/cardano/guards-one/source/tsconfig.json b/lazer/cardano/guards-one/source/tsconfig.json new file mode 100644 index 00000000..297a1e18 --- /dev/null +++ b/lazer/cardano/guards-one/source/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "strict": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "baseUrl": ".", + "paths": { + "@anaconda/core": [ + "packages/core/src/index.ts" + ], + "@anaconda/cardano": [ + "packages/cardano/src/index.ts" + ], + "@anaconda/svm": [ + "packages/svm/src/index.ts" + ], + "@anaconda/evm": [ + "packages/evm/src/index.ts" + ] + } + }, + "include": [ + "packages/**/*.ts", + "apps/**/*.ts" + ] +} From 7ce489aa67d13fe8543dcfd01cf284b27ba4601e Mon Sep 17 00:00:00 2001 From: Nicolas Fernandez Date: Sun, 22 Mar 2026 17:20:24 -0300 Subject: [PATCH 2/9] chore: reduce submission PR to draft placeholder --- lazer/cardano/guards-one/README.md | 80 +- lazer/cardano/guards-one/source/.env.example | 22 - lazer/cardano/guards-one/source/.gitignore | 9 - lazer/cardano/guards-one/source/NEXT_STEPS.md | 69 -- lazer/cardano/guards-one/source/README.md | 158 --- .../source/apps/backend/package.json | 11 - .../source/apps/backend/src/collector.ts | 27 - .../source/apps/backend/src/dashboard.ts | 399 ------ .../source/apps/backend/src/demo-state.ts | 290 ----- .../guards-one/source/apps/backend/src/env.ts | 56 - .../source/apps/backend/src/export-ui-data.ts | 16 - .../source/apps/backend/src/fixtures.ts | 33 - .../source/apps/backend/src/keeper.ts | 135 -- .../source/apps/backend/src/preview-server.ts | 84 -- .../source/apps/backend/src/risk-engine.ts | 20 - .../source/apps/backend/src/simulate.ts | 4 - .../source/apps/backend/src/storage.ts | 131 -- .../apps/backend/tests/dashboard.test.ts | 21 - .../source/apps/backend/tests/e2e.test.ts | 83 -- .../source/apps/backend/tests/storage.test.ts | 56 - .../cardano/guards-one/source/apps/ui/app.js | 617 ---------- .../source/apps/ui/data/demo-state.json | 268 ---- .../guards-one/source/apps/ui/index.html | 203 --- .../guards-one/source/apps/ui/styles.css | 881 ------------- .../guards-one/source/docs/functional-v4.md | 113 -- .../source/docs/landing-frontend-spec.md | 100 -- .../cardano/guards-one/source/docs/roadmap.md | 39 - lazer/cardano/guards-one/source/package.json | 18 - .../source/packages/cardano/package.json | 12 - .../source/packages/cardano/src/index.ts | 2 - .../packages/cardano/src/policy-vault.ts | 379 ------ .../source/packages/cardano/src/types.ts | 65 - .../cardano/tests/policy-vault.test.ts | 421 ------- .../source/packages/core/package.json | 9 - .../source/packages/core/src/capabilities.ts | 50 - .../source/packages/core/src/engine.ts | 488 -------- .../source/packages/core/src/fixtures.ts | 130 -- .../source/packages/core/src/hash.ts | 24 - .../source/packages/core/src/index.ts | 6 - .../source/packages/core/src/math.ts | 135 -- .../source/packages/core/src/types.ts | 183 --- .../source/packages/core/tests/engine.test.ts | 209 ---- .../source/packages/core/tests/math.test.ts | 71 -- .../source/packages/evm/package.json | 14 - .../source/packages/evm/src/fixtures.ts | 23 - .../source/packages/evm/src/index.ts | 151 --- .../source/packages/evm/tests/index.test.ts | 56 - .../source/packages/svm/package.json | 14 - .../source/packages/svm/src/fixtures.ts | 23 - .../source/packages/svm/src/index.ts | 151 --- .../source/packages/svm/tests/index.test.ts | 56 - .../cardano/guards-one/source/pnpm-lock.yaml | 1090 ----------------- .../guards-one/source/pnpm-workspace.yaml | 3 - lazer/cardano/guards-one/source/tsconfig.json | 33 - 54 files changed, 4 insertions(+), 7737 deletions(-) delete mode 100644 lazer/cardano/guards-one/source/.env.example delete mode 100644 lazer/cardano/guards-one/source/.gitignore delete mode 100644 lazer/cardano/guards-one/source/NEXT_STEPS.md delete mode 100644 lazer/cardano/guards-one/source/README.md delete mode 100644 lazer/cardano/guards-one/source/apps/backend/package.json delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/collector.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/dashboard.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/demo-state.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/env.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/export-ui-data.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/fixtures.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/keeper.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/preview-server.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/risk-engine.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/simulate.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/src/storage.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/tests/dashboard.test.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/tests/e2e.test.ts delete mode 100644 lazer/cardano/guards-one/source/apps/backend/tests/storage.test.ts delete mode 100644 lazer/cardano/guards-one/source/apps/ui/app.js delete mode 100644 lazer/cardano/guards-one/source/apps/ui/data/demo-state.json delete mode 100644 lazer/cardano/guards-one/source/apps/ui/index.html delete mode 100644 lazer/cardano/guards-one/source/apps/ui/styles.css delete mode 100644 lazer/cardano/guards-one/source/docs/functional-v4.md delete mode 100644 lazer/cardano/guards-one/source/docs/landing-frontend-spec.md delete mode 100644 lazer/cardano/guards-one/source/docs/roadmap.md delete mode 100644 lazer/cardano/guards-one/source/package.json delete mode 100644 lazer/cardano/guards-one/source/packages/cardano/package.json delete mode 100644 lazer/cardano/guards-one/source/packages/cardano/src/index.ts delete mode 100644 lazer/cardano/guards-one/source/packages/cardano/src/policy-vault.ts delete mode 100644 lazer/cardano/guards-one/source/packages/cardano/src/types.ts delete mode 100644 lazer/cardano/guards-one/source/packages/cardano/tests/policy-vault.test.ts delete mode 100644 lazer/cardano/guards-one/source/packages/core/package.json delete mode 100644 lazer/cardano/guards-one/source/packages/core/src/capabilities.ts delete mode 100644 lazer/cardano/guards-one/source/packages/core/src/engine.ts delete mode 100644 lazer/cardano/guards-one/source/packages/core/src/fixtures.ts delete mode 100644 lazer/cardano/guards-one/source/packages/core/src/hash.ts delete mode 100644 lazer/cardano/guards-one/source/packages/core/src/index.ts delete mode 100644 lazer/cardano/guards-one/source/packages/core/src/math.ts delete mode 100644 lazer/cardano/guards-one/source/packages/core/src/types.ts delete mode 100644 lazer/cardano/guards-one/source/packages/core/tests/engine.test.ts delete mode 100644 lazer/cardano/guards-one/source/packages/core/tests/math.test.ts delete mode 100644 lazer/cardano/guards-one/source/packages/evm/package.json delete mode 100644 lazer/cardano/guards-one/source/packages/evm/src/fixtures.ts delete mode 100644 lazer/cardano/guards-one/source/packages/evm/src/index.ts delete mode 100644 lazer/cardano/guards-one/source/packages/evm/tests/index.test.ts delete mode 100644 lazer/cardano/guards-one/source/packages/svm/package.json delete mode 100644 lazer/cardano/guards-one/source/packages/svm/src/fixtures.ts delete mode 100644 lazer/cardano/guards-one/source/packages/svm/src/index.ts delete mode 100644 lazer/cardano/guards-one/source/packages/svm/tests/index.test.ts delete mode 100644 lazer/cardano/guards-one/source/pnpm-lock.yaml delete mode 100644 lazer/cardano/guards-one/source/pnpm-workspace.yaml delete mode 100644 lazer/cardano/guards-one/source/tsconfig.json diff --git a/lazer/cardano/guards-one/README.md b/lazer/cardano/guards-one/README.md index e127d5b4..82398b10 100644 --- a/lazer/cardano/guards-one/README.md +++ b/lazer/cardano/guards-one/README.md @@ -1,79 +1,7 @@ -# Team SOLx-AR Pythathon Submission +# guards.one -## Details +Draft placeholder only. -Team Name: SOLx-AR -Submission Name: guards.one -Team Members: TODO before final submission -Contact: TODO before final submission +This PR is intentionally empty for now and should not be reviewed as a final hackathon submission. -## Project Description - -guards.one is an oracle-aware treasury control plane for Cardano treasuries with a multichain-native policy engine. It turns treasury risk rules into bounded execution intents and operator-visible actions. - -### What does this project do? - -- monitors treasury liquid value, stable ratio, drawdown versus EMA, oracle freshness, and confidence -- escalates through a risk ladder: `normal -> watch -> partial de-risk -> full stable exit -> auto re-entry` -- simulates the Cardano two-step execution flow: authorize intent first, execute the approved stable swap second -- ships a replayable operator UI inspired by treasury / multisig desks so judges can see the full breach lifecycle quickly - -### How does it integrate with Pyth? - -- uses Pyth price feeds as the primary market data source for `ADA/USD` and the stable reserve reference -- evaluates `price`, `emaPrice`, oracle freshness, and confidence inside the shared risk engine -- models the Cardano preprod witness flow with the provided `PYTH_PREPROD_POLICY_ID` and signed update envelope in the control-plane simulator -- keeps the codebase ready for the real off-chain Pyth Cardano SDK integration in the next implementation step - -### What problem does it solve? - -DAOs and on-chain treasuries do not fail only because an asset moves `X%`; they fail when protected fiat value drops below the floor they intended to defend. guards.one automates that defense with transparent oracle-aware rules instead of ad hoc human reactions. - -## Repository Structure - -```text -lazer/cardano/guards-one/ -├── README.md # Hackathon submission README -└── source/ - ├── apps/ # Backend demo server and operator UI - ├── docs/ # Functional spec, roadmap, frontend spec - ├── packages/ # Core engine, Cardano adapter, SVM/EVM scaffolds - ├── package.json - ├── pnpm-lock.yaml - ├── pnpm-workspace.yaml - ├── tsconfig.json - └── .env.example -``` - -## How to Test - -### Prerequisites - -- Node.js 22+ -- pnpm 10+ - -### Setup & Run Instructions - -```bash -cd lazer/cardano/guards-one/source -pnpm install -pnpm typecheck -pnpm test -pnpm simulate -pnpm export:ui-data -pnpm preview -``` - -Then open `http://localhost:4310` and use the `Replay breach` action in the UI. - -## Deployment Information - -Network: Cardano preprod (simulated control-plane path in this submission) -Contract Address(es): N/A in this snapshot; the current repo still uses the Cardano policy-vault simulator/spec -Demo URL: local preview via `pnpm preview` - -## Notes for Reviewers - -- The current submission includes the working core engine, backend simulation, multichain scaffolding, and replayable operator UI. -- The next technical step after the hackathon is porting the Cardano control-plane simulator to `Aiken` and wiring real Pyth signed updates on preprod. -- Replace the `TODO` team/contact fields before final submission. +A complete Cardano + Pyth submission will be pushed later. diff --git a/lazer/cardano/guards-one/source/.env.example b/lazer/cardano/guards-one/source/.env.example deleted file mode 100644 index dcf1d09b..00000000 --- a/lazer/cardano/guards-one/source/.env.example +++ /dev/null @@ -1,22 +0,0 @@ -APP_PUBLIC_NAME=guards.one -APP_INTERNAL_NAME=anaconda -PORT=4310 - -PYTH_API_KEY=replace_with_pyth_api_key -PYTH_PREPROD_POLICY_ID=d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6 -PYTH_API_BASE_URL=https://api.pyth.network -PYTH_STREAM_CHANNEL=fixed_rate@200ms -PYTH_PRIMARY_FEED_ID=pyth-ada-usd -CARDANO_PYTH_STATE_REFERENCE=replace_with_pyth_state_reference_utxo - -CARDANO_NETWORK=preprod -CARDANO_PROVIDER=blockfrost -CARDANO_PROVIDER_URL=https://cardano-preprod.blockfrost.io/api/v0 -CARDANO_BLOCKFROST_PROJECT_ID=replace_with_blockfrost_project_id -CARDANO_EXECUTION_ROUTE_ID=cardano-minswap-ada-usdm -CARDANO_EXECUTION_HOT_WALLET_ADDRESS=replace_with_execution_hot_wallet -CARDANO_EXECUTION_HOT_WALLET_SKEY_PATH=./secrets/execution-hot.skey -CARDANO_GOVERNANCE_WALLET_ADDRESS=replace_with_governance_wallet -CARDANO_GOVERNANCE_SKEY_PATH=./secrets/governance.skey - -AUDIT_DB_PATH=./data/guards-one.sqlite diff --git a/lazer/cardano/guards-one/source/.gitignore b/lazer/cardano/guards-one/source/.gitignore deleted file mode 100644 index de18100f..00000000 --- a/lazer/cardano/guards-one/source/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -node_modules/ -.DS_Store -.pnpm-store/ -*.log -.env -.env.* -!.env.example -secrets/ -*.sqlite diff --git a/lazer/cardano/guards-one/source/NEXT_STEPS.md b/lazer/cardano/guards-one/source/NEXT_STEPS.md deleted file mode 100644 index 862469db..00000000 --- a/lazer/cardano/guards-one/source/NEXT_STEPS.md +++ /dev/null @@ -1,69 +0,0 @@ -# NEXT STEPS - -This is the canonical tracker for the repository. Every new task, bug, review comment, or deployment prerequisite should be added here and checked off when done. - -## Rules -- Update this file in the same commit that changes the underlying work. -- Keep items concrete and testable. -- If something is blocked, note the blocker inline instead of deleting the task. - -## Current Review Pass - -### PR #1 · Docs -- [x] Align README wording with the documentation-only state of the branch -- [x] Standardize risk-ladder terminology across README, functional spec, and Copilot instructions -- [x] Clarify `ema_price` vs `emaPrice` -- [x] Clarify the "two-transaction flow" wording in the frontend spec - -### PR #2 · Core -- [x] Value stable assets using oracle price when available so depegs affect portfolio math -- [x] Enforce route `chainId` and `live` status in route selection -- [x] Sort snapshot IDs deterministically before hashing/auditing -- [x] Remove root scripts that reference apps before those workspaces exist -- [x] Regenerate the lockfile/workspace state to match the branch contents -- [x] Add a depeg regression test for stable valuation - -### PR #3 · Multichain -- [x] Reuse core shared types instead of duplicating multichain primitives -- [x] Align SVM/EVM stage naming with the core risk ladder -- [x] Validate simulation inputs before computing outputs -- [x] Add invalid-pricing regression tests for SVM and EVM - -### PR #4 · Cardano -- [x] Replace fragile `try/catch` tests with assertions that fail when no error is thrown -- [x] Enforce `result.routeId === intent.routeId` -- [x] Derive tx validity from intent/policy instead of hardcoded constants -- [x] Reject authorization while an intent is already in flight -- [x] Wire `pythStateReference` into the tx envelope -- [x] Reuse `intent.reasonHash` in the tx metadata -- [x] Validate vault, chain, execution time window, and max sell bounds on completion -- [x] Make simulated `averagePrice` consistent with the trade math - -### PR #5 · Apps -- [x] Fix invalid CSS `min()` usage with `calc()` -- [x] Replace broken local docs links in the UI with a safe repo/demo strategy -- [x] Align fallback chip tokens with existing styles -- [x] Escape or avoid unsafe HTML injection in the UI renderer -- [x] Remove the unused backend connector field -- [x] Make audit event ordering deterministic -- [x] Harden preview path resolution against traversal and absolute-path bugs -- [x] Avoid crashing when `Host` is missing in the preview server - -### PR #6 · UI Demo -- [x] Make the watch frame deterministic by using a separate watch-only snapshot before partial de-risk -- [x] Validate ladder tone tokens before interpolating them into CSS class names - -## Product / Stack Pending -- [x] Bootstrap root `.env` and `.env.example` for Pyth/Cardano preprod and deploy settings -- [x] Rebuild the operator UI with a Squads-inspired treasury shell and dark dashboard layout -- [x] Add deterministic demo frames and replay controls for the breach -> de-risk -> exit -> recovery scenario -- [x] Refresh the local preview flow so the UI can be shown as a working product demo -- [ ] Port `PolicyVault` to `Aiken` -- [ ] Integrate real Pyth signed updates and preprod witness flow -- [ ] Integrate a live Cardano swap venue -- [ ] Replace SQLite demo persistence with deployable storage -- [ ] Add CI for tests and typecheck -- [ ] Capture final demo screenshots / recording for the hackathon pitch -- [ ] Prepare the `pyth-examples` submission tree under `/lazer/cardano/my-project/` -- [ ] Adapt the submission README to the hackathon template from `pyth-examples` -- [ ] Open the submission PR from the fork back to `pyth-network/pyth-examples` diff --git a/lazer/cardano/guards-one/source/README.md b/lazer/cardano/guards-one/source/README.md deleted file mode 100644 index b895b84e..00000000 --- a/lazer/cardano/guards-one/source/README.md +++ /dev/null @@ -1,158 +0,0 @@ -# guards.one - -guards.one is an oracle-aware treasury policy enforcement MVP for Cardano with a multichain-native core. This documentation bootstrap describes the target architecture and the implementation plan that lands in the follow-up PR stack. - -Built for the Buenos Aires Pythathon, the project treats Pyth as a core system dependency, not a cosmetic data source: risk state changes, execution authorization, and auditability all depend on oracle evidence. Public branding is `guards.one`; the internal package/tooling namespace remains `anaconda`. - -## Product thesis -guards.one is not a dashboard that happens to read oracle data. It is a treasury control plane that turns risk rules into executable actions. - -The core loop is: -1. ingest Pyth snapshots, -2. evaluate treasury health using drawdown, fiat floors, freshness, and confidence, -3. authorize a bounded execution intent, -4. execute the approved route from the hot bucket, -5. anchor an audit trail with intent, result, and oracle evidence. - -For the MVP, `Cardano` is the live execution target and the risk logic is intentionally portable so future `SVM` and `EVM` connectors can consume the same business model without a refactor. - -## Why this matters -- DAOs and on-chain treasuries often define treasury mandates in percentages, but the real failure mode is breaching an absolute protection floor in fiat or stable terms. -- Oracle-driven automation should not only track spot price; it must also reason about stale data, confidence widening, and recovery hysteresis. -- guards.one converts those constraints into a bounded two-step execution model: authorize based on oracle evidence, then execute only an approved route from a capped hot wallet. - -## Implemented MVP scope -The current repository already includes: -- a shared core policy engine with liquid-value based triggers using `price`, `emaPrice`, `confidence`, freshness, and fiat floors -- a Cardano control-plane simulator and later an `Aiken` port -- backend services for collector, keeper, risk engine, audit logging, and end-to-end simulation -- a Squads-inspired operator UI shell for the treasury demo -- `SVM` and `EVM` scaffolding packages so the core remains multichain-native from day one - -## Why Pyth is required -- The policy engine evaluates both `price` and `emaPrice`. -- Automatic execution is blocked when the oracle snapshot is stale. -- Confidence widening is treated as a first-class risk signal, not just an informational metric. -- Every execution path is designed around carrying oracle evidence into the control plane so the decision can be audited later. - -## Canonical risk ladder -Display names and code identifiers will stay aligned across docs and implementation: -- `Normal` (`normal`): no policy breach, treasury operates normally -- `Watch` (`watch`): early warning, no forced swap yet -- `Partial DeRisk` (`partial_derisk`): sell just enough risky exposure to rebuild the protected stable floor / target ratio -- `Full Stable Exit` (`full_exit`): move the remaining risky bucket into the approved stable asset -- `Frozen` (`frozen`): stale or low-quality oracle conditions block automatic execution -- `Auto Re-entry`: recovery path that becomes available only after hysteresis and cooldown - -The engine measures both percentage risk and absolute protected value: - -`liquid_value = amount * pyth_price * (1 - haircut_bps / 10000)` - -That means the trigger is not just “asset dropped X%”, but also “the treasury or a protected asset fell below the fiat floor we promised to defend”. - -## Target architecture -### Shared core -- `PolicyConfig` -- `OracleSnapshot` -- `RiskStage` -- `ExecutionIntent` -- `ExecutionResult` -- `RouteSpec` -- `TreasuryConnector` - -### Cardano MVP path -- `Tx 1`: authorize execution with oracle-aware validation and an emitted `ExecutionIntent` -- `Tx 2`: execute the approved swap from the execution hot bucket and anchor the result - -### Multichain stance -- `Cardano`: live path in the MVP -- `SVM`: scaffold package with capability matrix and route simulation -- `EVM`: scaffold package with capability matrix and route simulation - -The business logic is shared; execution remains local to each connected treasury chain. - -## Intended demo flow -1. A treasury is configured with a protected asset, approved stable, floor thresholds, guardrails, and route policy. -2. The collector ingests an oracle snapshot and the risk engine evaluates treasury state. -3. If a breach is detected, the Cardano control plane authorizes a bounded `ExecutionIntent`. -4. The keeper executes a swap from the hot bucket using the approved route and anchors the result. -5. Once the market recovers beyond the re-entry threshold and cooldown, the system can auto re-enter. - -The current simulation and UI replay cover this full sequence: -- `partial_derisk` -- `full_exit` -- `auto re-entry` - -The frontend now exposes that run as a deterministic operator demo: -- a workspace card and treasury shell inspired by premium multisig / treasury desks -- account tables for the hot risk bucket and stable reserve -- a replayable timeline for `watch -> partial de-risk -> full stable exit -> recovery` -- a chart built from the simulated backend series -- audit cards rendered from the backend event log - -## Planned repo layout -The follow-up PR stack introduces: -- `packages/core`: shared types, formulas, policy engine, fixtures, tests -- `packages/cardano`: Cardano control-plane simulator and execution connector -- `packages/svm`: scaffold connector and tests -- `packages/evm`: scaffold connector and tests -- `apps/backend`: collector, keeper, audit store, simulation, e2e tests -- `apps/ui`: static landing and dashboard shell -- `docs`: functional spec, roadmap, landing/frontend spec - -## Development workflow -The repository is intentionally organized as a monorepo because the core risk model must stay chain-agnostic while adapters evolve independently. - -Suggested review order: -1. `docs/` for product scope and UX intent -2. `packages/core` for the business model and policy engine -3. `packages/cardano` for the Cardano control-plane simulator -4. `apps/backend` for orchestration, persistence, and e2e flow -5. `apps/ui` for the operator-facing shell - -## Planned scripts -These commands land in the follow-up code PRs: -- `pnpm test`: run the full test suite -- `pnpm typecheck`: run TypeScript type checks -- `pnpm simulate`: execute the end-to-end backend simulation -- `pnpm export:ui-data`: generate the backend demo payload for the static UI -- `pnpm preview`: serve `apps/ui` plus `/api/demo-state` locally - -## Quick start (once the code PRs land) -```bash -pnpm install -pnpm typecheck -pnpm test -pnpm simulate -pnpm export:ui-data -pnpm preview -``` - -Open `http://localhost:4310` after `pnpm preview` to inspect the static frontend using a backend-generated demo payload. - -## Current repo status -- The docs, core engine, backend simulation, UI shell, and multichain scaffolding are merged in `main`. -- The living execution tracker is maintained in `NEXT_STEPS.md`. -- The next engineering steps are the real `Aiken` port, real Pyth witness flow, and a live Cardano swap venue. - -## Constraints and non-goals -- Cardano is the only live execution target in this MVP. -- `SVM` and `EVM` are scaffolded from day one so the business logic does not need a future refactor. -- Hedge/perps are intentionally left in the roadmap, not in the live MVP. -- The Cardano validator path is modeled as a TypeScript simulator/spec in this repo; a compiled `Aiken` validator is the next implementation step. - -## Documentation -- [Functional spec](./docs/functional-v4.md) -- [Roadmap](./docs/roadmap.md) -- [Landing / frontend spec](./docs/landing-frontend-spec.md) -- [Execution tracker](./NEXT_STEPS.md) - -## Environment -- Root `.env` is used for local backend scripts and deploy preparation. -- `.env` is gitignored and already seeded locally with the provided Pyth API key and preprod policy id. -- `.env.example` is safe to commit and documents the variables needed for Pyth, Cardano preprod, wallets, provider, and audit storage. - -## Next implementation steps -1. Port the Cardano policy-vault simulator to an `Aiken` validator and wire the real Pyth witness flow. -2. Replace the simulated execution connector with a real Cardano route builder and settlement path. -3. Capture final screenshots / recording from the local replay demo for the pitch and `pyth-examples` submission. diff --git a/lazer/cardano/guards-one/source/apps/backend/package.json b/lazer/cardano/guards-one/source/apps/backend/package.json deleted file mode 100644 index ce3b7a4a..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/package.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "name": "@anaconda/backend", - "version": "0.0.0", - "private": true, - "type": "module", - "dependencies": { - "@anaconda/cardano": "workspace:*", - "@anaconda/core": "workspace:*", - "dotenv": "^17.2.3" - } -} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/collector.ts b/lazer/cardano/guards-one/source/apps/backend/src/collector.ts deleted file mode 100644 index fa8273fc..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/collector.ts +++ /dev/null @@ -1,27 +0,0 @@ -import type { OracleSnapshot } from "@anaconda/core"; -import { AuditStore } from "./storage.js"; - -export class PythCollector { - private readonly snapshots = new Map(); - - constructor(private readonly auditStore?: AuditStore) {} - - publish(snapshot: OracleSnapshot) { - this.snapshots.set(snapshot.assetId, snapshot); - this.auditStore?.recordSnapshot(snapshot); - this.auditStore?.recordEvent({ - eventId: `snapshot:${snapshot.snapshotId}`, - category: "snapshot", - payload: { - assetId: snapshot.assetId, - snapshotId: snapshot.snapshotId, - observedAtUs: snapshot.observedAtUs, - }, - createdAtUs: snapshot.observedAtUs, - }); - } - - current(): Record { - return Object.fromEntries(this.snapshots.entries()); - } -} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/dashboard.ts b/lazer/cardano/guards-one/source/apps/backend/src/dashboard.ts deleted file mode 100644 index 4cbfadff..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/dashboard.ts +++ /dev/null @@ -1,399 +0,0 @@ -import { - baseCapabilities, - computePositionLiquidValue, - evaluateRiskLadder, - type OracleSnapshot, - type PolicyConfig, - type RouteSpec, - type TreasuryState, -} from "@anaconda/core"; -import type { TickResult } from "./keeper.js"; -import type { AuditEvent } from "./storage.js"; - -export interface DashboardSeriesPoint { - label: string; - stage: TreasuryState["stage"]; - valueFiat: number; -} - -export interface DashboardDemoFrame { - label: string; - title: string; - copy: string; - stage: TreasuryState["stage"]; - valueFiat: number; - stableRatio: number; - reason: string; -} - -export interface DashboardPayload { - generatedAtUs: number; - source: string; - workspace: { - name: string; - label: string; - chain: string; - stage: string; - threshold: string; - members: string; - hotWallet: string; - governanceWallet: string; - totalBalance: string; - primaryAsset: string; - stableAsset: string; - vaultId: string; - }; - topbarChips: Array<{ - label: string; - value: string; - tone: string; - }>; - heroMetrics: Array<{ - label: string; - value: string; - copy: string; - chip?: string; - }>; - dashboardCards: Array<{ - label: string; - value: string; - copy: string; - chip: string; - }>; - chainCards: Array<{ - chain: string; - title: string; - copy: string; - chip: string; - }>; - riskLadder: Array<{ - stage: string; - title: string; - copy: string; - tone?: string; - }>; - executionTimeline: Array<{ - title: string; - copy: string; - status: string; - }>; - auditTrail: Array<{ - title: string; - copy: string; - stamp: string; - }>; - accounts: Array<{ - label: string; - address: string; - balance: string; - fiatValue: string; - weight: string; - role: string; - bucket: string; - }>; - portfolioSeries: Array<{ - label: string; - stage: string; - value: number; - displayValue: string; - }>; - demoFrames: Array<{ - label: string; - title: string; - copy: string; - stage: string; - balance: string; - stableRatio: string; - reason: string; - }>; -} - -function formatUsd(value: number): string { - return new Intl.NumberFormat("en-US", { - style: "currency", - currency: "USD", - maximumFractionDigits: value >= 1000 ? 0 : 2, - }).format(value); -} - -function formatPercent(value: number): string { - return `${(value * 100).toFixed(1)}%`; -} - -function formatAgeUs(nowUs: number, thenUs: number): string { - const seconds = Math.max(0, Math.round((nowUs - thenUs) / 1_000_000)); - return `${seconds}s`; -} - -function formatAmount(value: number): string { - return new Intl.NumberFormat("en-US", { - maximumFractionDigits: value >= 1000 ? 0 : 2, - }).format(value); -} - -function shortAddress(value: string): string { - if (value.length <= 14) { - return value; - } - - return `${value.slice(0, 8)}...${value.slice(-6)}`; -} - -function stageLabel(stage: TreasuryState["stage"]): string { - switch (stage) { - case "normal": - return "Normal"; - case "watch": - return "Watch"; - case "partial_derisk": - return "Partial De-Risk"; - case "full_exit": - return "Full Stable Exit"; - case "frozen": - return "Frozen"; - } -} - -function stageChip(stage: TreasuryState["stage"]): string { - switch (stage) { - case "normal": - return "ok"; - case "watch": - return "warn"; - case "partial_derisk": - return "warn"; - case "full_exit": - return "danger"; - case "frozen": - return "danger"; - } -} - -function formatAuditCopy(event: AuditEvent): string { - switch (event.category) { - case "snapshot": { - const assetId = typeof event.payload.assetId === "string" ? event.payload.assetId : "asset"; - const snapshotId = - typeof event.payload.snapshotId === "string" ? event.payload.snapshotId : "unknown snapshot"; - return `Observed ${assetId.toUpperCase()} from ${snapshotId}.`; - } - case "intent": { - const kind = typeof event.payload.kind === "string" ? event.payload.kind.replaceAll("_", " ") : "intent"; - const stage = typeof event.payload.stage === "string" ? event.payload.stage.replaceAll("_", " ") : "n/a"; - return `Authorized ${kind} at stage ${stage}.`; - } - case "execution": { - const sold = typeof event.payload.soldAmount === "number" ? formatAmount(event.payload.soldAmount) : "n/a"; - const bought = typeof event.payload.boughtAmount === "number" ? formatAmount(event.payload.boughtAmount) : "n/a"; - const stage = typeof event.payload.stage === "string" ? event.payload.stage.replaceAll("_", " ") : "n/a"; - return `Settled ${stage}. Sold ${sold} and bought ${bought}.`; - } - case "rejection": { - const code = typeof event.payload.code === "string" ? event.payload.code : "unknown"; - const keeperId = typeof event.payload.keeperId === "string" ? event.payload.keeperId : "keeper"; - return `Rejected with ${code} for ${keeperId}.`; - } - } -} - -export function buildDashboardPayload(input: { - treasury: TreasuryState; - policy: PolicyConfig; - routes: RouteSpec[]; - snapshots: Record; - operations: TickResult[]; - events: AuditEvent[]; - nowUs: number; - portfolioSeries: DashboardSeriesPoint[]; - demoFrames: DashboardDemoFrame[]; -}): DashboardPayload { - const assessment = evaluateRiskLadder( - input.treasury, - input.policy, - input.snapshots, - input.routes, - input.nowUs, - ); - const latestSnapshot = input.snapshots[input.policy.primaryAssetId]; - const route = input.routes.find((candidate) => - input.policy.approvedRouteIds.includes(candidate.routeId), - ); - const topReason = assessment.reasons[0]?.message ?? "Policy is currently inside the safe band."; - const liveChains = Object.values(baseCapabilities); - const riskPosition = input.treasury.positions.find((position) => position.role === "risk"); - const stablePosition = input.treasury.positions.find((position) => position.role === "stable"); - const accounts = input.treasury.positions.map((position) => { - const snapshot = input.snapshots[position.assetId]; - const liquidValue = computePositionLiquidValue(position, snapshot, input.policy); - const weight = - assessment.metrics.totalLiquidValueFiat === 0 - ? 0 - : liquidValue / assessment.metrics.totalLiquidValueFiat; - const wallet = - position.bucket === "hot" ? input.treasury.executionHotWallet : input.treasury.governanceWallet; - - return { - label: `${position.symbol} ${position.role === "risk" ? "risk bucket" : "stable reserve"}`, - address: shortAddress(wallet), - balance: `${formatAmount(position.amount)} ${position.symbol}`, - fiatValue: formatUsd(liquidValue), - weight: formatPercent(weight), - role: position.role === "risk" ? "Risk asset" : "Stable reserve", - bucket: position.bucket === "hot" ? "Execution hot" : "Governance cold", - }; - }); - - return { - generatedAtUs: input.nowUs, - source: "backend-demo", - workspace: { - name: input.treasury.name, - label: "guards.one live desk", - chain: "Cardano preprod", - stage: stageLabel(input.treasury.stage), - threshold: `${input.treasury.governanceSigners.length} governance signers`, - members: `${input.treasury.riskManagers.length} risk manager · ${input.treasury.keepers.length} keepers`, - hotWallet: shortAddress(input.treasury.executionHotWallet), - governanceWallet: shortAddress(input.treasury.governanceWallet), - totalBalance: formatUsd(assessment.metrics.totalLiquidValueFiat), - primaryAsset: riskPosition?.symbol ?? input.policy.primaryAssetId.toUpperCase(), - stableAsset: stablePosition?.symbol ?? input.policy.stableAssetId.toUpperCase(), - vaultId: input.treasury.vaultId, - }, - topbarChips: [ - { - label: "Network status", - value: "Cardano preprod live", - tone: "live", - }, - { - label: "Approved route", - value: route ? `${route.venue} · ${route.toAssetId.toUpperCase()}` : "No route", - tone: route?.live ? "neutral" : "warn", - }, - { - label: "Execution wallet", - value: shortAddress(input.treasury.executionHotWallet), - tone: "neutral", - }, - ], - heroMetrics: [ - { - label: "Current stage", - value: stageLabel(input.treasury.stage), - copy: "Final policy state after the simulated breach and recovery run.", - chip: stageChip(input.treasury.stage), - }, - { - label: "Treasury liquid value", - value: formatUsd(assessment.metrics.totalLiquidValueFiat), - copy: "Valued from the latest Pyth price and haircut-aware liquid value math.", - }, - { - label: "Stable protection", - value: formatPercent(assessment.metrics.stableRatio), - copy: "Current treasury share parked in the approved stable reserve.", - }, - { - label: "Oracle freshness", - value: latestSnapshot - ? formatAgeUs(input.nowUs, latestSnapshot.feedUpdateTimestampUs) - : "missing", - copy: "Age of the primary feed update relative to the latest policy evaluation.", - }, - ], - dashboardCards: [ - { - label: "Protected floor", - value: formatUsd(input.policy.portfolioFloorFiat), - copy: "Minimum fiat-equivalent value the policy tries to keep defended at all times.", - chip: "ok", - }, - { - label: "Emergency floor", - value: formatUsd(input.policy.emergencyPortfolioFloorFiat), - copy: "Crossing this floor escalates the vault into a full stable exit or freeze path.", - chip: "danger", - }, - { - label: "Primary reason", - value: topReason, - copy: "Top reason emitted by the risk engine for the current snapshot set.", - chip: assessment.reasons.length > 0 ? "warn" : "ok", - }, - { - label: "Execution policy", - value: route - ? `${route.chainId.toUpperCase()} · ${route.venue} · ${route.toAssetId.toUpperCase()}` - : "No approved route", - copy: "Only allowlisted routes can spend from the execution hot wallet.", - chip: route?.live ? "ok" : "warn", - }, - ], - chainCards: liveChains.map((capability) => ({ - chain: capability.chainId.toUpperCase(), - title: capability.live ? "Live execution surface" : "Scaffolded adapter", - copy: capability.notes.join(" "), - chip: capability.live ? "live" : "scaffold", - })), - riskLadder: [ - { - stage: "Normal", - title: "Operate with full permissions", - copy: "Fresh oracle, healthy confidence, and no forced action path active.", - }, - { - stage: "Watch", - title: "Increase monitoring", - copy: "The drawdown or fiat floor approaches the first trigger band.", - tone: "partial", - }, - { - stage: "Partial De-Risk", - title: "Sell only what restores the safe floor", - copy: "The keeper sells a bounded slice of the risky bucket into the approved stable route.", - tone: "partial", - }, - { - stage: "Full Stable Exit", - title: "Move the vault fully defensive", - copy: "A deeper breach exits the risk bucket and keeps the hot wallet on one stable rail.", - tone: "full", - }, - { - stage: "Auto Re-entry", - title: "Re-risk only after hysteresis clears", - copy: "Recovery must clear a separate band and cooldown before exposure comes back.", - tone: "reentry", - }, - ], - executionTimeline: input.operations.map((operation, index) => ({ - title: `Step ${index + 1} · ${stageLabel(operation.stage)}`, - copy: operation.rejected - ? `Execution rejected with ${operation.rejected}.` - : `Intent ${operation.intentId ?? "n/a"} anchored and settled in tx ${operation.txHash ?? "n/a"}.`, - status: operation.rejected ? "rejected" : "executed", - })), - auditTrail: input.events.slice(-6).map((event) => ({ - title: `${event.category.toUpperCase()} · ${event.eventId}`, - copy: formatAuditCopy(event), - stamp: formatAgeUs(input.nowUs, event.createdAtUs), - })), - accounts, - portfolioSeries: input.portfolioSeries.map((point) => ({ - label: point.label, - stage: point.stage, - value: point.valueFiat, - displayValue: formatUsd(point.valueFiat), - })), - demoFrames: input.demoFrames.map((frame) => ({ - label: frame.label, - title: frame.title, - copy: frame.copy, - stage: frame.stage, - balance: formatUsd(frame.valueFiat), - stableRatio: formatPercent(frame.stableRatio), - reason: frame.reason, - })), - }; -} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/demo-state.ts b/lazer/cardano/guards-one/source/apps/backend/src/demo-state.ts deleted file mode 100644 index 811afebd..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/demo-state.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { - evaluateRiskLadder, - type TreasuryState, -} from "@anaconda/core"; -import type { TickResult } from "./keeper.js"; -import { - buildDashboardPayload, - type DashboardDemoFrame, - type DashboardPayload, - type DashboardSeriesPoint, -} from "./dashboard.js"; -import { PythCollector } from "./collector.js"; -import { buildDemoScenario, sampleWitness } from "./fixtures.js"; -import { CardanoKeeperService } from "./keeper.js"; -import { AuditStore } from "./storage.js"; -import { buildSnapshots } from "@anaconda/core"; - -export interface DemoState { - payload: DashboardPayload; - operations: { - partial: TickResult; - fullExit: TickResult; - reentry: TickResult; - }; - counts: ReturnType; - events: ReturnType; -} - -function publishAll( - collector: PythCollector, - snapshots: Record[string]>, -) { - for (const snapshot of Object.values(snapshots)) { - collector.publish(snapshot); - } -} - -function reasonForFrame(result: ReturnType, fallback: string): string { - return result.reasons[0]?.message ?? fallback; -} - -function point(label: string, stage: TreasuryState["stage"], valueFiat: number): DashboardSeriesPoint { - return { - label, - stage, - valueFiat, - }; -} - -function frame(input: { - label: string; - title: string; - copy: string; - stage: TreasuryState["stage"]; - valueFiat: number; - stableRatio: number; - reason: string; -}): DashboardDemoFrame { - return input; -} - -export function createDemoState(): DemoState { - const auditStore = new AuditStore(); - const collector = new PythCollector(auditStore); - const keeper = new CardanoKeeperService(sampleWitness.pythPolicyId, auditStore); - const scenario = buildDemoScenario(); - - publishAll( - collector, - buildSnapshots({ - ada: { - snapshotId: "snapshot-ada-watch", - price: 75, - emaPrice: 80, - feedUpdateTimestampUs: 180_000_000, - observedAtUs: 180_000_000, - }, - usdm: { - snapshotId: "snapshot-usdm-watch", - feedUpdateTimestampUs: 180_000_000, - observedAtUs: 180_000_000, - }, - }), - ); - let treasury = scenario.treasury; - - const baselineAssessment = evaluateRiskLadder( - treasury, - scenario.policy, - collector.current(), - scenario.routes, - 180_000_000, - ); - - publishAll( - collector, - buildSnapshots({ - ada: { - snapshotId: "snapshot-ada-partial", - price: 72, - emaPrice: 80, - feedUpdateTimestampUs: 200_000_000, - observedAtUs: 200_000_000, - }, - usdm: { - snapshotId: "snapshot-usdm-partial", - feedUpdateTimestampUs: 200_000_000, - observedAtUs: 200_000_000, - }, - }), - ); - - const partial = keeper.tick({ - treasury, - policy: scenario.policy, - routes: scenario.routes, - snapshots: collector.current(), - nowUs: 200_000_000, - keeperId: "keeper-1", - witness: sampleWitness, - }); - treasury = partial.treasury; - - const partialAssessment = evaluateRiskLadder( - treasury, - scenario.policy, - collector.current(), - scenario.routes, - 200_000_000, - ); - - publishAll( - collector, - buildSnapshots({ - ada: { - snapshotId: "snapshot-ada-crash", - price: 39, - emaPrice: 80, - feedUpdateTimestampUs: 240_000_000, - observedAtUs: 240_000_000, - }, - usdm: { - snapshotId: "snapshot-usdm-2", - feedUpdateTimestampUs: 240_000_000, - observedAtUs: 240_000_000, - }, - }), - ); - - const fullExit = keeper.tick({ - treasury, - policy: scenario.policy, - routes: scenario.routes, - snapshots: collector.current(), - nowUs: 260_000_000, - keeperId: "keeper-1", - witness: sampleWitness, - }); - treasury = fullExit.treasury; - - const fullExitAssessment = evaluateRiskLadder( - treasury, - scenario.policy, - collector.current(), - scenario.routes, - 260_000_000, - ); - - publishAll( - collector, - buildSnapshots({ - ada: { - snapshotId: "snapshot-ada-recovery", - price: 79, - emaPrice: 80, - confidence: 0.2, - feedUpdateTimestampUs: 340_000_000, - observedAtUs: 340_000_000, - }, - usdm: { - snapshotId: "snapshot-usdm-3", - feedUpdateTimestampUs: 340_000_000, - observedAtUs: 340_000_000, - }, - }), - ); - - const reentry = keeper.tick({ - treasury, - policy: scenario.policy, - routes: scenario.routes, - snapshots: collector.current(), - nowUs: 360_000_000, - keeperId: "keeper-1", - witness: sampleWitness, - }); - - const reentryAssessment = evaluateRiskLadder( - reentry.treasury, - scenario.policy, - collector.current(), - scenario.routes, - 360_000_000, - ); - - const events = auditStore.listEvents(); - const portfolioSeries: DashboardSeriesPoint[] = [ - point("Watch", "watch", baselineAssessment.metrics.totalLiquidValueFiat), - point("Partial", partial.treasury.stage, partialAssessment.metrics.totalLiquidValueFiat), - point("Exit", fullExit.treasury.stage, fullExitAssessment.metrics.totalLiquidValueFiat), - point("Recovery", reentry.treasury.stage, reentryAssessment.metrics.totalLiquidValueFiat), - ]; - const demoFrames: DashboardDemoFrame[] = [ - frame({ - label: "01", - title: "Watchlist breach detected", - copy: - "ADA slips under its EMA while Pyth freshness and confidence remain healthy enough to authorize a policy action.", - stage: "watch", - valueFiat: baselineAssessment.metrics.totalLiquidValueFiat, - stableRatio: baselineAssessment.metrics.stableRatio, - reason: reasonForFrame( - baselineAssessment, - "Drawdown approaches the first policy band.", - ), - }), - frame({ - label: "02", - title: "Partial de-risk executes", - copy: - "The keeper emits an intent, swaps only the bounded amount needed, and restores the defended stable floor.", - stage: partial.treasury.stage, - valueFiat: partialAssessment.metrics.totalLiquidValueFiat, - stableRatio: partialAssessment.metrics.stableRatio, - reason: reasonForFrame( - partialAssessment, - "Partial stable target restored.", - ), - }), - frame({ - label: "03", - title: "Full stable exit after the second leg down", - copy: - "A deeper price break and thinner asset cushion push the vault into a full defensive configuration on the approved stable route.", - stage: fullExit.treasury.stage, - valueFiat: fullExitAssessment.metrics.totalLiquidValueFiat, - stableRatio: fullExitAssessment.metrics.stableRatio, - reason: reasonForFrame( - fullExitAssessment, - "Emergency floor forced the full stable path.", - ), - }), - frame({ - label: "04", - title: "Auto re-entry restores exposure", - copy: - "Recovery clears the hysteresis band, cooldown expires, and the treasury re-enters risk according to the configured target ratio.", - stage: reentry.treasury.stage, - valueFiat: reentryAssessment.metrics.totalLiquidValueFiat, - stableRatio: reentryAssessment.metrics.stableRatio, - reason: reasonForFrame( - reentryAssessment, - "Recovery cleared the re-entry guardrails.", - ), - }), - ]; - - const payload = buildDashboardPayload({ - treasury: reentry.treasury, - policy: scenario.policy, - routes: scenario.routes, - snapshots: collector.current(), - operations: [partial, fullExit, reentry], - events, - nowUs: 360_000_000, - portfolioSeries, - demoFrames, - }); - - return { - payload, - operations: { - partial, - fullExit, - reentry, - }, - counts: auditStore.counts(), - events, - }; -} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/env.ts b/lazer/cardano/guards-one/source/apps/backend/src/env.ts deleted file mode 100644 index dd363cd8..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/env.ts +++ /dev/null @@ -1,56 +0,0 @@ -import path from "node:path"; -import { config as loadDotenv } from "dotenv"; - -loadDotenv({ path: path.resolve(process.cwd(), ".env"), quiet: true }); - -function readString(name: string, fallback = ""): string { - const value = process.env[name]; - return typeof value === "string" && value.length > 0 ? value : fallback; -} - -function readNumber(name: string, fallback: number): number { - const value = process.env[name]; - if (!value) { - return fallback; - } - - const parsed = Number(value); - return Number.isFinite(parsed) ? parsed : fallback; -} - -export const runtimeEnv = { - appPublicName: readString("APP_PUBLIC_NAME", "guards.one"), - appInternalName: readString("APP_INTERNAL_NAME", "anaconda"), - port: readNumber("PORT", 4310), - pythApiKey: readString("PYTH_API_KEY"), - pythPreprodPolicyId: readString( - "PYTH_PREPROD_POLICY_ID", - "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6", - ), - pythApiBaseUrl: readString("PYTH_API_BASE_URL", "https://api.pyth.network"), - pythStreamChannel: readString("PYTH_STREAM_CHANNEL", "fixed_rate@200ms"), - pythPrimaryFeedId: readString("PYTH_PRIMARY_FEED_ID", "pyth-ada-usd"), - cardanoNetwork: readString("CARDANO_NETWORK", "preprod"), - cardanoProvider: readString("CARDANO_PROVIDER", "blockfrost"), - cardanoProviderUrl: readString( - "CARDANO_PROVIDER_URL", - "https://cardano-preprod.blockfrost.io/api/v0", - ), - cardanoBlockfrostProjectId: readString("CARDANO_BLOCKFROST_PROJECT_ID"), - cardanoPythStateReference: readString("CARDANO_PYTH_STATE_REFERENCE", "pyth-state-ref"), - cardanoExecutionRouteId: readString( - "CARDANO_EXECUTION_ROUTE_ID", - "cardano-minswap-ada-usdm", - ), - cardanoExecutionHotWalletAddress: readString("CARDANO_EXECUTION_HOT_WALLET_ADDRESS"), - cardanoExecutionHotWalletSkeyPath: readString( - "CARDANO_EXECUTION_HOT_WALLET_SKEY_PATH", - "./secrets/execution-hot.skey", - ), - cardanoGovernanceWalletAddress: readString("CARDANO_GOVERNANCE_WALLET_ADDRESS"), - cardanoGovernanceSkeyPath: readString( - "CARDANO_GOVERNANCE_SKEY_PATH", - "./secrets/governance.skey", - ), - auditDbPath: readString("AUDIT_DB_PATH", "./data/guards-one.sqlite"), -} as const; diff --git a/lazer/cardano/guards-one/source/apps/backend/src/export-ui-data.ts b/lazer/cardano/guards-one/source/apps/backend/src/export-ui-data.ts deleted file mode 100644 index b66b3792..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/export-ui-data.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { mkdir, writeFile } from "node:fs/promises"; -import path from "node:path"; -import { createDemoState } from "./demo-state.js"; -import "./env.js"; - -const outputDir = path.resolve(process.cwd(), "apps/ui/data"); -const outputFile = path.join(outputDir, "demo-state.json"); - -await mkdir(outputDir, { recursive: true }); -await writeFile( - outputFile, - `${JSON.stringify(createDemoState().payload, null, 2)}\n`, - "utf8", -); - -console.log(`Wrote ${outputFile}`); diff --git a/lazer/cardano/guards-one/source/apps/backend/src/fixtures.ts b/lazer/cardano/guards-one/source/apps/backend/src/fixtures.ts deleted file mode 100644 index 20890f38..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/fixtures.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - buildSnapshots, - samplePolicy, - sampleRoutes, - sampleTreasury, -} from "@anaconda/core"; -import { runtimeEnv } from "./env.js"; - -export const sampleWitness = { - pythPolicyId: runtimeEnv.pythPreprodPolicyId, - pythStateReference: runtimeEnv.cardanoPythStateReference, - signedUpdateHex: "0xfeedbeef", -}; - -export function buildDemoScenario() { - return { - treasury: structuredClone(sampleTreasury), - policy: structuredClone(samplePolicy), - routes: structuredClone(sampleRoutes), - snapshots: buildSnapshots({ - ada: { - snapshotId: "snapshot-ada-live", - feedUpdateTimestampUs: 180_000_000, - observedAtUs: 180_000_000, - }, - usdm: { - snapshotId: "snapshot-usdm-live", - feedUpdateTimestampUs: 180_000_000, - observedAtUs: 180_000_000, - }, - }), - }; -} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/keeper.ts b/lazer/cardano/guards-one/source/apps/backend/src/keeper.ts deleted file mode 100644 index 7f741cf6..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/keeper.ts +++ /dev/null @@ -1,135 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { - type ExecutionResult, - type PolicyConfig, - type RouteSpec, - type TreasuryState, -} from "@anaconda/core"; -import { - CardanoExecutionError, - PolicyVaultSimulator, - createCardanoConnector, - type CardanoPythWitness, -} from "@anaconda/cardano"; -import { AuditStore } from "./storage.js"; - -export interface TickInput { - treasury: TreasuryState; - policy: PolicyConfig; - routes: RouteSpec[]; - snapshots: ReturnType; - nowUs: number; - keeperId: string; - witness: CardanoPythWitness; -} - -export interface TickResult { - treasury: TreasuryState; - intentId?: string; - txHash?: string; - stage: TreasuryState["stage"]; - rejected?: string; -} - -export class CardanoKeeperService { - private readonly simulator: PolicyVaultSimulator; - - constructor( - pythPolicyId: string, - private readonly auditStore: AuditStore, - ) { - this.simulator = new PolicyVaultSimulator(pythPolicyId); - } - - tick(input: TickInput): TickResult { - const connector = createCardanoConnector(input.routes); - - try { - const authorization = this.simulator.authorizeExecution({ - treasury: input.treasury, - policy: input.policy, - snapshots: input.snapshots, - routes: input.routes, - nowUs: input.nowUs, - keeperId: input.keeperId, - witness: input.witness, - }); - - this.auditStore.recordIntent(authorization.intent); - this.auditStore.recordEvent({ - eventId: `intent:${authorization.intent.intentId}`, - category: "intent", - payload: { - stage: authorization.intent.stage, - kind: authorization.intent.kind, - reasonHash: authorization.intent.reasonHash, - }, - createdAtUs: authorization.intent.createdAtUs, - }); - - const simulated = connector.simulateRoute( - authorization.intent, - authorization.assessment.metrics.priceMap, - ); - const result: ExecutionResult = { - intentId: authorization.intent.intentId, - vaultId: authorization.intent.vaultId, - chainId: "cardano", - sourceAssetId: simulated.sourceAssetId, - destinationAssetId: simulated.destinationAssetId, - soldAmount: simulated.soldAmount, - boughtAmount: simulated.boughtAmount, - averagePrice: simulated.averagePrice, - txHash: `tx-${randomUUID()}`, - executedAtUs: input.nowUs + 1_000, - routeId: simulated.routeId, - }; - - const completion = this.simulator.completeExecution({ - treasury: authorization.treasury, - policy: input.policy, - intent: authorization.intent, - result, - }); - - this.auditStore.recordExecution(result); - this.auditStore.recordEvent({ - eventId: `execution:${result.txHash}`, - category: "execution", - payload: { - intentId: result.intentId, - txHash: result.txHash, - soldAmount: result.soldAmount, - boughtAmount: result.boughtAmount, - stage: completion.treasury.stage, - }, - createdAtUs: result.executedAtUs, - }); - - return { - treasury: completion.treasury, - intentId: authorization.intent.intentId, - txHash: result.txHash, - stage: completion.treasury.stage, - }; - } catch (error) { - const message = - error instanceof CardanoExecutionError ? error.code : "UNKNOWN_ERROR"; - this.auditStore.recordEvent({ - eventId: `rejection:${randomUUID()}`, - category: "rejection", - payload: { - code: message, - keeperId: input.keeperId, - }, - createdAtUs: input.nowUs, - }); - - return { - treasury: input.treasury, - stage: input.treasury.stage, - rejected: message, - }; - } - } -} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/preview-server.ts b/lazer/cardano/guards-one/source/apps/backend/src/preview-server.ts deleted file mode 100644 index 9a0a6c30..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/preview-server.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { createReadStream, existsSync } from "node:fs"; -import { stat } from "node:fs/promises"; -import { createServer } from "node:http"; -import path from "node:path"; -import { createDemoState } from "./demo-state.js"; -import { runtimeEnv } from "./env.js"; - -const port = runtimeEnv.port; -const uiRoot = path.resolve(process.cwd(), "apps/ui"); -const docsRoot = path.resolve(process.cwd(), "docs"); -const allowedDocs = new Set([ - "functional-v4.md", - "roadmap.md", - "landing-frontend-spec.md", -]); - -const contentTypes: Record = { - ".html": "text/html; charset=utf-8", - ".css": "text/css; charset=utf-8", - ".js": "text/javascript; charset=utf-8", - ".json": "application/json; charset=utf-8", - ".md": "text/markdown; charset=utf-8", -}; - -function resolveUiPath(requestPath: string): string { - const safePath = requestPath === "/" ? "/index.html" : requestPath; - const relativePath = safePath.replace(/^\/+/, ""); - const resolvedPath = path.resolve(uiRoot, relativePath); - const relativeToRoot = path.relative(uiRoot, resolvedPath); - - if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) { - return path.resolve(uiRoot, "index.html"); - } - - return resolvedPath; -} - -function resolveDocsPath(requestPath: string): string | undefined { - const fileName = requestPath.replace(/^\/docs\/+/, ""); - if (!allowedDocs.has(fileName)) { - return undefined; - } - - return path.resolve(docsRoot, fileName); -} - -const server = createServer(async (request, response) => { - const hostHeader = - typeof request.headers.host === "string" && request.headers.host.length > 0 - ? request.headers.host - : "localhost"; - const url = new URL(request.url ?? "/", `http://${hostHeader}`); - - if (url.pathname === "/api/demo-state") { - response.writeHead(200, { "content-type": "application/json; charset=utf-8" }); - response.end(JSON.stringify(createDemoState().payload, null, 2)); - return; - } - - const filePath = url.pathname.startsWith("/docs/") - ? resolveDocsPath(url.pathname) - : resolveUiPath(url.pathname); - if (!filePath || !existsSync(filePath)) { - response.writeHead(404, { "content-type": "text/plain; charset=utf-8" }); - response.end("Not found"); - return; - } - - const fileStats = await stat(filePath); - if (!fileStats.isFile()) { - response.writeHead(403, { "content-type": "text/plain; charset=utf-8" }); - response.end("Forbidden"); - return; - } - - response.writeHead(200, { - "content-type": contentTypes[path.extname(filePath)] ?? "application/octet-stream", - }); - createReadStream(filePath).pipe(response); -}); - -server.listen(port, () => { - console.log(`Preview server running at http://localhost:${port}`); -}); diff --git a/lazer/cardano/guards-one/source/apps/backend/src/risk-engine.ts b/lazer/cardano/guards-one/source/apps/backend/src/risk-engine.ts deleted file mode 100644 index c014c069..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/risk-engine.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { - evaluateRiskLadder, - type OracleSnapshot, - type PolicyConfig, - type RiskAssessment, - type RouteSpec, - type TreasuryState, -} from "@anaconda/core"; - -export class RiskEngine { - evaluate( - treasury: TreasuryState, - policy: PolicyConfig, - snapshots: Record, - routes: RouteSpec[], - nowUs: number, - ): RiskAssessment { - return evaluateRiskLadder(treasury, policy, snapshots, routes, nowUs); - } -} diff --git a/lazer/cardano/guards-one/source/apps/backend/src/simulate.ts b/lazer/cardano/guards-one/source/apps/backend/src/simulate.ts deleted file mode 100644 index 09aa04dd..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/simulate.ts +++ /dev/null @@ -1,4 +0,0 @@ -import "./env.js"; -import { createDemoState } from "./demo-state.js"; - -console.log(JSON.stringify(createDemoState(), null, 2)); diff --git a/lazer/cardano/guards-one/source/apps/backend/src/storage.ts b/lazer/cardano/guards-one/source/apps/backend/src/storage.ts deleted file mode 100644 index 19ce6c40..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/src/storage.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { DatabaseSync } from "node:sqlite"; -import type { ExecutionIntent, ExecutionResult, OracleSnapshot } from "@anaconda/core"; - -export interface AuditEvent { - eventId: string; - category: "snapshot" | "intent" | "execution" | "rejection"; - payload: Record; - createdAtUs: number; -} - -export class AuditStore { - private readonly db: DatabaseSync; - - constructor(location = ":memory:") { - this.db = new DatabaseSync(location); - this.db.exec(` - CREATE TABLE IF NOT EXISTS snapshots ( - snapshot_id TEXT PRIMARY KEY, - asset_id TEXT NOT NULL, - payload TEXT NOT NULL, - observed_at_us INTEGER NOT NULL - ); - CREATE TABLE IF NOT EXISTS intents ( - intent_id TEXT PRIMARY KEY, - vault_id TEXT NOT NULL, - payload TEXT NOT NULL, - created_at_us INTEGER NOT NULL - ); - CREATE TABLE IF NOT EXISTS executions ( - tx_hash TEXT PRIMARY KEY, - intent_id TEXT NOT NULL, - payload TEXT NOT NULL, - executed_at_us INTEGER NOT NULL - ); - CREATE TABLE IF NOT EXISTS audit_events ( - event_id TEXT PRIMARY KEY, - category TEXT NOT NULL, - payload TEXT NOT NULL, - created_at_us INTEGER NOT NULL - ); - `); - } - - recordSnapshot(snapshot: OracleSnapshot) { - this.db - .prepare(` - INSERT OR REPLACE INTO snapshots (snapshot_id, asset_id, payload, observed_at_us) - VALUES (?, ?, ?, ?) - `) - .run( - snapshot.snapshotId, - snapshot.assetId, - JSON.stringify(snapshot), - snapshot.observedAtUs, - ); - } - - recordIntent(intent: ExecutionIntent) { - this.db - .prepare(` - INSERT OR REPLACE INTO intents (intent_id, vault_id, payload, created_at_us) - VALUES (?, ?, ?, ?) - `) - .run(intent.intentId, intent.vaultId, JSON.stringify(intent), intent.createdAtUs); - } - - recordExecution(result: ExecutionResult) { - this.db - .prepare(` - INSERT OR REPLACE INTO executions (tx_hash, intent_id, payload, executed_at_us) - VALUES (?, ?, ?, ?) - `) - .run(result.txHash, result.intentId, JSON.stringify(result), result.executedAtUs); - } - - recordEvent(event: AuditEvent) { - this.db - .prepare(` - INSERT OR REPLACE INTO audit_events (event_id, category, payload, created_at_us) - VALUES (?, ?, ?, ?) - `) - .run( - event.eventId, - event.category, - JSON.stringify(event.payload), - event.createdAtUs, - ); - } - - listEvents(): AuditEvent[] { - const rows = this.db - .prepare( - `SELECT event_id, category, payload, created_at_us FROM audit_events ORDER BY created_at_us, event_id`, - ) - .all() as Array<{ - event_id: string; - category: AuditEvent["category"]; - payload: string; - created_at_us: number; - }>; - - return rows.map((row) => ({ - eventId: row.event_id, - category: row.category, - payload: JSON.parse(row.payload) as Record, - createdAtUs: row.created_at_us, - })); - } - - counts() { - const [snapshots] = this.db - .prepare(`SELECT COUNT(*) AS count FROM snapshots`) - .all() as Array<{ count: number }>; - const [intents] = this.db - .prepare(`SELECT COUNT(*) AS count FROM intents`) - .all() as Array<{ count: number }>; - const [executions] = this.db - .prepare(`SELECT COUNT(*) AS count FROM executions`) - .all() as Array<{ count: number }>; - const [events] = this.db - .prepare(`SELECT COUNT(*) AS count FROM audit_events`) - .all() as Array<{ count: number }>; - - return { - snapshots: snapshots?.count ?? 0, - intents: intents?.count ?? 0, - executions: executions?.count ?? 0, - events: events?.count ?? 0, - }; - } -} diff --git a/lazer/cardano/guards-one/source/apps/backend/tests/dashboard.test.ts b/lazer/cardano/guards-one/source/apps/backend/tests/dashboard.test.ts deleted file mode 100644 index c1792d8d..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/tests/dashboard.test.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { createDemoState } from "../src/demo-state.js"; - -describe("dashboard payload", () => { - it("builds a UI payload from the simulated backend state", () => { - const state = createDemoState(); - - expect(state.payload.source).toBe("backend-demo"); - expect(state.payload.workspace.name).toContain("Treasury"); - expect(state.payload.topbarChips).toHaveLength(3); - expect(state.payload.heroMetrics).toHaveLength(4); - expect(state.payload.dashboardCards).toHaveLength(4); - expect(state.payload.chainCards).toHaveLength(3); - expect(state.payload.accounts).toHaveLength(2); - expect(state.payload.portfolioSeries).toHaveLength(4); - expect(state.payload.demoFrames).toHaveLength(4); - expect(state.payload.executionTimeline).toHaveLength(3); - expect(state.payload.auditTrail.length).toBeGreaterThan(0); - expect(state.operations.reentry.stage).toBe("normal"); - }); -}); diff --git a/lazer/cardano/guards-one/source/apps/backend/tests/e2e.test.ts b/lazer/cardano/guards-one/source/apps/backend/tests/e2e.test.ts deleted file mode 100644 index d911b220..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/tests/e2e.test.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { buildSnapshots } from "@anaconda/core"; -import { PythCollector } from "../src/collector.js"; -import { createDemoState } from "../src/demo-state.js"; -import { buildDemoScenario, sampleWitness } from "../src/fixtures.js"; -import { CardanoKeeperService } from "../src/keeper.js"; -import { AuditStore } from "../src/storage.js"; - -describe("backend e2e flow", () => { - it("runs partial de-risk, full exit, reentry and records an audit trail", () => { - const state = createDemoState(); - const { partial, fullExit, reentry } = state.operations; - - expect(partial.stage).toBe("partial_derisk"); - expect(partial.txHash).toBeDefined(); - expect(fullExit.stage).toBe("full_exit"); - expect(fullExit.txHash).toBeDefined(); - expect(reentry.stage).toBe("normal"); - expect(reentry.txHash).toBeDefined(); - expect(state.counts.snapshots).toBeGreaterThanOrEqual(6); - expect(state.counts.intents).toBe(3); - expect(state.counts.executions).toBe(3); - expect(state.counts.events).toBeGreaterThanOrEqual(9); - }); - - it("records a rejection when oracle freshness is invalid", () => { - const scenario = buildDemoScenario(); - const auditStore = new AuditStore(); - const collector = new PythCollector(auditStore); - const keeper = new CardanoKeeperService(sampleWitness.pythPolicyId, auditStore); - - for (const snapshot of Object.values( - buildSnapshots({ - ada: { - snapshotId: "snapshot-ada-stale", - feedUpdateTimestampUs: 1_000_000, - observedAtUs: 1_000_000, - }, - }), - )) { - collector.publish(snapshot); - } - - const result = keeper.tick({ - treasury: scenario.treasury, - policy: scenario.policy, - routes: scenario.routes, - snapshots: collector.current(), - nowUs: 500_000_000, - keeperId: "keeper-1", - witness: sampleWitness, - }); - - expect(result.rejected).toBe("STALE_FEED"); - expect(auditStore.counts().events).toBeGreaterThanOrEqual(3); - }); - - it("records a rejection when no approved route exists for execution", () => { - const scenario = buildDemoScenario(); - const auditStore = new AuditStore(); - const collector = new PythCollector(auditStore); - const keeper = new CardanoKeeperService(sampleWitness.pythPolicyId, auditStore); - - for (const snapshot of Object.values(scenario.snapshots)) { - collector.publish(snapshot); - } - - const result = keeper.tick({ - treasury: scenario.treasury, - policy: { - ...scenario.policy, - approvedRouteIds: [], - }, - routes: scenario.routes, - snapshots: collector.current(), - nowUs: 200_000_000, - keeperId: "keeper-1", - witness: sampleWitness, - }); - - expect(result.rejected).toBe("NO_EXECUTABLE_INTENT"); - }); -}); diff --git a/lazer/cardano/guards-one/source/apps/backend/tests/storage.test.ts b/lazer/cardano/guards-one/source/apps/backend/tests/storage.test.ts deleted file mode 100644 index 47c037bb..00000000 --- a/lazer/cardano/guards-one/source/apps/backend/tests/storage.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { buildSnapshots } from "@anaconda/core"; -import { PythCollector } from "../src/collector.js"; -import { AuditStore } from "../src/storage.js"; - -describe("audit store and collector", () => { - it("records snapshots and returns event counts", () => { - const store = new AuditStore(); - const collector = new PythCollector(store); - const snapshots = buildSnapshots(); - - collector.publish(snapshots.ada!); - collector.publish(snapshots.usdm!); - - const current = collector.current(); - expect(current.ada!.snapshotId).toBe("snapshot-ada"); - expect(store.counts().snapshots).toBe(2); - expect(store.counts().events).toBe(2); - expect(store.listEvents()[0]?.category).toBe("snapshot"); - }); - - it("overwrites the latest snapshot per asset in collector state", () => { - const collector = new PythCollector(); - collector.publish(buildSnapshots().ada!); - collector.publish( - buildSnapshots({ - ada: { - snapshotId: "snapshot-ada-2", - observedAtUs: 2_000_000, - feedUpdateTimestampUs: 2_000_000, - }, - }).ada!, - ); - - expect(collector.current().ada!.snapshotId).toBe("snapshot-ada-2"); - }); - - it("orders audit events deterministically when timestamps tie", () => { - const store = new AuditStore(); - - store.recordEvent({ - eventId: "event-b", - category: "snapshot", - payload: { order: 2 }, - createdAtUs: 1_000_000, - }); - store.recordEvent({ - eventId: "event-a", - category: "snapshot", - payload: { order: 1 }, - createdAtUs: 1_000_000, - }); - - expect(store.listEvents().map((event) => event.eventId)).toEqual(["event-a", "event-b"]); - }); -}); diff --git a/lazer/cardano/guards-one/source/apps/ui/app.js b/lazer/cardano/guards-one/source/apps/ui/app.js deleted file mode 100644 index 117f9c75..00000000 --- a/lazer/cardano/guards-one/source/apps/ui/app.js +++ /dev/null @@ -1,617 +0,0 @@ -const fallbackState = { - source: "static-fallback", - workspace: { - name: "Anaconda Treasury Demo", - label: "guards.one live desk", - chain: "Cardano preprod", - stage: "Normal", - threshold: "3 governance signers", - members: "1 risk manager · 2 keepers", - hotWallet: "addr_test...hot_1", - governanceWallet: "addr_test...gov_1", - totalBalance: "$6,646", - primaryAsset: "ADA", - stableAsset: "USDM", - vaultId: "vault-anaconda-demo", - }, - topbarChips: [ - { label: "Network status", value: "Cardano preprod live", tone: "live" }, - { label: "Approved route", value: "Minswap · USDM", tone: "neutral" }, - { label: "Execution wallet", value: "addr_tes...hot_1", tone: "neutral" }, - ], - heroMetrics: [ - { - label: "Current stage", - value: "Normal", - copy: "Final policy state after the simulated breach and recovery run.", - chip: "ok", - }, - { - label: "Treasury liquid value", - value: "$6,646", - copy: "Valued from the latest Pyth price and haircut-aware liquid value math.", - }, - { - label: "Stable protection", - value: "36.0%", - copy: "Current treasury share parked in the approved stable reserve.", - }, - { - label: "Oracle freshness", - value: "20s", - copy: "Age of the primary feed update relative to the latest policy evaluation.", - }, - ], - dashboardCards: [ - { - label: "Protected floor", - value: "$4,500", - copy: "Minimum fiat-equivalent value the policy tries to keep defended at all times.", - chip: "ok", - }, - { - label: "Emergency floor", - value: "$3,400", - copy: "Crossing this floor escalates the vault into a full stable exit or freeze path.", - chip: "danger", - }, - { - label: "Primary reason", - value: "Cooldown prevents an automatic state transition", - copy: "Top reason emitted by the risk engine for the current snapshot set.", - chip: "warn", - }, - { - label: "Execution policy", - value: "CARDANO · Minswap · USDM", - copy: "Only allowlisted routes can spend from the execution hot wallet.", - chip: "ok", - }, - ], - chainCards: [ - { - chain: "CARDANO", - title: "Live execution surface", - copy: "Cardano is the only live execution target in the MVP. Execution uses a two-step authorize-and-swap flow.", - chip: "live", - }, - { - chain: "SVM", - title: "Scaffolded adapter", - copy: "Scaffolding only in the MVP. Designed to share the same policy and role model.", - chip: "scaffold", - }, - { - chain: "EVM", - title: "Scaffolded adapter", - copy: "Scaffolding only in the MVP. Connectors remain simulation-first until phase 2.", - chip: "scaffold", - }, - ], - riskLadder: [ - { - stage: "Normal", - title: "Operate with full permissions", - copy: "Fresh oracle, healthy confidence, and no forced action path active.", - }, - { - stage: "Watch", - title: "Increase monitoring", - copy: "The drawdown or fiat floor approaches the first trigger band.", - tone: "partial", - }, - { - stage: "Partial De-Risk", - title: "Sell only what restores the safe floor", - copy: "The keeper sells a bounded slice of the risky bucket into the approved stable route.", - tone: "partial", - }, - { - stage: "Full Stable Exit", - title: "Move the vault fully defensive", - copy: "A deeper breach exits the risk bucket and keeps the hot wallet on one stable rail.", - tone: "full", - }, - { - stage: "Auto Re-entry", - title: "Re-risk only after hysteresis clears", - copy: "Recovery must clear a separate band and cooldown before exposure comes back.", - tone: "reentry", - }, - ], - executionTimeline: [ - { - title: "Step 1 · Partial De-Risk", - copy: "Intent intent-1 anchored and settled in tx tx-1.", - status: "executed", - }, - { - title: "Step 2 · Full Stable Exit", - copy: "Intent intent-2 anchored and settled in tx tx-2.", - status: "executed", - }, - { - title: "Step 3 · Normal", - copy: "Intent intent-3 anchored and settled in tx tx-3.", - status: "executed", - }, - ], - auditTrail: [ - { - title: "EXECUTION · tx-915d", - copy: "Settled normal. Sold 4,440 and bought 5,553.", - stamp: "0s", - }, - ], - accounts: [ - { - label: "ADA risk bucket", - address: "addr_tes...hot_1", - balance: "5,553 ADA", - fiatValue: "$4,266", - weight: "64.2%", - role: "Risk asset", - bucket: "Execution hot", - }, - { - label: "USDM stable reserve", - address: "addr_tes...hot_1", - balance: "2,393 USDM", - fiatValue: "$2,393", - weight: "35.8%", - role: "Stable reserve", - bucket: "Execution hot", - }, - ], - portfolioSeries: [ - { label: "Watch", stage: "watch", value: 8400, displayValue: "$8,400" }, - { label: "Partial", stage: "partial_derisk", value: 7310, displayValue: "$7,310" }, - { label: "Exit", stage: "full_exit", value: 4580, displayValue: "$4,580" }, - { label: "Recovery", stage: "normal", value: 6646, displayValue: "$6,646" }, - ], - demoFrames: [ - { - label: "01", - title: "Watchlist breach detected", - copy: "ADA slips under its EMA while Pyth freshness and confidence remain healthy enough to authorize a policy action.", - stage: "watch", - balance: "$8,400", - stableRatio: "17.9%", - reason: "ADA liquid value fell below its protected floor", - }, - { - label: "02", - title: "Partial de-risk executes", - copy: "The keeper emits an intent, swaps only the bounded amount needed, and restores the defended stable floor.", - stage: "partial_derisk", - balance: "$7,310", - stableRatio: "44.0%", - reason: "Partial stable target restored.", - }, - { - label: "03", - title: "Full stable exit after the second leg down", - copy: "A deeper price break and thinner asset cushion push the vault into a full defensive configuration on the approved stable route.", - stage: "full_exit", - balance: "$4,580", - stableRatio: "100.0%", - reason: "Emergency floor forced the full stable path.", - }, - { - label: "04", - title: "Auto re-entry restores exposure", - copy: "Recovery clears the hysteresis band, cooldown expires, and the treasury re-enters risk according to the configured target ratio.", - stage: "normal", - balance: "$6,646", - stableRatio: "36.0%", - reason: "Recovery cleared the re-entry guardrails.", - }, - ], -}; - -const workspaceCard = document.querySelector("#workspace-card"); -const topbarChips = document.querySelector("#topbar-chips"); -const heroMetrics = document.querySelector("#hero-metrics"); -const dashboardCards = document.querySelector("#dashboard-cards"); -const chainList = document.querySelector("#chain-list"); -const ladderList = document.querySelector("#risk-ladder-list"); -const executionTimeline = document.querySelector("#execution-timeline"); -const auditTrail = document.querySelector("#audit-trail"); -const accountsBody = document.querySelector("#accounts-body"); -const overviewStage = document.querySelector("#overview-stage"); -const overviewBalance = document.querySelector("#overview-balance"); -const overviewCopy = document.querySelector("#overview-copy"); -const demoTitle = document.querySelector("#demo-title"); -const demoStage = document.querySelector("#demo-stage"); -const demoCopy = document.querySelector("#demo-copy"); -const frameStrip = document.querySelector("#frame-strip"); -const portfolioChart = document.querySelector("#portfolio-chart"); -const frameBalance = document.querySelector("#frame-balance"); -const frameStableRatio = document.querySelector("#frame-stable-ratio"); -const frameReason = document.querySelector("#frame-reason"); -const replayButton = document.querySelector("#replay-button"); - -const allowedTones = new Set(["ok", "warn", "danger", "live", "neutral", "executed", "rejected"]); -const allowedStages = new Set(["normal", "watch", "partial_derisk", "full_exit", "frozen"]); -const allowedLadderTones = new Set(["", "partial", "full", "reentry"]); -const stageFramesToLadderCount = [2, 3, 4, 5]; -let replayTimer = null; -let currentState = fallbackState; -let activeFrameIndex = Math.max(0, fallbackState.demoFrames.length - 1); - -async function loadState() { - const candidates = ["/api/demo-state", "./data/demo-state.json"]; - - for (const url of candidates) { - try { - const response = await fetch(url); - if (response.ok) { - return await response.json(); - } - } catch { - // Try the next source. - } - } - - return fallbackState; -} - -function escapeHTML(value) { - if (value === null || value === undefined) { - return ""; - } - - return String(value) - .replaceAll("&", "&") - .replaceAll("<", "<") - .replaceAll(">", ">") - .replaceAll('"', """) - .replaceAll("'", "'"); -} - -function safeToken(value, allowed, fallback) { - return allowed.has(value) ? value : fallback; -} - -function toneForStage(stage) { - switch (safeToken(stage, allowedStages, "watch")) { - case "normal": - return "ok"; - case "watch": - return "warn"; - case "partial_derisk": - return "warn"; - case "full_exit": - return "danger"; - case "frozen": - return "danger"; - default: - return "neutral"; - } -} - -function humanizeStage(stage) { - return String(stage).replaceAll("_", " "); -} - -function renderWorkspace(workspace) { - return ` -
-
G1
-
-

${escapeHTML(workspace.label)}

- ${escapeHTML(workspace.name)} -
-
-
- Total balance - ${escapeHTML(workspace.totalBalance)} -
-
-
- Stage - ${escapeHTML(workspace.stage)} -
-
- Chain - ${escapeHTML(workspace.chain)} -
-
- Primary - ${escapeHTML(workspace.primaryAsset)} -
-
- Stable - ${escapeHTML(workspace.stableAsset)} -
-
-
-

${escapeHTML(workspace.threshold)}

-

${escapeHTML(workspace.members)}

-

Hot wallet ${escapeHTML(workspace.hotWallet)}

-

Vault ${escapeHTML(workspace.vaultId)}

-
- `; -} - -function renderTopbarChip({ label, value, tone }) { - const safeTone = safeToken(tone, allowedTones, "neutral"); - return ` -
- ${escapeHTML(label)} - ${escapeHTML(value)} -
- `; -} - -function renderMetric({ label, value, copy, chip }) { - const safeChip = safeToken(chip ?? "neutral", allowedTones, "neutral"); - return ` -
-
- ${escapeHTML(label)} - ${chip ? `` : ""} -
- ${escapeHTML(value)} -

${escapeHTML(copy)}

-
- `; -} - -function renderPolicyCard({ label, value, copy, chip }) { - const safeChip = safeToken(chip, allowedTones, "neutral"); - return ` -
-
- ${escapeHTML(label)} - -
- ${escapeHTML(value)} -

${escapeHTML(copy)}

-
- `; -} - -function renderChain({ chain, title, copy, chip }) { - const safeChip = chip === "live" ? "live" : "warn"; - return ` -
-
- ${escapeHTML(chain)} - ${escapeHTML(chip === "live" ? "Live" : "Scaffold")} -
- ${escapeHTML(title)} -

${escapeHTML(copy)}

-
- `; -} - -function renderLadder({ stage, title, copy, tone = "" }, index) { - const safeTone = safeToken(tone ?? "", allowedLadderTones, ""); - const toneClass = safeTone ? ` ladder-${safeTone}` : ""; - return ` -
- ${escapeHTML(stage)} - ${escapeHTML(title)} -

${escapeHTML(copy)}

-
- `; -} - -function renderTimelineItem({ title, copy, status }, index) { - const safeStatus = status === "rejected" ? "rejected" : "executed"; - return ` -
-
- ${escapeHTML(title)} - ${escapeHTML(safeStatus)} -
-

${escapeHTML(copy)}

-
- `; -} - -function renderAuditItem({ title, copy, stamp }) { - return ` -
-
- ${escapeHTML(title)} - ${escapeHTML(stamp)} ago -
-

${escapeHTML(copy)}

-
- `; -} - -function renderAccountRow({ label, address, balance, fiatValue, weight, role, bucket }) { - return ` - - - - - ${escapeHTML(balance)} - ${escapeHTML(fiatValue)} - ${escapeHTML(weight)} - - `; -} - -function renderFramePill(frame, index, activeIndex) { - const safeTone = toneForStage(frame.stage); - const activeClass = index === activeIndex ? " active" : ""; - return ` - - `; -} - -function renderChart(series, activeIndex) { - if (!Array.isArray(series) || series.length === 0) { - return ""; - } - - const width = 640; - const height = 280; - const padX = 36; - const padTop = 18; - const padBottom = 44; - const values = series.map((point) => Number(point.value) || 0); - const min = Math.min(...values); - const max = Math.max(...values); - const span = Math.max(1, max - min); - const innerWidth = width - padX * 2; - const innerHeight = height - padTop - padBottom; - - const x = (index) => padX + (innerWidth * index) / Math.max(1, series.length - 1); - const y = (value) => padTop + (1 - (value - min) / span) * innerHeight; - - const linePath = series - .map((point, index) => `${index === 0 ? "M" : "L"} ${x(index).toFixed(2)} ${y(point.value).toFixed(2)}`) - .join(" "); - const areaPath = `${linePath} L ${x(series.length - 1).toFixed(2)} ${(height - padBottom).toFixed(2)} L ${x(0).toFixed(2)} ${(height - padBottom).toFixed(2)} Z`; - - const grid = Array.from({ length: 3 }, (_, index) => { - const yPos = padTop + (innerHeight * index) / 2; - return ``; - }).join(""); - - const points = series - .map((point, index) => { - const tone = toneForStage(point.stage); - const activeClass = index === activeIndex ? " active" : ""; - return ` - - - ${escapeHTML(point.label)} - - `; - }) - .join(""); - - const activeValue = series[activeIndex]?.displayValue ?? ""; - const activeX = x(activeIndex); - const activeY = y(series[activeIndex]?.value ?? 0); - - return ` - - - - - - - ${grid} - - - ${points} - - - ${escapeHTML(activeValue)} - - `; -} - -function setStagePill(element, label, stage) { - const tone = toneForStage(stage); - element.textContent = label; - element.className = `status-pill tone-${tone}`; -} - -function applyFrame(state, index) { - const frames = Array.isArray(state.demoFrames) ? state.demoFrames : []; - if (frames.length === 0) { - return; - } - - activeFrameIndex = Math.max(0, Math.min(index, frames.length - 1)); - const frame = frames[activeFrameIndex]; - demoTitle.textContent = frame.title; - demoCopy.textContent = frame.copy; - setStagePill(demoStage, humanizeStage(frame.stage), frame.stage); - frameBalance.textContent = frame.balance; - frameStableRatio.textContent = frame.stableRatio; - frameReason.textContent = frame.reason; - frameStrip.innerHTML = frames.map((item, frameIndex) => renderFramePill(item, frameIndex, activeFrameIndex)).join(""); - portfolioChart.innerHTML = renderChart(state.portfolioSeries ?? [], activeFrameIndex); - - const timelineItems = executionTimeline.querySelectorAll(".timeline-item"); - timelineItems.forEach((item, timelineIndex) => { - item.classList.toggle("is-complete", timelineIndex < activeFrameIndex - 1); - item.classList.toggle("is-active", timelineIndex === activeFrameIndex - 1); - }); - - const ladderCards = ladderList.querySelectorAll(".ladder-card"); - const activeCount = stageFramesToLadderCount[Math.min(activeFrameIndex, stageFramesToLadderCount.length - 1)] ?? ladderCards.length; - ladderCards.forEach((item, ladderIndex) => { - item.classList.toggle("is-active", ladderIndex < activeCount); - }); -} - -function startReplay() { - if (replayTimer) { - clearInterval(replayTimer); - replayTimer = null; - } - - applyFrame(currentState, 0); - replayTimer = window.setInterval(() => { - const nextIndex = activeFrameIndex + 1; - if (nextIndex >= currentState.demoFrames.length) { - clearInterval(replayTimer); - replayTimer = null; - return; - } - - applyFrame(currentState, nextIndex); - }, 1400); -} - -function renderState(state) { - currentState = state; - workspaceCard.innerHTML = renderWorkspace(state.workspace); - topbarChips.innerHTML = state.topbarChips.map(renderTopbarChip).join(""); - heroMetrics.innerHTML = state.heroMetrics.map(renderMetric).join(""); - dashboardCards.innerHTML = state.dashboardCards.map(renderPolicyCard).join(""); - chainList.innerHTML = state.chainCards.map(renderChain).join(""); - ladderList.innerHTML = state.riskLadder.map(renderLadder).join(""); - executionTimeline.innerHTML = state.executionTimeline.map(renderTimelineItem).join(""); - auditTrail.innerHTML = (state.auditTrail ?? []).map(renderAuditItem).join(""); - accountsBody.innerHTML = state.accounts.map(renderAccountRow).join(""); - - overviewBalance.textContent = state.workspace.totalBalance; - overviewCopy.textContent = state.dashboardCards[2]?.value ?? "Policy is currently inside the safe band."; - setStagePill(overviewStage, state.workspace.stage, state.demoFrames.at(-1)?.stage ?? "normal"); - applyFrame(state, Math.max(0, state.demoFrames.length - 1)); -} - -frameStrip.addEventListener("click", (event) => { - const target = event.target.closest("[data-frame-index]"); - if (!(target instanceof HTMLElement)) { - return; - } - - const index = Number(target.dataset.frameIndex); - if (!Number.isFinite(index)) { - return; - } - - if (replayTimer) { - clearInterval(replayTimer); - replayTimer = null; - } - - applyFrame(currentState, index); -}); - -replayButton.addEventListener("click", () => { - startReplay(); -}); - -loadState().then((state) => { - renderState(state); -}); diff --git a/lazer/cardano/guards-one/source/apps/ui/data/demo-state.json b/lazer/cardano/guards-one/source/apps/ui/data/demo-state.json deleted file mode 100644 index 726f5b53..00000000 --- a/lazer/cardano/guards-one/source/apps/ui/data/demo-state.json +++ /dev/null @@ -1,268 +0,0 @@ -{ - "generatedAtUs": 360000000, - "source": "backend-demo", - "workspace": { - "name": "Anaconda Treasury Demo", - "label": "guards.one live desk", - "chain": "Cardano preprod", - "stage": "Normal", - "threshold": "3 governance signers", - "members": "1 risk manager · 2 keepers", - "hotWallet": "addr_tes..._hot_1", - "governanceWallet": "addr_tes..._gov_1", - "totalBalance": "$6,646", - "primaryAsset": "ADA", - "stableAsset": "USDM", - "vaultId": "vault-anaconda-demo" - }, - "topbarChips": [ - { - "label": "Network status", - "value": "Cardano preprod live", - "tone": "live" - }, - { - "label": "Approved route", - "value": "Minswap · USDM", - "tone": "neutral" - }, - { - "label": "Execution wallet", - "value": "addr_tes..._hot_1", - "tone": "neutral" - } - ], - "heroMetrics": [ - { - "label": "Current stage", - "value": "Normal", - "copy": "Final policy state after the simulated breach and recovery run.", - "chip": "ok" - }, - { - "label": "Treasury liquid value", - "value": "$6,646", - "copy": "Valued from the latest Pyth price and haircut-aware liquid value math." - }, - { - "label": "Stable protection", - "value": "36.0%", - "copy": "Current treasury share parked in the approved stable reserve." - }, - { - "label": "Oracle freshness", - "value": "20s", - "copy": "Age of the primary feed update relative to the latest policy evaluation." - } - ], - "dashboardCards": [ - { - "label": "Protected floor", - "value": "$4,500", - "copy": "Minimum fiat-equivalent value the policy tries to keep defended at all times.", - "chip": "ok" - }, - { - "label": "Emergency floor", - "value": "$3,400", - "copy": "Crossing this floor escalates the vault into a full stable exit or freeze path.", - "chip": "danger" - }, - { - "label": "Primary reason", - "value": "Cooldown prevents an automatic state transition", - "copy": "Top reason emitted by the risk engine for the current snapshot set.", - "chip": "warn" - }, - { - "label": "Execution policy", - "value": "CARDANO · Minswap · USDM", - "copy": "Only allowlisted routes can spend from the execution hot wallet.", - "chip": "ok" - } - ], - "chainCards": [ - { - "chain": "CARDANO", - "title": "Live execution surface", - "copy": "Cardano is the only live execution target in the MVP. Execution uses a two-step authorize-and-swap flow.", - "chip": "live" - }, - { - "chain": "SVM", - "title": "Scaffolded adapter", - "copy": "Scaffolding only in the MVP. Designed to share the same policy and role model.", - "chip": "scaffold" - }, - { - "chain": "EVM", - "title": "Scaffolded adapter", - "copy": "Scaffolding only in the MVP. Connectors remain simulation-first until phase 2.", - "chip": "scaffold" - } - ], - "riskLadder": [ - { - "stage": "Normal", - "title": "Operate with full permissions", - "copy": "Fresh oracle, healthy confidence, and no forced action path active." - }, - { - "stage": "Watch", - "title": "Increase monitoring", - "copy": "The drawdown or fiat floor approaches the first trigger band.", - "tone": "partial" - }, - { - "stage": "Partial De-Risk", - "title": "Sell only what restores the safe floor", - "copy": "The keeper sells a bounded slice of the risky bucket into the approved stable route.", - "tone": "partial" - }, - { - "stage": "Full Stable Exit", - "title": "Move the vault fully defensive", - "copy": "A deeper breach exits the risk bucket and keeps the hot wallet on one stable rail.", - "tone": "full" - }, - { - "stage": "Auto Re-entry", - "title": "Re-risk only after hysteresis clears", - "copy": "Recovery must clear a separate band and cooldown before exposure comes back.", - "tone": "reentry" - } - ], - "executionTimeline": [ - { - "title": "Step 1 · Partial De-Risk", - "copy": "Intent 815ede7a-6cf0-49fe-939f-262dfa1b6a4c anchored and settled in tx tx-8eb5d881-9efe-4ae5-9cb8-11d7a5d06424.", - "status": "executed" - }, - { - "title": "Step 2 · Full Stable Exit", - "copy": "Intent 27535a6c-6d30-4a06-9e8b-655c620913c8 anchored and settled in tx tx-b713b68d-6d8e-42d3-8ec6-f20e66c0a61f.", - "status": "executed" - }, - { - "title": "Step 3 · Normal", - "copy": "Intent 8205bd55-2e1e-452d-b0ca-5c1889e371d4 anchored and settled in tx tx-41e2236d-9cd7-4f9b-958b-b9de83246025.", - "status": "executed" - } - ], - "auditTrail": [ - { - "title": "INTENT · intent:27535a6c-6d30-4a06-9e8b-655c620913c8", - "copy": "Authorized derisk swap at stage full exit.", - "stamp": "100s" - }, - { - "title": "EXECUTION · execution:tx-b713b68d-6d8e-42d3-8ec6-f20e66c0a61f", - "copy": "Settled full exit. Sold 5,466 and bought 2,106.", - "stamp": "100s" - }, - { - "title": "SNAPSHOT · snapshot:snapshot-ada-recovery", - "copy": "Observed ADA from snapshot-ada-recovery.", - "stamp": "20s" - }, - { - "title": "SNAPSHOT · snapshot:snapshot-usdm-3", - "copy": "Observed USDM from snapshot-usdm-3.", - "stamp": "20s" - }, - { - "title": "INTENT · intent:8205bd55-2e1e-452d-b0ca-5c1889e371d4", - "copy": "Authorized reentry swap at stage normal.", - "stamp": "0s" - }, - { - "title": "EXECUTION · execution:tx-41e2236d-9cd7-4f9b-958b-b9de83246025", - "copy": "Settled normal. Sold 4,440 and bought 5,553.", - "stamp": "0s" - } - ], - "accounts": [ - { - "label": "ADA risk bucket", - "address": "addr_tes..._hot_1", - "balance": "5,553 ADA", - "fiatValue": "$4,255", - "weight": "64.0%", - "role": "Risk asset", - "bucket": "Execution hot" - }, - { - "label": "USDM stable reserve", - "address": "addr_tes..._hot_1", - "balance": "2,391 USDM", - "fiatValue": "$2,391", - "weight": "36.0%", - "role": "Stable reserve", - "bucket": "Execution hot" - } - ], - "portfolioSeries": [ - { - "label": "Watch", - "stage": "watch", - "value": 8775, - "displayValue": "$8,775" - }, - { - "label": "Partial", - "stage": "partial_derisk", - "value": 8542.754226799423, - "displayValue": "$8,543" - }, - { - "label": "Exit", - "stage": "full_exit", - "value": 6831.30402061, - "displayValue": "$6,831" - }, - { - "label": "Recovery", - "stage": "normal", - "value": 6646.407945987443, - "displayValue": "$6,646" - } - ], - "demoFrames": [ - { - "label": "01", - "title": "Watchlist breach detected", - "copy": "ADA slips under its EMA while Pyth freshness and confidence remain healthy enough to authorize a policy action.", - "stage": "watch", - "balance": "$8,775", - "stableRatio": "17.1%", - "reason": "Primary asset drawdown crossed watch threshold" - }, - { - "label": "02", - "title": "Partial de-risk executes", - "copy": "The keeper emits an intent, swaps only the bounded amount needed, and restores the defended stable floor.", - "stage": "partial_derisk", - "balance": "$8,543", - "stableRatio": "55.3%", - "reason": "Primary asset drawdown crossed watch threshold" - }, - { - "label": "03", - "title": "Full stable exit after the second leg down", - "copy": "A deeper price break and thinner asset cushion push the vault into a full defensive configuration on the approved stable route.", - "stage": "full_exit", - "balance": "$6,831", - "stableRatio": "100.0%", - "reason": "Primary asset drawdown crossed watch threshold" - }, - { - "label": "04", - "title": "Auto re-entry restores exposure", - "copy": "Recovery clears the hysteresis band, cooldown expires, and the treasury re-enters risk according to the configured target ratio.", - "stage": "normal", - "balance": "$6,646", - "stableRatio": "36.0%", - "reason": "Cooldown prevents an automatic state transition" - } - ] -} diff --git a/lazer/cardano/guards-one/source/apps/ui/index.html b/lazer/cardano/guards-one/source/apps/ui/index.html deleted file mode 100644 index 51321e13..00000000 --- a/lazer/cardano/guards-one/source/apps/ui/index.html +++ /dev/null @@ -1,203 +0,0 @@ - - - - - - - guards.one - - - - - - -
- - -
-
-
-

Cardano live · Pyth-backed controls

-

Treasury Control Center

-
-
-
- -
-
-
-
-
-

Overview

-

Liquid treasury value

-
- -
- -
-
-

Current total balance

-
-

-
-
- - Open runbook - Read spec -
-
- -
-
- -
-
-
-

Simulation

-

Demo frame

-
- -
- -

-
-
- -
-
-
- Frame balance - -
-
- Stable ratio - -
-
- Trigger - -
-
-
-
- -
-
-
-
-

Accounts

-

Hot and cold treasury buckets

-
-
-
- - - - - - - - - - -
AccountBalanceFiat valueWeight
-
-
- -
-
-
-
-

Policy

-

Guardrails

-
-
-
-
- -
-
-
-

Multichain

-

Execution surfaces

-
-
-
-
-
-
- -
-
-
-
-

Risk ladder

-

Escalation path

-
-
-
-
- -
-
-
-

Execution

-

Runbook timeline

-
-
-
-
-
- -
-
-
-

Audit log

-

Deterministic replay trail

-
-
-
-
-
-
-
- - - - diff --git a/lazer/cardano/guards-one/source/apps/ui/styles.css b/lazer/cardano/guards-one/source/apps/ui/styles.css deleted file mode 100644 index 46c703e9..00000000 --- a/lazer/cardano/guards-one/source/apps/ui/styles.css +++ /dev/null @@ -1,881 +0,0 @@ -:root { - color-scheme: dark; - --bg: #17181d; - --bg-soft: #1c1e25; - --panel: #212329; - --panel-2: #272932; - --panel-3: #1b1d24; - --line: #31343d; - --line-soft: #292c36; - --text: #f4f2ec; - --muted: #9b9ca4; - --muted-strong: #c6c8ce; - --green: #22c55e; - --yellow: #f0bf5f; - --red: #ef6f6c; - --white-chip: #f6f3ee; - --shadow: 0 20px 56px rgba(0, 0, 0, 0.28); -} - -* { - box-sizing: border-box; -} - -html { - scroll-behavior: smooth; -} - -body { - margin: 0; - min-height: 100vh; - background: - radial-gradient(circle at top right, rgba(255, 255, 255, 0.06), transparent 30%), - radial-gradient(circle at top left, rgba(255, 255, 255, 0.03), transparent 20%), - linear-gradient(180deg, #18191f 0%, #15161b 100%); - color: var(--text); - font-family: "Manrope", system-ui, sans-serif; -} - -body::before { - content: ""; - position: fixed; - inset: 0; - pointer-events: none; - background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent 22%); - opacity: 0.4; -} - -a { - color: inherit; - text-decoration: none; -} - -button, -a { - transition: 160ms ease; -} - -button { - font: inherit; -} - -.app-shell { - display: grid; - grid-template-columns: 308px minmax(0, 1fr); - min-height: 100vh; -} - -.sidebar { - position: sticky; - top: 0; - align-self: start; - min-height: 100vh; - padding: 28px 20px; - border-right: 1px solid var(--line-soft); - background: rgba(22, 24, 31, 0.9); - backdrop-filter: blur(18px); - display: grid; - gap: 18px; -} - -.brand { - display: flex; - align-items: center; - gap: 14px; - padding: 4px 10px 16px; -} - -.brand-mark { - width: 40px; - height: 40px; - border-radius: 14px; - background: linear-gradient(180deg, #fcfaf6, #d8d5cd); - display: inline-flex; - align-items: center; - justify-content: center; - box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06); -} - -.brand-mark span { - width: 18px; - height: 18px; - border: 2px solid #1a1b20; - border-radius: 5px; -} - -.brand-copy { - display: grid; - gap: 2px; -} - -.brand-copy strong { - font-size: 1.15rem; - letter-spacing: -0.04em; -} - -.brand-copy small, -.side-label, -.workspace-kicker, -.panel-eyebrow, -.eyebrow, -.audit-stamp, -.frame-label, -.ladder-stage, -.metric-top span, -.card-top span, -.summary-block span, -.account-cell span, -.workspace-balance-row span, -.workspace-meta-grid span, -.workspace-foot p, -th { - font-family: "IBM Plex Mono", monospace; -} - -.brand-copy small, -.side-label, -.workspace-kicker, -.panel-eyebrow, -.eyebrow, -.audit-stamp, -.frame-label, -.ladder-stage, -.metric-top span, -.card-top span, -.summary-block span, -.account-cell span, -.workspace-balance-row span, -.workspace-meta-grid span, -.workspace-foot p { - color: var(--muted); - font-size: 0.72rem; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -.panel, -.panel-soft { - background: linear-gradient(180deg, rgba(35, 37, 45, 0.96), rgba(29, 31, 38, 0.96)); - border: 1px solid var(--line); - border-radius: 24px; - box-shadow: var(--shadow); -} - -.panel-soft { - padding: 18px; - box-shadow: none; -} - -.workspace-card { - display: grid; - gap: 16px; -} - -.workspace-head { - display: flex; - align-items: center; - gap: 14px; -} - -.workspace-avatar { - width: 52px; - height: 52px; - border-radius: 16px; - background: linear-gradient(180deg, #f8f5ef, #d5d2cb); - color: #17181d; - display: inline-flex; - align-items: center; - justify-content: center; - font-weight: 800; - letter-spacing: -0.04em; -} - -.workspace-head strong, -.workspace-balance-row strong, -.workspace-meta-grid strong, -.workspace-balance, -.balance-value, -.metric-card strong, -.policy-card strong, -.chain-card strong, -.ladder-card strong, -.summary-block strong, -.account-cell strong, -.timeline-item strong, -.audit-item strong, -.topbar h1, -.balance-panel h2, -.chart-panel h2, -.panel h2 { - letter-spacing: -0.04em; -} - -.workspace-balance-row { - display: flex; - justify-content: space-between; - align-items: center; - padding: 14px 0 10px; - border-top: 1px solid var(--line-soft); - border-bottom: 1px solid var(--line-soft); -} - -.workspace-balance-row strong { - font-size: 1.8rem; -} - -.workspace-meta-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 12px; -} - -.workspace-meta-grid article { - padding: 12px; - border-radius: 16px; - background: rgba(255, 255, 255, 0.03); - border: 1px solid var(--line-soft); - display: grid; - gap: 6px; -} - -.workspace-foot { - display: grid; - gap: 8px; -} - -.workspace-foot p { - margin: 0; -} - -.sidebar-nav { - display: grid; - gap: 10px; -} - -.nav-item { - min-height: 52px; - padding: 0 16px; - border-radius: 18px; - border: 1px solid transparent; - display: flex; - align-items: center; - color: var(--muted-strong); - background: transparent; - font-weight: 600; -} - -.nav-item:hover, -.nav-item:focus-visible, -.nav-item.active { - background: var(--panel-3); - border-color: var(--line); - color: var(--text); -} - -.sidebar-foot { - align-self: end; -} - -.doc-links { - display: grid; - gap: 10px; -} - -.doc-links a { - padding: 12px 14px; - border-radius: 14px; - border: 1px solid var(--line-soft); - background: rgba(255, 255, 255, 0.03); - color: var(--muted-strong); -} - -.doc-links a:hover, -.doc-links a:focus-visible { - color: var(--text); - border-color: #484b56; -} - -.workspace { - padding: 24px 28px 36px; -} - -.topbar { - display: flex; - justify-content: space-between; - align-items: start; - gap: 18px; - margin-bottom: 22px; -} - -.eyebrow { - margin: 0 0 10px; -} - -.topbar h1 { - margin: 0; - font-size: clamp(2.15rem, 3.8vw, 3.5rem); -} - -.topbar-chips { - display: flex; - flex-wrap: wrap; - justify-content: end; - gap: 12px; -} - -.status-chip, -.status-pill, -.chip { - display: inline-flex; - align-items: center; - gap: 10px; - border-radius: 16px; - border: 1px solid var(--line); -} - -.status-chip { - min-height: 52px; - padding: 0 18px; - background: var(--panel-3); -} - -.status-chip strong { - font-size: 1rem; -} - -.status-chip span { - color: var(--muted); - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.status-pill, -.chip { - min-height: 34px; - padding: 0 12px; - background: rgba(255, 255, 255, 0.04); - font-size: 0.84rem; - font-weight: 700; -} - -.status-pill.subtle { - background: var(--panel-3); -} - -.tone-live::before, -.status-pill::before, -.mini-dot { - content: ""; - width: 8px; - height: 8px; - border-radius: 999px; - background: currentColor; - display: inline-block; -} - -.tone-live { - color: var(--green); -} - -.tone-neutral { - color: var(--muted-strong); -} - -.tone-ok { - color: var(--green); -} - -.tone-warn { - color: var(--yellow); -} - -.tone-danger { - color: var(--red); -} - -.content { - display: grid; - gap: 20px; -} - -.overview-grid, -.content-grid, -.split-grid { - display: grid; - gap: 20px; -} - -.overview-grid { - grid-template-columns: 1.15fr 1fr; -} - -.content-grid { - grid-template-columns: 1.2fr 0.88fr; -} - -.split-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.side-stack { - display: grid; - gap: 20px; -} - -.panel { - padding: 24px; -} - -.panel-header { - display: flex; - justify-content: space-between; - align-items: start; - gap: 18px; - margin-bottom: 22px; -} - -.panel-header.compact { - margin-bottom: 18px; -} - -.panel-header h2, -.balance-panel h2, -.chart-panel h2 { - margin: 0; - font-size: clamp(1.35rem, 2.4vw, 2rem); -} - -.panel-copy, -.balance-copy, -.metric-card p, -.policy-card p, -.chain-card p, -.ladder-card p, -.timeline-item p, -.audit-item p { - margin: 0; - color: var(--muted); - line-height: 1.6; -} - -.balance-row { - display: flex; - justify-content: space-between; - align-items: end; - gap: 20px; - padding-bottom: 22px; - border-bottom: 1px solid var(--line-soft); -} - -.balance-label { - margin: 0 0 8px; - color: var(--muted); -} - -.balance-value { - font-size: clamp(2.8rem, 5vw, 4.6rem); - font-weight: 800; - line-height: 0.95; -} - -.action-row { - display: flex; - flex-wrap: wrap; - justify-content: end; - gap: 12px; -} - -.button { - min-height: 48px; - padding: 0 18px; - border-radius: 16px; - border: 1px solid transparent; - display: inline-flex; - align-items: center; - justify-content: center; - font-weight: 700; - cursor: pointer; -} - -.button-primary { - background: var(--white-chip); - color: #17181d; -} - -.button-secondary { - background: rgba(255, 255, 255, 0.03); - border-color: var(--line); - color: var(--text); -} - -.button:hover, -.button:focus-visible { - transform: translateY(-1px); - border-color: #555966; -} - -.metric-grid, -.policy-grid, -.chain-list, -.ladder-list, -.timeline-list, -.audit-list { - display: grid; - gap: 14px; -} - -.metric-grid { - margin-top: 22px; - grid-template-columns: repeat(4, minmax(0, 1fr)); -} - -.metric-card, -.policy-card, -.chain-card, -.ladder-card, -.timeline-item, -.audit-item { - padding: 18px; - border-radius: 18px; - border: 1px solid var(--line-soft); - background: rgba(255, 255, 255, 0.03); -} - -.metric-card { - min-height: 142px; - display: grid; - align-content: space-between; - gap: 10px; -} - -.metric-card strong { - font-size: 1.55rem; -} - -.metric-top, -.card-top { - display: flex; - justify-content: space-between; - align-items: center; - gap: 12px; -} - -.policy-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.policy-card, -.chain-card, -.ladder-card, -.timeline-item, -.audit-item { - display: grid; - gap: 10px; -} - -.policy-card strong, -.chain-card strong, -.ladder-card strong, -.timeline-item strong, -.audit-item strong { - font-size: 1.06rem; -} - -.frame-strip { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); - gap: 10px; - margin-bottom: 18px; -} - -.frame-pill { - width: 100%; - padding: 14px; - border-radius: 18px; - border: 1px solid var(--line-soft); - background: rgba(255, 255, 255, 0.03); - color: inherit; - text-align: left; - display: grid; - gap: 6px; - cursor: pointer; -} - -.frame-pill strong { - font-size: 0.96rem; - letter-spacing: -0.03em; -} - -.frame-pill small { - font-size: 0.78rem; - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.frame-pill.active, -.frame-pill:hover, -.frame-pill:focus-visible { - border-color: #5a5e69; - background: rgba(255, 255, 255, 0.06); -} - -.chart-shell { - min-height: 280px; - border-radius: 22px; - border: 1px solid var(--line-soft); - background: - radial-gradient(circle at top center, rgba(255, 255, 255, 0.05), transparent 36%), - linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01)); - padding: 8px; - overflow: hidden; -} - -#portfolio-chart { - width: 100%; - height: 280px; - display: block; -} - -.chart-grid-line { - stroke: rgba(255, 255, 255, 0.1); - stroke-width: 1; -} - -.chart-area { - fill: url(#chartAreaGradient); -} - -.chart-line { - fill: none; - stroke: rgba(255, 255, 255, 0.9); - stroke-width: 2.6; - stroke-linecap: round; - stroke-linejoin: round; -} - -.chart-point { - stroke: #17181d; - stroke-width: 3; -} - -.chart-point.tone-ok, -.chart-point.active.tone-ok { - fill: var(--green); -} - -.chart-point.tone-warn, -.chart-point.active.tone-warn { - fill: var(--yellow); -} - -.chart-point.tone-danger, -.chart-point.active.tone-danger { - fill: var(--red); -} - -.chart-label, -.chart-value { - fill: var(--muted-strong); - font-family: "IBM Plex Mono", monospace; -} - -.chart-label { - font-size: 11px; -} - -.chart-bubble rect { - fill: rgba(255, 255, 255, 0.08); - stroke: rgba(255, 255, 255, 0.16); -} - -.chart-value { - font-size: 12px; -} - -.demo-summary { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 14px; - margin-top: 18px; -} - -.summary-block { - padding: 16px; - border-radius: 18px; - border: 1px solid var(--line-soft); - background: rgba(255, 255, 255, 0.03); - display: grid; - gap: 10px; -} - -.summary-block strong { - font-size: 1.18rem; -} - -.summary-reason strong { - font-size: 0.96rem; - line-height: 1.5; -} - -.table-wrap { - overflow-x: auto; -} - -.table-wrap table { - width: 100%; - border-collapse: collapse; -} - -th, -td { - text-align: left; - padding: 14px 10px; - border-bottom: 1px solid var(--line-soft); -} - -th { - color: var(--muted); - font-size: 0.72rem; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.account-cell { - display: grid; - gap: 6px; -} - -.account-cell strong { - font-size: 1rem; -} - -.ladder-list { - grid-template-columns: repeat(5, minmax(0, 1fr)); -} - -.ladder-card { - min-height: 188px; - align-content: start; - opacity: 0.62; -} - -.ladder-card.is-active { - opacity: 1; - border-color: #565a67; - background: rgba(255, 255, 255, 0.05); -} - -.ladder-card.ladder-partial { - border-top: 1px solid rgba(240, 191, 95, 0.45); -} - -.ladder-card.ladder-full { - border-top: 1px solid rgba(239, 111, 108, 0.45); -} - -.ladder-card.ladder-reentry { - border-top: 1px solid rgba(34, 197, 94, 0.45); -} - -.timeline-item, -.audit-item { - position: relative; -} - -.timeline-item.is-active, -.audit-item:hover { - border-color: #565a67; - background: rgba(255, 255, 255, 0.05); -} - -.timeline-item.is-complete { - opacity: 0.72; -} - -.chip { - font-size: 0.74rem; - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.chip-live { - color: var(--green); -} - -.chip-warn { - color: var(--yellow); -} - -@media (max-width: 1400px) { - .overview-grid, - .content-grid, - .split-grid { - grid-template-columns: 1fr; - } - - .ladder-list { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 1080px) { - .app-shell { - grid-template-columns: 1fr; - } - - .sidebar { - position: static; - min-height: auto; - border-right: 0; - border-bottom: 1px solid var(--line-soft); - } - - .metric-grid, - .policy-grid, - .frame-strip, - .demo-summary { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 720px) { - .workspace { - padding: 18px; - } - - .sidebar { - padding: 18px; - } - - .topbar, - .balance-row, - .panel-header { - flex-direction: column; - align-items: stretch; - } - - .topbar-chips, - .action-row { - justify-content: stretch; - } - - .metric-grid, - .policy-grid, - .frame-strip, - .demo-summary, - .ladder-list, - .workspace-meta-grid { - grid-template-columns: 1fr; - } - - .status-chip, - .button, - .nav-item { - width: 100%; - } - - .balance-value { - font-size: 2.8rem; - } -} diff --git a/lazer/cardano/guards-one/source/docs/functional-v4.md b/lazer/cardano/guards-one/source/docs/functional-v4.md deleted file mode 100644 index 68542777..00000000 --- a/lazer/cardano/guards-one/source/docs/functional-v4.md +++ /dev/null @@ -1,113 +0,0 @@ -# guards.one Functional Spec - -## Summary -guards.one is a treasury policy enforcement system for Cardano with a multichain-native core. The MVP keeps Cardano live and uses Pyth as the oracle evidence layer for every automated execution. The product shifts from a simple mode toggle into a risk ladder that can perform partial de-risk, full stable exit, and oracle-aware auto re-entry. - -## Product Goal -The system helps DAOs, communities, and treasury operators protect fiat-denominated value in volatile markets. It does not try to forecast markets or maximize yield. It enforces policy: if exposure becomes too large, too stale, or too uncertain, the treasury transitions into a safer state and optionally executes a swap into a single approved stable asset. - -## Core Principles -- Oracle data is not decorative. Price, confidence, and freshness are inputs to execution. -- The business logic is chain-agnostic. Cardano is only the first live connector. -- Risk is measured both as percentage change and as liquid fiat value. -- The treasury should react to floors, not just drawdown. -- The system must remain explainable to a judge, a governance signer, and an auditor. - -## Risk Model -The policy engine evaluates the treasury against three classes of thresholds. - -### 1. Market drawdown -Compares the latest Pyth price against the EMA price (`ema_price` in the Pyth feed, `emaPrice` in the internal model) and computes a drawdown in bps. This is the first trigger for Watch and Partial DeRisk. - -### 2. Liquid value floor -Computes the liquid value of each protected asset using: - -`liquid_value = amount * pyth_price * (1 - haircut_bps/10000)` - -If a protected asset or the full portfolio falls below a fiat/stable floor, the engine moves to Partial DeRisk or Full Stable Exit. - -### 3. Oracle quality -If the update is stale or the confidence ratio is too wide, the treasury should stop trusting the market view and reduce exposure aggressively. - -## Risk Ladder -The ladder is the main product abstraction. - -1. `Normal` -The treasury is healthy. Governance can act and the keeper watches the oracle. - -2. `Watch` -The treasury is approaching a threshold. No forced swap yet. - -3. `Partial DeRisk` -The treasury sells part of the risky asset into the approved stable asset. - -4. `Full Stable Exit` -The treasury fully exits the risky asset into the approved stable asset. - -5. `Frozen` -If the oracle update is stale or the confidence ratio is too wide, automatic execution is blocked until the market view is trustworthy again. - -6. `Auto Re-entry` -If the market recovers into the re-entry band and the cooldown has elapsed, the treasury can restore exposure. - -## Swap Policy -The MVP uses one approved stable token and one preapproved route on Cardano. The execution flow is intentionally simple. - -### Inputs -- Source asset -- Destination stable asset -- Route id -- Max sell amount -- Minimum buy amount -- Expiry -- Risk stage -- Reason hash - -### Execution Behavior -- `Partial DeRisk` only sells enough to restore the safe floor or reduce exposure to the target band. -- `Full Stable Exit` sells the remaining risky exposure. -- Re-entry is oracle-aware and hysteresis-based. The system must not immediately re-open after a single recovered tick. - -## On-chain Responsibilities -- Validate Pyth evidence in the same transaction. -- Verify freshness, confidence, and policy guards. -- Enforce the current stage of the vault. -- Emit or anchor an `ExecutionIntent`. -- Reject unauthorized signers, stale updates, or invalid routes. - -## Off-chain Responsibilities -- Collect and cache oracle updates. -- Evaluate policies and compute breach events. -- Build the execution transaction. -- Route the swap through the approved execution wallet. -- Persist an auditable log of snapshots, intents, and outcomes. - -## Multichain Model -The core code must not depend on Cardano-specific types. Instead, it exposes a common model that other chains can consume later. - -### Shared abstractions -- `PolicyConfig` -- `OracleSnapshot` -- `RiskStage` -- `ExecutionIntent` -- `ExecutionResult` -- `TreasuryConnector` -- `RouteSpec` - -### Current status -- Cardano: live MVP target. -- SVM: scaffolded from day one, no live deployment in this hackathon scope. -- EVM: scaffolded from day one, no live deployment in this hackathon scope. - -## Non-goals -- Full perps engine -- Cross-chain vault migration -- Multi-DEX optimization -- Yield farming or portfolio maximization - -## Acceptance Criteria -- A treasury can be created with a policy and roles. -- The dashboard can explain the latest oracle state and the current risk stage. -- A breach can trigger partial de-risk and full stable exit. -- A recovery condition can trigger auto re-entry. -- The product can explain how the same logic will later run on SVM and EVM. diff --git a/lazer/cardano/guards-one/source/docs/landing-frontend-spec.md b/lazer/cardano/guards-one/source/docs/landing-frontend-spec.md deleted file mode 100644 index c79755c6..00000000 --- a/lazer/cardano/guards-one/source/docs/landing-frontend-spec.md +++ /dev/null @@ -1,100 +0,0 @@ -# guards.one Landing and Frontend Spec - -## Goal -Create a static but high-signal frontend shell that communicates the product in under one minute: -- What guards.one is -- Why Pyth is required -- How the risk ladder works -- What is live today versus planned later - -## Information Architecture -1. App-shell overview -2. Simulation and replay panel -3. Accounts table -4. Policy guardrails -5. Risk ladder -6. Execution timeline -7. Audit log -8. Documentation links - -## Visual Direction -- Dark, high-contrast treasury shell inspired by `squads.xyz` -- Flat charcoal panels with restrained borders and premium spacing -- White action pills and muted operator chrome -- Minimal accent colors only for state, risk, and execution tone -- Mobile-friendly layout with stacked panels and responsive sidebar collapse - -## Key Messages -- guards.one is a policy enforcement layer, not just an alert board. -- Cardano is the live MVP chain. -- The business logic is multichain-native from the start. -- The system uses price, confidence, freshness, and fiat floors. -- Partial de-risk and full stable exit are the core execution actions. -- Hedge and perps are roadmap items, not MVP scope. - -## Required UI Blocks - -### App Shell -- Branded sidebar with workspace card -- Primary nav for overview, accounts, policy, risk, execution, and audit -- Topbar chips for network, route, and wallet status -- Main overview panel with total liquid treasury value and action buttons - -### Simulation Panel -- Demo replay control -- Per-frame copy for watch, partial de-risk, full exit, and recovery -- SVG chart driven by backend series data -- Summary boxes for frame balance, stable ratio, and trigger reason - -### Accounts Table -- Hot bucket and stable reserve rows -- Address, balance, fiat value, and weight -- Clear distinction between risk and stable roles - -### Policy Cards -- Protected floor -- Emergency floor -- Primary reason -- Current execution route / policy - -### Risk Ladder -- Normal -- Watch -- Partial DeRisk -- Full Stable Exit -- Auto Re-entry - -Each stage must explain the trigger and the resulting action. - -### Execution Timeline -Show the simulated runbook steps emitted by the backend demo state: -- partial de-risk execution -- full stable exit -- re-entry execution - -### Multichain Positioning -Explain that Cardano is the live deployment target while SVM and EVM are first-class future connectors. - -### Audit Log -- Recent snapshots, intents, and execution events -- Human-readable summaries generated from the backend event log - -## Interaction Notes -- The page stays build-free, but it is not static in presentation. -- A small JS file renders backend demo data into the shell and powers replay controls. -- The UI should prefer `/api/demo-state` and fall back to a committed demo JSON export. -- No build step required. -- All content should render without network access except the font import, which is optional. - -## Copy Constraints -- Keep explanations concrete. -- Mention `Pyth` as the oracle evidence layer. -- Avoid generic “AI agent” or “dashboard app” language. -- Make it clear that hedge/perps are planned later, not part of the live MVP. - -## Acceptance Criteria -- The landing reads clearly on desktop and mobile. -- The visual hierarchy is obvious. -- The docs match the product direction in the functional spec. -- A reviewer can understand the product and the roadmap without talking to the team. -- The replay demo clearly shows the breach lifecycle without needing a live blockchain connection. diff --git a/lazer/cardano/guards-one/source/docs/roadmap.md b/lazer/cardano/guards-one/source/docs/roadmap.md deleted file mode 100644 index fd8b1a8e..00000000 --- a/lazer/cardano/guards-one/source/docs/roadmap.md +++ /dev/null @@ -1,39 +0,0 @@ -# guards.one Roadmap - -## Phase 1 -Deliver a credible hackathon MVP on Cardano with a polished dashboard, a documented policy model, and at least one real oracle-driven execution path. - -### Deliverables -- Functional spec v4 -- Static landing/dashboard shell -- Cardano execution path for partial de-risk and full stable exit -- Audit trail for oracle evidence and execution result - -## Phase 2 -Expand the policy model and UX so treasury operators can manage more than one protected asset and more than one threshold family. - -### Deliverables -- Multiple asset floors -- Better re-entry controls -- Historical snapshots and replay -- Visual policy editor - -## Phase 3 -Introduce hedge-aware policy branches without making them part of the Cardano MVP. - -### Deliverables -- Hedge engine types and interfaces -- Long/short policy hooks -- Roadmap for perps and structured protection products - -## Phase 4 -Turn the core into a genuine multichain operating layer. - -### Deliverables -- SVM connector with live execution -- EVM connector with live execution -- Shared policy engine used by all adapters -- Per-chain capability matrix in the UI - -## Strategic Direction -The product should remain conservative in the near term and expand in scope only after the core policy enforcement loop is trustworthy. The next big feature after the hackathon is not a more complex swap route; it is a broader treasury control plane that can execute across chains without rewriting the business logic. diff --git a/lazer/cardano/guards-one/source/package.json b/lazer/cardano/guards-one/source/package.json deleted file mode 100644 index 3f486036..00000000 --- a/lazer/cardano/guards-one/source/package.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "name": "guards-one", - "private": true, - "type": "module", - "scripts": { - "test": "vitest run", - "typecheck": "tsc --noEmit", - "simulate": "tsx apps/backend/src/simulate.ts", - "export:ui-data": "tsx apps/backend/src/export-ui-data.ts", - "preview": "tsx apps/backend/src/preview-server.ts" - }, - "devDependencies": { - "@types/node": "^24.0.0", - "tsx": "^4.20.3", - "typescript": "^5.8.3", - "vitest": "^3.2.4" - } -} diff --git a/lazer/cardano/guards-one/source/packages/cardano/package.json b/lazer/cardano/guards-one/source/packages/cardano/package.json deleted file mode 100644 index 8608453d..00000000 --- a/lazer/cardano/guards-one/source/packages/cardano/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "@anaconda/cardano", - "version": "0.0.0", - "private": true, - "type": "module", - "exports": { - ".": "./src/index.ts" - }, - "dependencies": { - "@anaconda/core": "workspace:*" - } -} diff --git a/lazer/cardano/guards-one/source/packages/cardano/src/index.ts b/lazer/cardano/guards-one/source/packages/cardano/src/index.ts deleted file mode 100644 index bd3efca6..00000000 --- a/lazer/cardano/guards-one/source/packages/cardano/src/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./policy-vault.js"; -export * from "./types.js"; diff --git a/lazer/cardano/guards-one/source/packages/cardano/src/policy-vault.ts b/lazer/cardano/guards-one/source/packages/cardano/src/policy-vault.ts deleted file mode 100644 index 8e8e97f6..00000000 --- a/lazer/cardano/guards-one/source/packages/cardano/src/policy-vault.ts +++ /dev/null @@ -1,379 +0,0 @@ -import { - baseCapabilities, - evaluateRiskLadder, - type ExecutionIntent, - type ExecutionResult, - type PolicyConfig, - type RouteSpec, - type TreasuryState, -} from "@anaconda/core"; -import type { - AuthorizeExecutionParams, - AuthorizeExecutionResult, - CardanoPolicyVaultArtifact, - CardanoTxEnvelope, - CompleteExecutionParams, - CompleteExecutionResult, -} from "./types.js"; - -export class CardanoExecutionError extends Error { - code: string; - - constructor(code: string, message: string) { - super(message); - this.code = code; - } -} - -export const policyVaultArtifact: CardanoPolicyVaultArtifact = { - name: "PolicyVault", - datumFields: [ - "vault_id", - "stage", - "last_transition_us", - "governance_signers", - "keepers", - "approved_route_ids", - "stable_asset_id", - "primary_asset_id", - "current_intent_id", - ], - redeemers: [ - "AuthorizeExecution", - "CompleteExecution", - "UpdatePolicy", - "Resume", - "EmergencyWithdraw", - ], - invariants: [ - "Pyth witness must be present in the same transaction for authorize execution", - "Only approved keepers can authorize automatic execution", - "Only approved routes and assets can be used by execution results", - "Cooldown and guardrails are enforced before stage transitions", - ], -}; - -function buildTxEnvelope( - kind: CardanoTxEnvelope["kind"], - reasonHash: string, - startUs: number, - validityEndUs: number, - referenceInputs: string[], - withdrawals: CardanoTxEnvelope["withdrawals"], - spendingTargets: string[], - metadata: Record = {}, -): CardanoTxEnvelope { - return { - kind, - validityStartUs: startUs, - validityEndUs, - referenceInputs, - withdrawals, - spendingTargets, - metadata: { - reason_hash: reasonHash, - tx_kind: kind, - ...metadata, - }, - }; -} - -function deriveValidityEndUs( - startUs: number, - expiryUs: number, - maxValidityDurationUs: number, -): number { - return Math.max(startUs, Math.min(expiryUs, startUs + maxValidityDurationUs)); -} - -function updatePositionAmount( - treasury: TreasuryState, - assetId: string, - delta: number, -): TreasuryState["positions"] { - let touched = false; - const positions = treasury.positions.map((position) => { - if (position.assetId !== assetId) { - return position; - } - - touched = true; - const nextAmount = Number((position.amount + delta).toFixed(8)); - if (nextAmount < -1e-9) { - throw new CardanoExecutionError( - "NEGATIVE_BALANCE", - `Asset ${assetId} would go negative after execution`, - ); - } - - return { - ...position, - amount: Math.max(0, nextAmount), - }; - }); - - if (!touched) { - throw new CardanoExecutionError("ASSET_NOT_FOUND", `Asset ${assetId} is not in treasury`); - } - - return positions; -} - -export class PolicyVaultSimulator { - readonly pythPolicyId: string; - readonly capabilities = baseCapabilities.cardano; - - constructor(pythPolicyId: string) { - this.pythPolicyId = pythPolicyId; - } - - authorizeExecution(params: AuthorizeExecutionParams): AuthorizeExecutionResult { - const { treasury, policy, snapshots, routes, nowUs, keeperId, witness } = params; - - if (treasury.currentIntentId) { - throw new CardanoExecutionError( - "INTENT_ALREADY_IN_FLIGHT", - "Treasury already has an in-flight execution intent", - ); - } - - if (!treasury.keepers.includes(keeperId)) { - throw new CardanoExecutionError( - "KEEPER_NOT_AUTHORIZED", - `Keeper ${keeperId} is not allowed to authorize automatic execution`, - ); - } - - if (witness.pythPolicyId !== this.pythPolicyId) { - throw new CardanoExecutionError( - "PYTH_POLICY_MISMATCH", - "The provided Pyth witness does not match the configured Cardano deployment", - ); - } - - const assessment = evaluateRiskLadder(treasury, policy, snapshots, routes, nowUs); - if (assessment.shouldFreeze) { - const reason = assessment.reasons[0]; - throw new CardanoExecutionError( - reason?.code?.toUpperCase() ?? "FREEZE_REQUIRED", - reason?.message ?? "Oracle guardrails require a freeze instead of execution", - ); - } - - if (assessment.cooldownRemainingUs > 0) { - throw new CardanoExecutionError( - "COOLDOWN_ACTIVE", - "Cooldown is still active for this vault", - ); - } - - const intent = assessment.intent; - if (!intent) { - throw new CardanoExecutionError( - "NO_EXECUTABLE_INTENT", - "Current policy evaluation did not produce an automatic execution intent", - ); - } - - if (!policy.approvedRouteIds.includes(intent.routeId)) { - throw new CardanoExecutionError( - "ROUTE_NOT_APPROVED", - `Route ${intent.routeId} is not approved by governance`, - ); - } - - if ( - intent.destinationAssetId !== policy.stableAssetId && - intent.destinationAssetId !== policy.primaryAssetId - ) { - throw new CardanoExecutionError( - "ASSET_NOT_APPROVED", - `Destination asset ${intent.destinationAssetId} is not approved`, - ); - } - - const nextTreasury: TreasuryState = { - ...treasury, - stage: assessment.nextStage, - lastTransitionUs: nowUs, - currentIntentId: intent.intentId, - }; - - const tx = buildTxEnvelope( - "authorize_execution", - intent.reasonHash, - nowUs, - deriveValidityEndUs(nowUs, intent.expiryUs, policy.maxStaleUs), - [witness.pythStateReference], - [ - { - policyId: witness.pythPolicyId, - amount: 0, - redeemer: witness.signedUpdateHex, - }, - ], - ["policy-vault-utxo"], - ); - - return { - treasury: nextTreasury, - assessment, - intent, - tx, - }; - } - - completeExecution(params: CompleteExecutionParams): CompleteExecutionResult { - const { treasury, policy, intent, result } = params; - - if (result.vaultId !== treasury.vaultId || result.vaultId !== intent.vaultId) { - throw new CardanoExecutionError( - "RESULT_VAULT_MISMATCH", - "Execution result does not belong to this vault", - ); - } - - if (result.chainId !== treasury.chainId || result.chainId !== intent.chainId) { - throw new CardanoExecutionError( - "RESULT_CHAIN_MISMATCH", - "Execution result does not belong to this chain", - ); - } - - if (treasury.currentIntentId !== intent.intentId) { - throw new CardanoExecutionError( - "INTENT_MISMATCH", - "Treasury does not hold the provided execution intent", - ); - } - - if (result.intentId !== intent.intentId) { - throw new CardanoExecutionError( - "RESULT_INTENT_MISMATCH", - "Execution result does not match the current intent", - ); - } - - if (!policy.approvedRouteIds.includes(result.routeId)) { - throw new CardanoExecutionError( - "ROUTE_NOT_APPROVED", - `Route ${result.routeId} is not approved by governance`, - ); - } - - if (result.routeId !== intent.routeId) { - throw new CardanoExecutionError( - "ROUTE_MISMATCH", - "Execution result route does not match the authorized intent", - ); - } - - if ( - result.sourceAssetId !== intent.sourceAssetId || - result.destinationAssetId !== intent.destinationAssetId - ) { - throw new CardanoExecutionError( - "ASSET_NOT_APPROVED", - "Execution result asset pair differs from the approved intent", - ); - } - - if (result.executedAtUs < intent.createdAtUs || result.executedAtUs > intent.expiryUs) { - throw new CardanoExecutionError( - "RESULT_OUT_OF_WINDOW", - "Execution result falls outside the authorized intent window", - ); - } - - if (result.soldAmount - intent.maxSellAmount > 1e-8) { - throw new CardanoExecutionError( - "MAX_SELL_EXCEEDED", - "Execution result sold more than the authorized intent", - ); - } - - if (result.boughtAmount + 1e-8 < intent.minBuyAmount) { - throw new CardanoExecutionError( - "MIN_BUY_NOT_MET", - "Execution result under-delivered against the minimum buy amount", - ); - } - - let nextPositions = updatePositionAmount(treasury, result.sourceAssetId, -result.soldAmount); - nextPositions = updatePositionAmount( - { ...treasury, positions: nextPositions }, - result.destinationAssetId, - result.boughtAmount, - ); - - const nextTreasury: TreasuryState = { - ...treasury, - positions: nextPositions, - stage: intent.kind === "reentry_swap" ? "normal" : intent.stage, - currentIntentId: undefined, - lastTransitionUs: result.executedAtUs, - }; - - return { - treasury: nextTreasury, - tx: buildTxEnvelope( - "complete_execution", - intent.reasonHash, - result.executedAtUs, - deriveValidityEndUs(result.executedAtUs, intent.expiryUs, policy.maxStaleUs), - [], - [], - ["policy-vault-utxo", "execution-hot-wallet"], - { - intent_id: intent.intentId, - tx_hash: result.txHash, - }, - ), - }; - } -} - -export function createCardanoConnector(routes: RouteSpec[]) { - return { - chainId: "cardano" as const, - capabilities: baseCapabilities.cardano, - describeExecutionConstraints() { - return [ - "Cardano executes live in the MVP via a two-step authorize and swap flow.", - "Only approved routes can consume the execution hot bucket.", - "Oracle verification must be present in the same authorization transaction.", - ]; - }, - simulateRoute(intent: ExecutionIntent, priceMap: Record) { - const route = routes.find((candidate) => candidate.routeId === intent.routeId); - if (!route) { - throw new CardanoExecutionError( - "ROUTE_NOT_FOUND", - `Missing route definition for ${intent.routeId}`, - ); - } - - const sourcePrice = priceMap[intent.sourceAssetId]; - const destinationPrice = priceMap[intent.destinationAssetId]; - if (!sourcePrice || !destinationPrice) { - throw new CardanoExecutionError( - "PRICE_MAP_INCOMPLETE", - "Price map is missing one or more assets for route simulation", - ); - } - - const soldAmount = intent.maxSellAmount; - const grossDestination = soldAmount * sourcePrice / destinationPrice; - const boughtAmount = grossDestination * (1 - route.maxSlippageBps / 10_000); - - return { - sourceAssetId: intent.sourceAssetId, - destinationAssetId: intent.destinationAssetId, - soldAmount: Number(soldAmount.toFixed(8)), - boughtAmount: Number(boughtAmount.toFixed(8)), - averagePrice: Number((boughtAmount / soldAmount).toFixed(8)), - routeId: route.routeId, - }; - }, - }; -} diff --git a/lazer/cardano/guards-one/source/packages/cardano/src/types.ts b/lazer/cardano/guards-one/source/packages/cardano/src/types.ts deleted file mode 100644 index d7a81943..00000000 --- a/lazer/cardano/guards-one/source/packages/cardano/src/types.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { - ExecutionIntent, - ExecutionResult, - OracleSnapshot, - PolicyConfig, - RiskAssessment, - RouteSpec, - TreasuryState, -} from "@anaconda/core"; - -export interface CardanoPythWitness { - pythPolicyId: string; - pythStateReference: string; - signedUpdateHex: string; -} - -export interface CardanoTxEnvelope { - kind: "authorize_execution" | "complete_execution"; - validityStartUs: number; - validityEndUs: number; - referenceInputs: string[]; - withdrawals: Array<{ - policyId: string; - amount: number; - redeemer: string; - }>; - spendingTargets: string[]; - metadata: Record; -} - -export interface AuthorizeExecutionParams { - treasury: TreasuryState; - policy: PolicyConfig; - snapshots: Record; - routes: RouteSpec[]; - nowUs: number; - keeperId: string; - witness: CardanoPythWitness; -} - -export interface AuthorizeExecutionResult { - treasury: TreasuryState; - assessment: RiskAssessment; - intent: ExecutionIntent; - tx: CardanoTxEnvelope; -} - -export interface CompleteExecutionParams { - treasury: TreasuryState; - policy: PolicyConfig; - intent: ExecutionIntent; - result: ExecutionResult; -} - -export interface CompleteExecutionResult { - treasury: TreasuryState; - tx: CardanoTxEnvelope; -} - -export interface CardanoPolicyVaultArtifact { - name: string; - datumFields: string[]; - redeemers: string[]; - invariants: string[]; -} diff --git a/lazer/cardano/guards-one/source/packages/cardano/tests/policy-vault.test.ts b/lazer/cardano/guards-one/source/packages/cardano/tests/policy-vault.test.ts deleted file mode 100644 index c1d1b187..00000000 --- a/lazer/cardano/guards-one/source/packages/cardano/tests/policy-vault.test.ts +++ /dev/null @@ -1,421 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - buildSnapshots, - samplePolicy, - sampleRoutes, - sampleTreasury, -} from "@anaconda/core"; -import { - CardanoExecutionError, - PolicyVaultSimulator, - createCardanoConnector, -} from "../src/index.js"; - -const simulator = new PolicyVaultSimulator( - "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6", -); - -const witness = { - pythPolicyId: "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6", - pythStateReference: "pyth-state-ref", - signedUpdateHex: "0xfeedbeef", -}; - -function expectCardanoError(action: () => void, expectedCode: string) { - try { - action(); - throw new Error(`Expected CardanoExecutionError(${expectedCode})`); - } catch (error) { - expect(error).toBeInstanceOf(CardanoExecutionError); - expect((error as CardanoExecutionError).code).toBe(expectedCode); - } -} - -describe("cardano policy vault simulator", () => { - it("authorizes a valid partial de-risk execution", () => { - const result = simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }); - - expect(result.intent.stage).toBe("partial_derisk"); - expect(result.treasury.stage).toBe("partial_derisk"); - expect(result.tx.withdrawals).toHaveLength(1); - expect(result.tx.referenceInputs).toEqual([witness.pythStateReference]); - expect(result.tx.metadata.reason_hash).toBe(result.intent.reasonHash); - expect(result.tx.validityEndUs).toBe(result.intent.expiryUs); - }); - - it("rejects stale snapshots", () => { - expectCardanoError( - () => - simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots({ - ada: { feedUpdateTimestampUs: 1_000_000, observedAtUs: 1_000_000 }, - }), - routes: sampleRoutes, - nowUs: 500_000_000, - keeperId: "keeper-1", - witness, - }), - "STALE_FEED", - ); - }); - - it("rejects wide confidence intervals", () => { - expectCardanoError( - () => - simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots({ - ada: { confidence: 0.04, exponent: 0, price: 1, emaPrice: 1 }, - }), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }), - "CONFIDENCE_GUARDRAIL", - ); - }); - - it("rejects execution during cooldown", () => { - expectCardanoError( - () => - simulator.authorizeExecution({ - treasury: { - ...sampleTreasury, - lastTransitionUs: 90_000_000, - }, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }), - "COOLDOWN_ACTIVE", - ); - }); - - it("rejects Pyth witness mismatches", () => { - expectCardanoError( - () => - simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness: { - ...witness, - pythPolicyId: "bad-policy-id", - }, - }), - "PYTH_POLICY_MISMATCH", - ); - }); - - it("rejects unauthorized keepers", () => { - expectCardanoError( - () => - simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-x", - witness, - }), - "KEEPER_NOT_AUTHORIZED", - ); - }); - - it("rejects authorization while another intent is in flight", () => { - expectCardanoError( - () => - simulator.authorizeExecution({ - treasury: { - ...sampleTreasury, - currentIntentId: "intent-existing", - }, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }), - "INTENT_ALREADY_IN_FLIGHT", - ); - }); - - it("completes an authorized execution and updates treasury balances", () => { - const authorization = simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }); - const connector = createCardanoConnector(sampleRoutes); - const simulated = connector.simulateRoute( - authorization.intent, - authorization.assessment.metrics.priceMap, - ); - - const completion = simulator.completeExecution({ - treasury: authorization.treasury, - policy: samplePolicy, - intent: authorization.intent, - result: { - intentId: authorization.intent.intentId, - vaultId: sampleTreasury.vaultId, - chainId: "cardano", - sourceAssetId: simulated.sourceAssetId, - destinationAssetId: simulated.destinationAssetId, - soldAmount: simulated.soldAmount, - boughtAmount: simulated.boughtAmount, - averagePrice: simulated.averagePrice, - txHash: "tx-good", - executedAtUs: 100_001_000, - routeId: simulated.routeId, - }, - }); - - expect(completion.treasury.currentIntentId).toBeUndefined(); - expect( - completion.treasury.positions.find((position) => position.assetId === "ada")?.amount, - ).toBeLessThan(10_000); - expect( - completion.treasury.positions.find((position) => position.assetId === "usdm")?.amount, - ).toBeGreaterThan(1_500); - expect(completion.tx.metadata.reason_hash).toBe(authorization.intent.reasonHash); - expect(simulated.averagePrice).toBeCloseTo( - simulated.boughtAmount / simulated.soldAmount, - 8, - ); - }); - - it("rejects executions that do not meet the minimum buy amount", () => { - const authorization = simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }); - - expectCardanoError( - () => - simulator.completeExecution({ - treasury: authorization.treasury, - policy: samplePolicy, - intent: authorization.intent, - result: { - intentId: authorization.intent.intentId, - vaultId: sampleTreasury.vaultId, - chainId: "cardano", - sourceAssetId: authorization.intent.sourceAssetId, - destinationAssetId: authorization.intent.destinationAssetId, - soldAmount: authorization.intent.maxSellAmount, - boughtAmount: authorization.intent.minBuyAmount - 10, - averagePrice: 1, - txHash: "tx-short", - executedAtUs: 101_000_000, - routeId: authorization.intent.routeId, - }, - }), - "MIN_BUY_NOT_MET", - ); - }); - - it("rejects unapproved route/asset pairs at completion time", () => { - const authorized = simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }); - - expectCardanoError( - () => - simulator.completeExecution({ - treasury: authorized.treasury, - policy: samplePolicy, - intent: authorized.intent, - result: { - intentId: authorized.intent.intentId, - vaultId: sampleTreasury.vaultId, - chainId: "cardano", - sourceAssetId: "ada", - destinationAssetId: "evil", - soldAmount: authorized.intent.maxSellAmount, - boughtAmount: authorized.intent.minBuyAmount + 1, - averagePrice: 1, - txHash: "tx-bad", - executedAtUs: 101_000_000, - routeId: authorized.intent.routeId, - }, - }), - "ASSET_NOT_APPROVED", - ); - }); - - it("rejects route mismatches even when the route is approved", () => { - const authorized = simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }); - - expectCardanoError( - () => - simulator.completeExecution({ - treasury: authorized.treasury, - policy: samplePolicy, - intent: authorized.intent, - result: { - intentId: authorized.intent.intentId, - vaultId: sampleTreasury.vaultId, - chainId: "cardano", - sourceAssetId: authorized.intent.sourceAssetId, - destinationAssetId: authorized.intent.destinationAssetId, - soldAmount: authorized.intent.maxSellAmount, - boughtAmount: authorized.intent.minBuyAmount + 1, - averagePrice: 1, - txHash: "tx-other-route", - executedAtUs: 101_000_000, - routeId: "cardano-minswap-usdm-ada", - }, - }), - "ROUTE_MISMATCH", - ); - }); - - it("rejects executions outside the intent window", () => { - const authorized = simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }); - - expectCardanoError( - () => - simulator.completeExecution({ - treasury: authorized.treasury, - policy: samplePolicy, - intent: authorized.intent, - result: { - intentId: authorized.intent.intentId, - vaultId: sampleTreasury.vaultId, - chainId: "cardano", - sourceAssetId: authorized.intent.sourceAssetId, - destinationAssetId: authorized.intent.destinationAssetId, - soldAmount: authorized.intent.maxSellAmount, - boughtAmount: authorized.intent.minBuyAmount + 1, - averagePrice: 1, - txHash: "tx-late", - executedAtUs: authorized.intent.expiryUs + 1, - routeId: authorized.intent.routeId, - }, - }), - "RESULT_OUT_OF_WINDOW", - ); - }); - - it("rejects cross-vault or cross-chain execution results", () => { - const authorized = simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }); - - expectCardanoError( - () => - simulator.completeExecution({ - treasury: authorized.treasury, - policy: samplePolicy, - intent: authorized.intent, - result: { - intentId: authorized.intent.intentId, - vaultId: "vault-elsewhere", - chainId: "evm", - sourceAssetId: authorized.intent.sourceAssetId, - destinationAssetId: authorized.intent.destinationAssetId, - soldAmount: authorized.intent.maxSellAmount, - boughtAmount: authorized.intent.minBuyAmount + 1, - averagePrice: 1, - txHash: "tx-wrong-domain", - executedAtUs: 101_000_000, - routeId: authorized.intent.routeId, - }, - }), - "RESULT_VAULT_MISMATCH", - ); - }); - - it("rejects results that sell more than the authorized intent", () => { - const authorized = simulator.authorizeExecution({ - treasury: sampleTreasury, - policy: samplePolicy, - snapshots: buildSnapshots(), - routes: sampleRoutes, - nowUs: 100_000_000, - keeperId: "keeper-1", - witness, - }); - - expectCardanoError( - () => - simulator.completeExecution({ - treasury: authorized.treasury, - policy: samplePolicy, - intent: authorized.intent, - result: { - intentId: authorized.intent.intentId, - vaultId: sampleTreasury.vaultId, - chainId: "cardano", - sourceAssetId: authorized.intent.sourceAssetId, - destinationAssetId: authorized.intent.destinationAssetId, - soldAmount: authorized.intent.maxSellAmount + 1, - boughtAmount: authorized.intent.minBuyAmount + 1, - averagePrice: 1, - txHash: "tx-oversold", - executedAtUs: 101_000_000, - routeId: authorized.intent.routeId, - }, - }), - "MAX_SELL_EXCEEDED", - ); - }); -}); diff --git a/lazer/cardano/guards-one/source/packages/core/package.json b/lazer/cardano/guards-one/source/packages/core/package.json deleted file mode 100644 index 71501bd0..00000000 --- a/lazer/cardano/guards-one/source/packages/core/package.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "name": "@anaconda/core", - "version": "0.0.0", - "private": true, - "type": "module", - "exports": { - ".": "./src/index.ts" - } -} diff --git a/lazer/cardano/guards-one/source/packages/core/src/capabilities.ts b/lazer/cardano/guards-one/source/packages/core/src/capabilities.ts deleted file mode 100644 index b54f9f9c..00000000 --- a/lazer/cardano/guards-one/source/packages/core/src/capabilities.ts +++ /dev/null @@ -1,50 +0,0 @@ -import type { ChainCapabilities, ChainId, Role } from "./types.js"; - -export const roleModel: Role[] = [ - "governance", - "risk_manager", - "keeper", - "viewer", -]; - -export const baseCapabilities: Record = { - cardano: { - chainId: "cardano", - live: true, - supportsOracleVerifiedExecution: true, - supportsHotColdBuckets: true, - supportsAutoSwap: true, - supportsAutoReentry: true, - supportsLiveDeployments: true, - notes: [ - "Cardano is the only live execution target in the MVP.", - "Execution uses a two-step authorize-and-swap flow.", - ], - }, - svm: { - chainId: "svm", - live: false, - supportsOracleVerifiedExecution: true, - supportsHotColdBuckets: true, - supportsAutoSwap: true, - supportsAutoReentry: true, - supportsLiveDeployments: false, - notes: [ - "Scaffolding only in the MVP.", - "Designed to share the same policy and role model.", - ], - }, - evm: { - chainId: "evm", - live: false, - supportsOracleVerifiedExecution: true, - supportsHotColdBuckets: true, - supportsAutoSwap: true, - supportsAutoReentry: true, - supportsLiveDeployments: false, - notes: [ - "Scaffolding only in the MVP.", - "Connectors remain simulation-first until phase 2.", - ], - }, -}; diff --git a/lazer/cardano/guards-one/source/packages/core/src/engine.ts b/lazer/cardano/guards-one/source/packages/core/src/engine.ts deleted file mode 100644 index 98b8f6a8..00000000 --- a/lazer/cardano/guards-one/source/packages/core/src/engine.ts +++ /dev/null @@ -1,488 +0,0 @@ -import { randomUUID } from "node:crypto"; -import { stableHash } from "./hash.js"; -import { - confidenceBps, - isSnapshotStale, - STAGE_SEVERITY, - stageMax, - summarizePortfolio, - toDecimalPrice, -} from "./math.js"; -import type { - EvaluationReason, - ExecutionIntent, - PolicyConfig, - RiskAssessment, - RiskStage, - RouteSpec, - TreasuryPosition, - TreasuryState, - OracleSnapshot, -} from "./types.js"; - -function buildReason( - code: string, - severity: RiskStage, - message: string, - details?: Record, -): EvaluationReason { - return { code, severity, message, details }; -} - -function clampAmount(amount: number): number { - if (!Number.isFinite(amount) || amount < 0) { - return 0; - } - - return Number(amount.toFixed(8)); -} - -function getPrimaryRiskPosition( - treasury: TreasuryState, - policy: PolicyConfig, -): TreasuryPosition | undefined { - return treasury.positions.find((position) => position.assetId === policy.primaryAssetId); -} - -function getStablePosition( - treasury: TreasuryState, - policy: PolicyConfig, -): TreasuryPosition | undefined { - return treasury.positions.find((position) => position.assetId === policy.stableAssetId); -} - -function chooseRoute( - treasury: TreasuryState, - policy: PolicyConfig, - routes: RouteSpec[], - fromAssetId: string, - toAssetId: string, -): RouteSpec | undefined { - return routes.find((candidate) => - policy.approvedRouteIds.includes(candidate.routeId) && - candidate.chainId === treasury.chainId && - candidate.fromAssetId === fromAssetId && - candidate.toAssetId === toAssetId && - candidate.live, - ); -} - -function buildIntent( - kind: ExecutionIntent["kind"], - stage: RiskStage, - sellAmount: number, - expectedBuyAmount: number, - treasury: TreasuryState, - policy: PolicyConfig, - snapshots: Record, - route: RouteSpec, - nowUs: number, - reasons: EvaluationReason[], -): ExecutionIntent | undefined { - if (sellAmount <= 0 || expectedBuyAmount <= 0) { - return undefined; - } - - const sourceAssetId = - kind === "reentry_swap" ? policy.stableAssetId : policy.primaryAssetId; - const destinationAssetId = - kind === "reentry_swap" ? policy.primaryAssetId : policy.stableAssetId; - const snapshotIds = Object.values(snapshots) - .sort((left, right) => left.snapshotId.localeCompare(right.snapshotId)) - .map((snapshot) => snapshot.snapshotId); - - return { - intentId: randomUUID(), - vaultId: treasury.vaultId, - chainId: treasury.chainId, - kind, - stage, - sourceAssetId, - destinationAssetId, - routeId: route.routeId, - maxSellAmount: clampAmount(sellAmount), - minBuyAmount: clampAmount( - expectedBuyAmount * (1 - route.maxSlippageBps / 10_000), - ), - expiryUs: nowUs + policy.maxStaleUs, - reasonHash: stableHash({ - stage, - reasons, - snapshotIds, - sellAmount, - expectedBuyAmount, - }), - snapshotIds, - createdAtUs: nowUs, - }; -} - -function buildDeriskIntent( - targetStage: RiskStage, - treasury: TreasuryState, - policy: PolicyConfig, - snapshots: Record, - routes: RouteSpec[], - nowUs: number, - reasons: EvaluationReason[], -): ExecutionIntent | undefined { - const riskPosition = getPrimaryRiskPosition(treasury, policy); - const stablePosition = getStablePosition(treasury, policy); - const riskSnapshot = snapshots[policy.primaryAssetId]; - const stableSnapshot = snapshots[policy.stableAssetId]; - - if (!riskPosition || !stablePosition || !riskSnapshot || !stableSnapshot) { - return undefined; - } - - const metrics = summarizePortfolio(treasury.positions, snapshots, policy); - const route = chooseRoute( - treasury, - policy, - routes, - policy.primaryAssetId, - policy.stableAssetId, - ); - if (!route) { - return undefined; - } - const stablePrice = stableSnapshot ? toDecimalPrice(stableSnapshot) : 1; - const riskPrice = toDecimalPrice(riskSnapshot) * (1 - policy.haircutBps / 10_000); - const targetStableValue = - targetStage === "full_exit" - ? metrics.totalLiquidValueFiat - : Math.max( - policy.portfolioFloorFiat, - metrics.totalLiquidValueFiat * policy.partialStableTargetRatio, - ); - const stableGapFiat = Math.max(0, targetStableValue - metrics.stableLiquidValueFiat); - const sellAmount = - targetStage === "full_exit" - ? riskPosition.amount - : Math.min(riskPosition.amount, stableGapFiat / Math.max(riskPrice, 1e-9)); - const expectedBuyAmount = - sellAmount * toDecimalPrice(riskSnapshot) / Math.max(stablePrice, 1e-9); - - return buildIntent( - "derisk_swap", - targetStage, - sellAmount, - expectedBuyAmount, - treasury, - policy, - snapshots, - route, - nowUs, - reasons, - ); -} - -function buildReentryIntent( - treasury: TreasuryState, - policy: PolicyConfig, - snapshots: Record, - routes: RouteSpec[], - nowUs: number, - reasons: EvaluationReason[], -): ExecutionIntent | undefined { - const riskPosition = getPrimaryRiskPosition(treasury, policy); - const stablePosition = getStablePosition(treasury, policy); - const riskSnapshot = snapshots[policy.primaryAssetId]; - const stableSnapshot = snapshots[policy.stableAssetId]; - - if (!riskPosition || !stablePosition || !riskSnapshot || !stableSnapshot) { - return undefined; - } - - const metrics = summarizePortfolio(treasury.positions, snapshots, policy); - const route = chooseRoute( - treasury, - policy, - routes, - policy.stableAssetId, - policy.primaryAssetId, - ); - if (!route) { - return undefined; - } - const stablePrice = stableSnapshot ? toDecimalPrice(stableSnapshot) : 1; - const riskPrice = toDecimalPrice(riskSnapshot); - const targetRiskValue = metrics.totalLiquidValueFiat * policy.reentryRiskTargetRatio; - const riskGapFiat = Math.max(0, targetRiskValue - metrics.riskLiquidValueFiat); - const sellAmount = Math.min( - stablePosition.amount, - riskGapFiat / Math.max(stablePrice, 1e-9), - ); - const expectedBuyAmount = sellAmount * stablePrice / Math.max(riskPrice, 1e-9); - - return buildIntent( - "reentry_swap", - "normal", - sellAmount, - expectedBuyAmount, - treasury, - policy, - snapshots, - route, - nowUs, - reasons, - ); -} - -export function evaluateRiskLadder( - treasury: TreasuryState, - policy: PolicyConfig, - snapshots: Record, - routes: RouteSpec[], - nowUs: number, -): RiskAssessment { - const reasons: EvaluationReason[] = []; - const primarySnapshot = snapshots[policy.primaryAssetId]; - const metrics = summarizePortfolio(treasury.positions, snapshots, policy); - const cooldownRemainingUs = Math.max( - 0, - policy.cooldownUs - (nowUs - treasury.lastTransitionUs), - ); - - if (!primarySnapshot) { - return { - nowUs, - currentStage: treasury.stage, - nextStage: treasury.stage, - metrics, - reasons: [ - buildReason( - "missing_primary_snapshot", - "frozen", - "Primary oracle snapshot is unavailable", - ), - ], - cooldownRemainingUs, - shouldFreeze: true, - }; - } - - if (isSnapshotStale(primarySnapshot, nowUs, policy.maxStaleUs)) { - reasons.push( - buildReason("stale_feed", "frozen", "Primary feed update is stale", { - age_us: nowUs - primarySnapshot.feedUpdateTimestampUs, - }), - ); - } - - const relativeConfidence = confidenceBps(primarySnapshot); - if (relativeConfidence > policy.maxConfidenceBps) { - reasons.push( - buildReason( - "confidence_guardrail", - "frozen", - "Confidence interval is wider than the configured guardrail", - { confidence_bps: Number(relativeConfidence.toFixed(2)) }, - ), - ); - } - - if (reasons.some((reason) => reason.severity === "frozen")) { - return { - nowUs, - currentStage: treasury.stage, - nextStage: "frozen", - metrics, - reasons, - cooldownRemainingUs, - shouldFreeze: true, - }; - } - - let targetStage: RiskStage = "normal"; - if (metrics.drawdownBps >= policy.watchDrawdownBps) { - targetStage = stageMax(targetStage, "watch"); - reasons.push( - buildReason( - "drawdown_watch", - "watch", - "Primary asset drawdown crossed watch threshold", - { drawdown_bps: Number(metrics.drawdownBps.toFixed(2)) }, - ), - ); - } - - if (metrics.drawdownBps >= policy.partialDrawdownBps) { - targetStage = stageMax(targetStage, "partial_derisk"); - reasons.push( - buildReason( - "drawdown_partial", - "partial_derisk", - "Primary asset drawdown crossed partial de-risk threshold", - { drawdown_bps: Number(metrics.drawdownBps.toFixed(2)) }, - ), - ); - } - - if (metrics.drawdownBps >= policy.fullExitDrawdownBps) { - targetStage = stageMax(targetStage, "full_exit"); - reasons.push( - buildReason( - "drawdown_full_exit", - "full_exit", - "Primary asset drawdown crossed full exit threshold", - { drawdown_bps: Number(metrics.drawdownBps.toFixed(2)) }, - ), - ); - } - - if (metrics.totalLiquidValueFiat <= policy.portfolioFloorFiat) { - targetStage = stageMax(targetStage, "partial_derisk"); - reasons.push( - buildReason( - "portfolio_floor_breach", - "partial_derisk", - "Portfolio liquid value fell below the configured floor", - { - total_liquid_fiat: Number(metrics.totalLiquidValueFiat.toFixed(2)), - floor_fiat: policy.portfolioFloorFiat, - }, - ), - ); - } - - if (metrics.totalLiquidValueFiat <= policy.emergencyPortfolioFloorFiat) { - targetStage = stageMax(targetStage, "full_exit"); - reasons.push( - buildReason( - "portfolio_emergency_floor_breach", - "full_exit", - "Portfolio liquid value fell below the emergency floor", - { - total_liquid_fiat: Number(metrics.totalLiquidValueFiat.toFixed(2)), - floor_fiat: policy.emergencyPortfolioFloorFiat, - }, - ), - ); - } - - for (const assetRule of policy.assetRules.filter((rule) => rule.enabled)) { - const liquidValue = metrics.assetLiquidValues[assetRule.assetId] ?? 0; - const hasExposure = treasury.positions.some( - (position) => position.assetId === assetRule.assetId && position.amount > 0, - ); - if (!hasExposure) { - continue; - } - - if (liquidValue <= assetRule.protectedFloorFiat) { - targetStage = stageMax(targetStage, "partial_derisk"); - reasons.push( - buildReason( - "asset_floor_breach", - "partial_derisk", - `${assetRule.symbol} liquid value fell below its protected floor`, - { - asset_id: assetRule.assetId, - liquid_value_fiat: Number(liquidValue.toFixed(2)), - floor_fiat: assetRule.protectedFloorFiat, - }, - ), - ); - } - if (liquidValue <= assetRule.emergencyExitFloorFiat) { - targetStage = stageMax(targetStage, "full_exit"); - reasons.push( - buildReason( - "asset_emergency_floor_breach", - "full_exit", - `${assetRule.symbol} liquid value fell below its emergency floor`, - { - asset_id: assetRule.assetId, - liquid_value_fiat: Number(liquidValue.toFixed(2)), - floor_fiat: assetRule.emergencyExitFloorFiat, - }, - ), - ); - } - } - - const reentryAllowed = - (treasury.stage === "partial_derisk" || treasury.stage === "full_exit") && - metrics.drawdownBps <= policy.reentryDrawdownBps && - metrics.totalLiquidValueFiat > policy.portfolioFloorFiat && - metrics.stableLiquidValueFiat > policy.portfolioFloorFiat; - - let nextStage = targetStage; - let intent: ExecutionIntent | undefined; - - if (cooldownRemainingUs > 0 && targetStage !== "frozen") { - reasons.push( - buildReason( - "cooldown_active", - treasury.stage, - "Cooldown prevents an automatic state transition", - { cooldown_remaining_us: cooldownRemainingUs }, - ), - ); - nextStage = treasury.stage; - } else if (reentryAllowed && targetStage === "normal") { - reasons.push( - buildReason( - "reentry_window", - "normal", - "Recovery conditions satisfied the automatic re-entry band", - { drawdown_bps: Number(metrics.drawdownBps.toFixed(2)) }, - ), - ); - nextStage = "normal"; - intent = buildReentryIntent( - treasury, - policy, - snapshots, - routes, - nowUs, - reasons, - ); - if (!intent) { - reasons.push( - buildReason( - "reentry_route_unavailable", - treasury.stage, - "Recovery conditions are satisfied but no approved route is available for re-entry", - ), - ); - nextStage = treasury.stage; - } - } else if (STAGE_SEVERITY[targetStage] > STAGE_SEVERITY[treasury.stage]) { - nextStage = targetStage; - if (targetStage === "partial_derisk" || targetStage === "full_exit") { - intent = buildDeriskIntent( - targetStage, - treasury, - policy, - snapshots, - routes, - nowUs, - reasons, - ); - if (!intent) { - reasons.push( - buildReason( - "execution_route_unavailable", - targetStage, - "Risk stage changed but no approved route is available for execution", - ), - ); - } - } - } else { - nextStage = treasury.stage; - } - - return { - nowUs, - currentStage: treasury.stage, - nextStage, - metrics, - reasons, - intent, - cooldownRemainingUs, - shouldFreeze: false, - }; -} diff --git a/lazer/cardano/guards-one/source/packages/core/src/fixtures.ts b/lazer/cardano/guards-one/source/packages/core/src/fixtures.ts deleted file mode 100644 index 7b5a4ea0..00000000 --- a/lazer/cardano/guards-one/source/packages/core/src/fixtures.ts +++ /dev/null @@ -1,130 +0,0 @@ -import type { - OracleSnapshot, - PolicyConfig, - RouteSpec, - TreasuryState, -} from "./types.js"; - -export const samplePolicy: PolicyConfig = { - policyId: "policy-treasury-guard-v4", - primaryAssetId: "ada", - primaryFeedId: "pyth-ada-usd", - stableAssetId: "usdm", - approvedRouteIds: [ - "cardano-minswap-ada-usdm", - "cardano-minswap-usdm-ada", - ], - haircutBps: 300, - maxStaleUs: 120_000_000, - maxConfidenceBps: 150, - cooldownUs: 30_000_000, - watchDrawdownBps: 500, - partialDrawdownBps: 900, - fullExitDrawdownBps: 1_500, - reentryDrawdownBps: 250, - portfolioFloorFiat: 4_500, - emergencyPortfolioFloorFiat: 3_400, - partialStableTargetRatio: 0.55, - reentryRiskTargetRatio: 0.65, - assetRules: [ - { - assetId: "ada", - symbol: "ADA", - feedId: "pyth-ada-usd", - protectedFloorFiat: 2_700, - emergencyExitFloorFiat: 2_050, - enabled: true, - }, - ], -}; - -export const sampleRoutes: RouteSpec[] = [ - { - routeId: "cardano-minswap-ada-usdm", - venue: "Minswap", - chainId: "cardano", - fromAssetId: "ada", - toAssetId: "usdm", - maxSlippageBps: 120, - live: true, - notes: "Primary approved route for Cardano execution bucket", - }, - { - routeId: "cardano-minswap-usdm-ada", - venue: "Minswap", - chainId: "cardano", - fromAssetId: "usdm", - toAssetId: "ada", - maxSlippageBps: 120, - live: true, - notes: "Re-entry route for the same pair", - }, -]; - -export const sampleTreasury: TreasuryState = { - vaultId: "vault-anaconda-demo", - name: "Anaconda Treasury Demo", - chainId: "cardano", - stage: "normal", - positions: [ - { - assetId: "ada", - symbol: "ADA", - amount: 10_000, - decimals: 6, - role: "risk", - feedId: "pyth-ada-usd", - bucket: "hot", - }, - { - assetId: "usdm", - symbol: "USDM", - amount: 1_500, - decimals: 6, - role: "stable", - feedId: "stable-usdm-usd", - bucket: "hot", - }, - ], - governanceSigners: ["gov-1", "gov-2", "gov-3"], - riskManagers: ["risk-1"], - keepers: ["keeper-1", "keeper-2"], - viewers: ["viewer-1"], - safeAddresses: ["addr_test_safe_1"], - executionHotWallet: "addr_test_hot_1", - governanceWallet: "addr_test_gov_1", - lastTransitionUs: 0, -}; - -export function buildSnapshots( - overrides?: Partial>>, -): Record { - return { - ada: { - snapshotId: "snapshot-ada", - feedId: "pyth-ada-usd", - assetId: "ada", - symbol: "ADA/USD", - price: 72, - emaPrice: 80, - confidence: 0.4, - exponent: -2, - feedUpdateTimestampUs: 1_000_000, - observedAtUs: 1_000_000, - ...overrides?.ada, - }, - usdm: { - snapshotId: "snapshot-usdm", - feedId: "stable-usdm-usd", - assetId: "usdm", - symbol: "USDM/USD", - price: 1_000_000, - emaPrice: 1_000_000, - confidence: 500, - exponent: -6, - feedUpdateTimestampUs: 1_000_000, - observedAtUs: 1_000_000, - ...overrides?.usdm, - }, - }; -} diff --git a/lazer/cardano/guards-one/source/packages/core/src/hash.ts b/lazer/cardano/guards-one/source/packages/core/src/hash.ts deleted file mode 100644 index 3ddfa345..00000000 --- a/lazer/cardano/guards-one/source/packages/core/src/hash.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { createHash } from "node:crypto"; - -function stableValue(input: unknown): unknown { - if (Array.isArray(input)) { - return input.map(stableValue); - } - - if (input && typeof input === "object") { - return Object.keys(input as Record) - .sort() - .reduce>((accumulator, key) => { - accumulator[key] = stableValue((input as Record)[key]); - return accumulator; - }, {}); - } - - return input; -} - -export function stableHash(input: unknown): string { - return createHash("sha256") - .update(JSON.stringify(stableValue(input))) - .digest("hex"); -} diff --git a/lazer/cardano/guards-one/source/packages/core/src/index.ts b/lazer/cardano/guards-one/source/packages/core/src/index.ts deleted file mode 100644 index 12bfd1c6..00000000 --- a/lazer/cardano/guards-one/source/packages/core/src/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -export * from "./capabilities.js"; -export * from "./engine.js"; -export * from "./fixtures.js"; -export * from "./hash.js"; -export * from "./math.js"; -export * from "./types.js"; diff --git a/lazer/cardano/guards-one/source/packages/core/src/math.ts b/lazer/cardano/guards-one/source/packages/core/src/math.ts deleted file mode 100644 index 39e76a19..00000000 --- a/lazer/cardano/guards-one/source/packages/core/src/math.ts +++ /dev/null @@ -1,135 +0,0 @@ -import type { - OracleSnapshot, - PolicyConfig, - PortfolioMetrics, - RiskStage, - TreasuryPosition, -} from "./types.js"; - -export const STAGE_SEVERITY = { - normal: 0, - watch: 1, - partial_derisk: 2, - full_exit: 3, - frozen: 4, -} as const; - -export function stageAtLeast( - current: RiskStage, - target: RiskStage, -): boolean { - return STAGE_SEVERITY[current] >= STAGE_SEVERITY[target]; -} - -export function stageMax(left: RiskStage, right: RiskStage): RiskStage { - return STAGE_SEVERITY[left] >= STAGE_SEVERITY[right] ? left : right; -} - -export function toDecimalPrice(snapshot: OracleSnapshot): number { - return snapshot.price * 10 ** snapshot.exponent; -} - -export function toDecimalEma(snapshot: OracleSnapshot): number { - return snapshot.emaPrice * 10 ** snapshot.exponent; -} - -export function confidenceBps(snapshot: OracleSnapshot): number { - const price = Math.abs(toDecimalPrice(snapshot)); - if (price === 0) { - return Number.POSITIVE_INFINITY; - } - - return (snapshot.confidence * 10 ** snapshot.exponent) / price * 10_000; -} - -export function isSnapshotStale( - snapshot: OracleSnapshot, - nowUs: number, - maxStaleUs: number, -): boolean { - return nowUs - snapshot.feedUpdateTimestampUs > maxStaleUs; -} - -export function computeDrawdownBps(snapshot: OracleSnapshot): number { - const price = toDecimalPrice(snapshot); - const ema = toDecimalEma(snapshot); - if (ema <= 0 || price >= ema) { - return 0; - } - - return ((ema - price) / ema) * 10_000; -} - -export function applyHaircut(value: number, haircutBps: number): number { - return value * (1 - haircutBps / 10_000); -} - -export function computePositionLiquidValue( - position: TreasuryPosition, - snapshot: OracleSnapshot | undefined, - policy: PolicyConfig, -): number { - if (position.role === "stable") { - if (!snapshot) { - return position.amount; - } - - return position.amount * toDecimalPrice(snapshot); - } - - if (!snapshot) { - return 0; - } - - const price = toDecimalPrice(snapshot); - return applyHaircut(position.amount * price, policy.haircutBps); -} - -export function summarizePortfolio( - positions: TreasuryPosition[], - snapshots: Record, - policy: PolicyConfig, -): PortfolioMetrics { - const assetLiquidValues: Record = {}; - const priceMap: Record = {}; - let totalLiquidValueFiat = 0; - let stableLiquidValueFiat = 0; - let riskLiquidValueFiat = 0; - let drawdownBps = 0; - - for (const position of positions) { - const snapshot = snapshots[position.assetId]; - const liquidValue = computePositionLiquidValue(position, snapshot, policy); - assetLiquidValues[position.assetId] = - (assetLiquidValues[position.assetId] ?? 0) + liquidValue; - - if (snapshot) { - priceMap[position.assetId] = toDecimalPrice(snapshot); - } else if (position.role === "stable") { - priceMap[position.assetId] = 1; - } - - totalLiquidValueFiat += liquidValue; - if (position.role === "stable") { - stableLiquidValueFiat += liquidValue; - } else { - riskLiquidValueFiat += liquidValue; - if (position.assetId === policy.primaryAssetId && snapshot) { - drawdownBps = computeDrawdownBps(snapshot); - } - } - } - - const stableRatio = - totalLiquidValueFiat === 0 ? 0 : stableLiquidValueFiat / totalLiquidValueFiat; - - return { - totalLiquidValueFiat, - stableLiquidValueFiat, - riskLiquidValueFiat, - stableRatio, - drawdownBps, - assetLiquidValues, - priceMap, - }; -} diff --git a/lazer/cardano/guards-one/source/packages/core/src/types.ts b/lazer/cardano/guards-one/source/packages/core/src/types.ts deleted file mode 100644 index bec937f0..00000000 --- a/lazer/cardano/guards-one/source/packages/core/src/types.ts +++ /dev/null @@ -1,183 +0,0 @@ -export type ChainId = "cardano" | "svm" | "evm"; - -export type Role = "governance" | "risk_manager" | "keeper" | "viewer"; - -export type RiskStage = - | "normal" - | "watch" - | "partial_derisk" - | "full_exit" - | "frozen"; - -export type ExecutionKind = "derisk_swap" | "reentry_swap"; - -export interface RouteSpec { - routeId: string; - venue: string; - chainId: ChainId; - fromAssetId: string; - toAssetId: string; - maxSlippageBps: number; - live: boolean; - notes?: string; -} - -export interface AssetRiskRule { - assetId: string; - symbol: string; - feedId: string; - protectedFloorFiat: number; - emergencyExitFloorFiat: number; - enabled: boolean; -} - -export interface PolicyConfig { - policyId: string; - primaryAssetId: string; - primaryFeedId: string; - stableAssetId: string; - approvedRouteIds: string[]; - haircutBps: number; - maxStaleUs: number; - maxConfidenceBps: number; - cooldownUs: number; - watchDrawdownBps: number; - partialDrawdownBps: number; - fullExitDrawdownBps: number; - reentryDrawdownBps: number; - portfolioFloorFiat: number; - emergencyPortfolioFloorFiat: number; - partialStableTargetRatio: number; - reentryRiskTargetRatio: number; - assetRules: AssetRiskRule[]; -} - -export interface OracleSnapshot { - snapshotId: string; - feedId: string; - assetId: string; - symbol: string; - price: number; - emaPrice: number; - confidence: number; - exponent: number; - feedUpdateTimestampUs: number; - observedAtUs: number; - publisherCount?: number; - marketSession?: string; -} - -export interface TreasuryPosition { - assetId: string; - symbol: string; - amount: number; - decimals: number; - role: "risk" | "stable"; - feedId: string; - bucket: "cold" | "hot"; -} - -export interface TreasuryState { - vaultId: string; - name: string; - chainId: ChainId; - stage: RiskStage; - positions: TreasuryPosition[]; - governanceSigners: string[]; - riskManagers: string[]; - keepers: string[]; - viewers: string[]; - safeAddresses: string[]; - executionHotWallet: string; - governanceWallet: string; - lastTransitionUs: number; - currentIntentId?: string | undefined; -} - -export interface PortfolioMetrics { - totalLiquidValueFiat: number; - stableLiquidValueFiat: number; - riskLiquidValueFiat: number; - stableRatio: number; - drawdownBps: number; - assetLiquidValues: Record; - priceMap: Record; -} - -export interface EvaluationReason { - code: string; - severity: RiskStage; - message: string; - details?: Record | undefined; -} - -export interface ExecutionIntent { - intentId: string; - vaultId: string; - chainId: ChainId; - kind: ExecutionKind; - stage: RiskStage; - sourceAssetId: string; - destinationAssetId: string; - routeId: string; - maxSellAmount: number; - minBuyAmount: number; - expiryUs: number; - reasonHash: string; - snapshotIds: string[]; - createdAtUs: number; -} - -export interface ExecutionResult { - intentId: string; - vaultId: string; - chainId: ChainId; - sourceAssetId: string; - destinationAssetId: string; - soldAmount: number; - boughtAmount: number; - averagePrice: number; - txHash: string; - executedAtUs: number; - routeId: string; -} - -export interface RiskAssessment { - nowUs: number; - currentStage: RiskStage; - nextStage: RiskStage; - metrics: PortfolioMetrics; - reasons: EvaluationReason[]; - intent?: ExecutionIntent | undefined; - cooldownRemainingUs: number; - shouldFreeze: boolean; -} - -export interface ChainCapabilities { - chainId: ChainId; - live: boolean; - supportsOracleVerifiedExecution: boolean; - supportsHotColdBuckets: boolean; - supportsAutoSwap: boolean; - supportsAutoReentry: boolean; - supportsLiveDeployments: boolean; - notes: string[]; -} - -export interface TreasuryConnector { - chainId: ChainId; - capabilities: ChainCapabilities; - describeExecutionConstraints(): string[]; - simulateRoute( - intent: ExecutionIntent, - priceMap: Record, - ): Pick< - ExecutionResult, - | "sourceAssetId" - | "destinationAssetId" - | "soldAmount" - | "boughtAmount" - | "averagePrice" - | "routeId" - >; -} diff --git a/lazer/cardano/guards-one/source/packages/core/tests/engine.test.ts b/lazer/cardano/guards-one/source/packages/core/tests/engine.test.ts deleted file mode 100644 index 4293a931..00000000 --- a/lazer/cardano/guards-one/source/packages/core/tests/engine.test.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - buildSnapshots, - evaluateRiskLadder, - samplePolicy, - sampleRoutes, - sampleTreasury, -} from "../src/index.js"; - -describe("core risk ladder", () => { - it("computes partial de-risk when drawdown breaches the medium band", () => { - const assessment = evaluateRiskLadder( - sampleTreasury, - samplePolicy, - buildSnapshots(), - sampleRoutes, - 100_000_000, - ); - - expect(assessment.nextStage).toBe("partial_derisk"); - expect(assessment.intent?.kind).toBe("derisk_swap"); - expect(assessment.intent?.maxSellAmount).toBeGreaterThan(0); - }); - - it("promotes to full exit on emergency floor breach", () => { - const treasury = { - ...sampleTreasury, - positions: sampleTreasury.positions.map((position) => - position.assetId === "usdm" ? { ...position, amount: 300 } : position, - ), - }; - const snapshots = buildSnapshots({ - ada: { price: 42, emaPrice: 80, snapshotId: "snapshot-ada-2" }, - }); - - const assessment = evaluateRiskLadder( - treasury, - samplePolicy, - snapshots, - sampleRoutes, - 100_000_000, - ); - - expect(assessment.nextStage).toBe("full_exit"); - expect(assessment.intent?.stage).toBe("full_exit"); - }); - - it("freezes when the primary snapshot is stale", () => { - const assessment = evaluateRiskLadder( - sampleTreasury, - samplePolicy, - buildSnapshots({ - ada: { feedUpdateTimestampUs: 1_000_000, observedAtUs: 1_000_000 }, - }), - sampleRoutes, - 500_000_000, - ); - - expect(assessment.nextStage).toBe("frozen"); - expect(assessment.shouldFreeze).toBe(true); - expect(assessment.reasons[0]?.code).toBe("stale_feed"); - }); - - it("triggers a reentry swap after recovery and hysteresis", () => { - const treasury = { - ...sampleTreasury, - stage: "partial_derisk" as const, - lastTransitionUs: 0, - positions: sampleTreasury.positions.map((position) => - position.assetId === "ada" - ? { ...position, amount: 6_000 } - : { ...position, amount: 4_800 }, - ), - }; - const snapshots = buildSnapshots({ - ada: { - price: 79, - emaPrice: 80, - confidence: 0.2, - snapshotId: "snapshot-ada-reentry", - }, - }); - - const assessment = evaluateRiskLadder( - treasury, - samplePolicy, - snapshots, - sampleRoutes, - 90_000_000, - ); - - expect(assessment.nextStage).toBe("normal"); - expect(assessment.intent?.kind).toBe("reentry_swap"); - expect(assessment.intent?.destinationAssetId).toBe("ada"); - }); - - it("respects cooldown and blocks new automatic transitions", () => { - const treasury = { - ...sampleTreasury, - lastTransitionUs: 9_000_000, - }; - - const assessment = evaluateRiskLadder( - treasury, - samplePolicy, - buildSnapshots(), - sampleRoutes, - 10_000_000, - ); - - expect(assessment.nextStage).toBe("normal"); - expect(assessment.intent).toBeUndefined(); - expect(assessment.cooldownRemainingUs).toBeGreaterThan(0); - }); - - it("keeps the stage change but emits no intent when no approved route exists", () => { - const assessment = evaluateRiskLadder( - sampleTreasury, - { - ...samplePolicy, - approvedRouteIds: [], - }, - buildSnapshots(), - sampleRoutes, - 100_000_000, - ); - - expect(assessment.nextStage).toBe("partial_derisk"); - expect(assessment.intent).toBeUndefined(); - expect( - assessment.reasons.some((reason) => reason.code === "execution_route_unavailable"), - ).toBe(true); - }); - - it("does not select routes from another chain or disabled routes", () => { - const foreignRoutes = sampleRoutes.map((route, index) => ({ - ...route, - chainId: index === 0 ? "evm" as const : route.chainId, - live: index === 1 ? false : route.live, - })); - - const assessment = evaluateRiskLadder( - sampleTreasury, - samplePolicy, - buildSnapshots(), - foreignRoutes, - 100_000_000, - ); - - expect(assessment.nextStage).toBe("partial_derisk"); - expect(assessment.intent).toBeUndefined(); - expect( - assessment.reasons.some((reason) => reason.code === "execution_route_unavailable"), - ).toBe(true); - }); - - it("builds deterministic snapshot hashes regardless of input object order", () => { - const firstAssessment = evaluateRiskLadder( - sampleTreasury, - samplePolicy, - buildSnapshots(), - sampleRoutes, - 100_000_000, - ); - const reversedSnapshots = buildSnapshots(); - const secondAssessment = evaluateRiskLadder( - sampleTreasury, - samplePolicy, - { - usdm: reversedSnapshots.usdm!, - ada: reversedSnapshots.ada!, - }, - sampleRoutes, - 100_000_000, - ); - - expect(firstAssessment.intent?.snapshotIds).toEqual(secondAssessment.intent?.snapshotIds); - expect(firstAssessment.intent?.reasonHash).toBe(secondAssessment.intent?.reasonHash); - }); - - it("ignores asset floor breaches when the risky asset is fully exited", () => { - const treasury = { - ...sampleTreasury, - stage: "full_exit" as const, - positions: sampleTreasury.positions.map((position) => - position.assetId === "ada" - ? { ...position, amount: 0 } - : { ...position, amount: 7_200 }, - ), - }; - const assessment = evaluateRiskLadder( - treasury, - samplePolicy, - buildSnapshots({ - ada: { - price: 79, - emaPrice: 80, - confidence: 0.2, - snapshotId: "snapshot-ada-flat", - }, - }), - sampleRoutes, - 100_000_000, - ); - - expect(assessment.nextStage).toBe("normal"); - expect(assessment.intent?.kind).toBe("reentry_swap"); - }); -}); diff --git a/lazer/cardano/guards-one/source/packages/core/tests/math.test.ts b/lazer/cardano/guards-one/source/packages/core/tests/math.test.ts deleted file mode 100644 index af572af1..00000000 --- a/lazer/cardano/guards-one/source/packages/core/tests/math.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, expect, it } from "vitest"; -import { - applyHaircut, - buildSnapshots, - computeDrawdownBps, - confidenceBps, - samplePolicy, - sampleTreasury, - summarizePortfolio, - toDecimalEma, - toDecimalPrice, -} from "../src/index.js"; - -describe("core math helpers", () => { - it("converts Pyth values into decimal price and ema", () => { - const snapshots = buildSnapshots(); - expect(toDecimalPrice(snapshots.ada!)).toBe(0.72); - expect(toDecimalEma(snapshots.ada!)).toBe(0.8); - }); - - it("computes relative confidence in bps", () => { - const snapshots = buildSnapshots(); - expect(confidenceBps(snapshots.ada!)).toBeCloseTo(55.5555, 2); - }); - - it("computes drawdown only when price is below ema", () => { - const snapshots = buildSnapshots(); - expect(computeDrawdownBps(snapshots.ada!)).toBeCloseTo(1000, 4); - expect( - computeDrawdownBps({ - ...snapshots.ada!, - price: 81, - }), - ).toBe(0); - }); - - it("applies the configured haircut", () => { - expect(applyHaircut(100, 300)).toBe(97); - }); - - it("summarizes liquid portfolio values and ratios", () => { - const metrics = summarizePortfolio( - sampleTreasury.positions, - buildSnapshots(), - samplePolicy, - ); - - expect(metrics.totalLiquidValueFiat).toBeCloseTo(8484, 2); - expect(metrics.riskLiquidValueFiat).toBeCloseTo(6984, 2); - expect(metrics.stableLiquidValueFiat).toBe(1500); - expect(metrics.stableRatio).toBeCloseTo(0.1768, 3); - }); - - it("values stable assets using the oracle price when a depeg is present", () => { - const metrics = summarizePortfolio( - sampleTreasury.positions, - buildSnapshots({ - usdm: { - price: 970_000, - emaPrice: 990_000, - snapshotId: "snapshot-usdm-depeg", - }, - }), - samplePolicy, - ); - - expect(metrics.stableLiquidValueFiat).toBe(1455); - expect(metrics.totalLiquidValueFiat).toBeCloseTo(8439, 2); - expect(metrics.stableRatio).toBeCloseTo(1455 / 8439, 4); - }); -}); diff --git a/lazer/cardano/guards-one/source/packages/evm/package.json b/lazer/cardano/guards-one/source/packages/evm/package.json deleted file mode 100644 index ca74ee66..00000000 --- a/lazer/cardano/guards-one/source/packages/evm/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "@anaconda/evm", - "version": "0.0.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "dependencies": { - "@anaconda/core": "workspace:*" - } -} diff --git a/lazer/cardano/guards-one/source/packages/evm/src/fixtures.ts b/lazer/cardano/guards-one/source/packages/evm/src/fixtures.ts deleted file mode 100644 index fbdc1027..00000000 --- a/lazer/cardano/guards-one/source/packages/evm/src/fixtures.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { RouteSpec, TreasuryProfile } from './index.js'; - -export const evmFixtures = { - treasury: { - treasuryId: 'evm-demo-treasury', - chainId: 'evm', - vaultMode: 'watch', - governanceThreshold: 3, - approvedRiskOffAssets: ['USDC', 'USDT'], - protectedAssets: ['ETH', 'BTC'], - } satisfies TreasuryProfile, - route: { - fromAsset: 'ETH', - toAsset: 'USDC', - venue: 'simulated-evm-route', - maxSlippageBps: 100, - routeId: 'evm-route-01', - } satisfies RouteSpec, - simulationPrice: { - fromAssetPrice: 3200, - toAssetPrice: 1, - }, -} as const; diff --git a/lazer/cardano/guards-one/source/packages/evm/src/index.ts b/lazer/cardano/guards-one/source/packages/evm/src/index.ts deleted file mode 100644 index 10062b7d..00000000 --- a/lazer/cardano/guards-one/source/packages/evm/src/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - baseCapabilities, - roleModel, - type ChainCapabilities, - type ChainId as CoreChainId, - type RiskStage, - type Role as CoreRole, -} from '../../core/src/index.js'; -import { evmFixtures } from './fixtures.js'; - -export type Role = CoreRole; - -export type ChainId = Extract; - -export type VaultMode = RiskStage; - -export interface TreasuryProfile { - treasuryId: string; - chainId: ChainId; - vaultMode: VaultMode; - governanceThreshold: number; - approvedRiskOffAssets: readonly string[]; - protectedAssets: readonly string[]; -} - -export interface RouteSpec { - routeId: string; - fromAsset: string; - toAsset: string; - venue: string; - maxSlippageBps: number; -} - -export interface RouteSimulationInput { - route: RouteSpec; - amountIn: number; - fromAssetPrice: number; - toAssetPrice: number; - haircutBps?: number; -} - -export interface RouteSimulationResult { - chainId: ChainId; - routeId: string; - canExecute: boolean; - estimatedOutput: number; - estimatedValueUsd: number; - estimatedImpactBps: number; - executionNotes: readonly string[]; -} - -export interface ExecutionConstraints { - isLive: false; - executionMode: 'scaffold'; - requiresPreapproval: true; - routeSimulationOnly: true; - maxRouteAgeSeconds: number; - allowedRoles: readonly Role[]; - notes: readonly string[]; -} - -export interface TreasuryConnector { - chainId: ChainId; - roles: readonly Role[]; - capabilities: ChainCapabilities; - executionConstraints: ExecutionConstraints; - simulateRoute(input: RouteSimulationInput): RouteSimulationResult; - describeAvailability(): string; -} - -export const chainId: ChainId = 'evm'; - -export const capabilities: ChainCapabilities = baseCapabilities.evm; - -export const executionConstraints: ExecutionConstraints = { - isLive: false, - executionMode: 'scaffold', - requiresPreapproval: true, - routeSimulationOnly: true, - maxRouteAgeSeconds: 20, - allowedRoles: ['governance', 'risk_manager', 'keeper', 'viewer'], - notes: [ - 'EVM support is scaffolded for phase 2.', - 'Route simulation is deterministic and does not submit real transactions.', - 'Execution must be approved by governance before any live adapter is added.', - ], -}; - -export const roles: readonly Role[] = roleModel; - -export const createEvmConnector = (): TreasuryConnector => ({ - chainId, - roles, - capabilities, - executionConstraints, - simulateRoute: (input) => { - const validationNotes: string[] = []; - - if (!Number.isFinite(input.amountIn) || input.amountIn <= 0) { - validationNotes.push('Invalid amountIn: must be finite and > 0.'); - } - if (!Number.isFinite(input.fromAssetPrice) || input.fromAssetPrice <= 0) { - validationNotes.push('Invalid fromAssetPrice: must be finite and > 0.'); - } - if (!Number.isFinite(input.toAssetPrice) || input.toAssetPrice <= 0) { - validationNotes.push('Invalid toAssetPrice: must be finite and > 0.'); - } - - if (validationNotes.length > 0) { - return { - chainId, - routeId: input.route.routeId, - canExecute: false, - estimatedOutput: 0, - estimatedValueUsd: 0, - estimatedImpactBps: 0, - executionNotes: [ - 'Scaffold connector only.', - `Venue ${input.route.venue} is simulated, not live.`, - 'Route blocked due to invalid simulation inputs.', - ...validationNotes, - ], - }; - } - - const haircutBps = input.haircutBps ?? 150; - const grossValue = input.amountIn * input.fromAssetPrice; - const effectiveOutputValue = grossValue * (1 - haircutBps / 10_000); - const estimatedOutput = effectiveOutputValue / input.toAssetPrice; - const estimatedImpactBps = Math.min(input.route.maxSlippageBps, haircutBps); - const canExecute = estimatedOutput > 0 && input.route.maxSlippageBps <= 300; - - return { - chainId, - routeId: input.route.routeId, - canExecute, - estimatedOutput, - estimatedValueUsd: effectiveOutputValue, - estimatedImpactBps, - executionNotes: [ - 'Scaffold connector only.', - `Venue ${input.route.venue} is simulated, not live.`, - canExecute ? 'Route is eligible for future live adapter work.' : 'Route blocked by constraint or invalid sizing.', - ], - }; - }, - describeAvailability: () => - 'EVM connector is scaffold-only. It exposes policy, route simulation and role surfaces for future live integration.', -}); - -export const fixtures = evmFixtures; diff --git a/lazer/cardano/guards-one/source/packages/evm/tests/index.test.ts b/lazer/cardano/guards-one/source/packages/evm/tests/index.test.ts deleted file mode 100644 index 0d68a2ff..00000000 --- a/lazer/cardano/guards-one/source/packages/evm/tests/index.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { chainId, capabilities, createEvmConnector, executionConstraints, fixtures, roles } from '../src/index.js'; - -describe('@anaconda/evm', () => { - it('exposes a scaffold-only capability matrix', () => { - expect(chainId).toBe('evm'); - expect(capabilities.live).toBe(false); - expect(capabilities.supportsAutoSwap).toBe(true); - expect(executionConstraints.isLive).toBe(false); - expect(roles).toEqual(['governance', 'risk_manager', 'keeper', 'viewer']); - }); - - it('simulates routes using the fixture surface', () => { - const connector = createEvmConnector(); - const result = connector.simulateRoute({ - route: fixtures.route, - amountIn: 2, - fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, - toAssetPrice: fixtures.simulationPrice.toAssetPrice, - }); - - expect(result.chainId).toBe('evm'); - expect(result.canExecute).toBe(true); - expect(result.estimatedOutput).toBeGreaterThan(0); - expect(result.executionNotes[0]).toContain('Scaffold connector only.'); - }); - - it('blocks invalid or too-wide routes in simulation', () => { - const connector = createEvmConnector(); - const result = connector.simulateRoute({ - route: { - ...fixtures.route, - maxSlippageBps: 400, - }, - amountIn: 0, - fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, - toAssetPrice: fixtures.simulationPrice.toAssetPrice, - }); - - expect(result.canExecute).toBe(false); - }); - - it('rejects invalid pricing inputs', () => { - const connector = createEvmConnector(); - const result = connector.simulateRoute({ - route: fixtures.route, - amountIn: 2, - fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, - toAssetPrice: 0, - }); - - expect(result.canExecute).toBe(false); - expect(result.estimatedOutput).toBe(0); - expect(result.executionNotes.some((note) => note.includes('Invalid toAssetPrice'))).toBe(true); - }); -}); diff --git a/lazer/cardano/guards-one/source/packages/svm/package.json b/lazer/cardano/guards-one/source/packages/svm/package.json deleted file mode 100644 index 7f47cc4c..00000000 --- a/lazer/cardano/guards-one/source/packages/svm/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "@anaconda/svm", - "version": "0.0.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "exports": { - ".": "./src/index.ts" - }, - "dependencies": { - "@anaconda/core": "workspace:*" - } -} diff --git a/lazer/cardano/guards-one/source/packages/svm/src/fixtures.ts b/lazer/cardano/guards-one/source/packages/svm/src/fixtures.ts deleted file mode 100644 index 7ebaa56e..00000000 --- a/lazer/cardano/guards-one/source/packages/svm/src/fixtures.ts +++ /dev/null @@ -1,23 +0,0 @@ -import type { RouteSpec, TreasuryProfile } from './index.js'; - -export const svmFixtures = { - treasury: { - treasuryId: 'svm-demo-treasury', - chainId: 'svm', - vaultMode: 'watch', - governanceThreshold: 2, - approvedRiskOffAssets: ['USDC'], - protectedAssets: ['SOL', 'BTC'], - } satisfies TreasuryProfile, - route: { - fromAsset: 'SOL', - toAsset: 'USDC', - venue: 'simulated-svm-route', - maxSlippageBps: 75, - routeId: 'svm-route-01', - } satisfies RouteSpec, - simulationPrice: { - fromAssetPrice: 178.25, - toAssetPrice: 1, - }, -} as const; diff --git a/lazer/cardano/guards-one/source/packages/svm/src/index.ts b/lazer/cardano/guards-one/source/packages/svm/src/index.ts deleted file mode 100644 index 194cf3f7..00000000 --- a/lazer/cardano/guards-one/source/packages/svm/src/index.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - baseCapabilities, - roleModel, - type ChainCapabilities, - type ChainId as CoreChainId, - type RiskStage, - type Role as CoreRole, -} from '../../core/src/index.js'; -import { svmFixtures } from './fixtures.js'; - -export type Role = CoreRole; - -export type ChainId = Extract; - -export type VaultMode = RiskStage; - -export interface TreasuryProfile { - treasuryId: string; - chainId: ChainId; - vaultMode: VaultMode; - governanceThreshold: number; - approvedRiskOffAssets: readonly string[]; - protectedAssets: readonly string[]; -} - -export interface RouteSpec { - routeId: string; - fromAsset: string; - toAsset: string; - venue: string; - maxSlippageBps: number; -} - -export interface RouteSimulationInput { - route: RouteSpec; - amountIn: number; - fromAssetPrice: number; - toAssetPrice: number; - haircutBps?: number; -} - -export interface RouteSimulationResult { - chainId: ChainId; - routeId: string; - canExecute: boolean; - estimatedOutput: number; - estimatedValueUsd: number; - estimatedImpactBps: number; - executionNotes: readonly string[]; -} - -export interface ExecutionConstraints { - isLive: false; - executionMode: 'scaffold'; - requiresPreapproval: true; - routeSimulationOnly: true; - maxRouteAgeSeconds: number; - allowedRoles: readonly Role[]; - notes: readonly string[]; -} - -export interface TreasuryConnector { - chainId: ChainId; - roles: readonly Role[]; - capabilities: ChainCapabilities; - executionConstraints: ExecutionConstraints; - simulateRoute(input: RouteSimulationInput): RouteSimulationResult; - describeAvailability(): string; -} - -export const chainId: ChainId = 'svm'; - -export const capabilities: ChainCapabilities = baseCapabilities.svm; - -export const executionConstraints: ExecutionConstraints = { - isLive: false, - executionMode: 'scaffold', - requiresPreapproval: true, - routeSimulationOnly: true, - maxRouteAgeSeconds: 30, - allowedRoles: ['governance', 'risk_manager', 'keeper', 'viewer'], - notes: [ - 'SVM support is scaffolded for phase 2.', - 'Route simulation is deterministic and does not submit real transactions.', - 'Execution must be approved by governance before any live adapter is added.', - ], -}; - -export const roles: readonly Role[] = roleModel; - -export const createSvmConnector = (): TreasuryConnector => ({ - chainId, - roles, - capabilities, - executionConstraints, - simulateRoute: (input) => { - const validationNotes: string[] = []; - - if (!Number.isFinite(input.amountIn) || input.amountIn <= 0) { - validationNotes.push('Invalid amountIn: must be finite and > 0.'); - } - if (!Number.isFinite(input.fromAssetPrice) || input.fromAssetPrice <= 0) { - validationNotes.push('Invalid fromAssetPrice: must be finite and > 0.'); - } - if (!Number.isFinite(input.toAssetPrice) || input.toAssetPrice <= 0) { - validationNotes.push('Invalid toAssetPrice: must be finite and > 0.'); - } - - if (validationNotes.length > 0) { - return { - chainId, - routeId: input.route.routeId, - canExecute: false, - estimatedOutput: 0, - estimatedValueUsd: 0, - estimatedImpactBps: 0, - executionNotes: [ - 'Scaffold connector only.', - `Venue ${input.route.venue} is simulated, not live.`, - 'Route blocked due to invalid simulation inputs.', - ...validationNotes, - ], - }; - } - - const haircutBps = input.haircutBps ?? 125; - const grossValue = input.amountIn * input.fromAssetPrice; - const effectiveOutputValue = grossValue * (1 - haircutBps / 10_000); - const estimatedOutput = effectiveOutputValue / input.toAssetPrice; - const estimatedImpactBps = Math.min(input.route.maxSlippageBps, haircutBps); - const canExecute = estimatedOutput > 0 && input.route.maxSlippageBps <= 250; - - return { - chainId, - routeId: input.route.routeId, - canExecute, - estimatedOutput, - estimatedValueUsd: effectiveOutputValue, - estimatedImpactBps, - executionNotes: [ - 'Scaffold connector only.', - `Venue ${input.route.venue} is simulated, not live.`, - canExecute ? 'Route is eligible for future live adapter work.' : 'Route blocked by constraint or invalid sizing.', - ], - }; - }, - describeAvailability: () => - 'SVM connector is scaffold-only. It exposes policy, route simulation and role surfaces for future live integration.', -}); - -export const fixtures = svmFixtures; diff --git a/lazer/cardano/guards-one/source/packages/svm/tests/index.test.ts b/lazer/cardano/guards-one/source/packages/svm/tests/index.test.ts deleted file mode 100644 index 914d4598..00000000 --- a/lazer/cardano/guards-one/source/packages/svm/tests/index.test.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { chainId, capabilities, createSvmConnector, executionConstraints, fixtures, roles } from '../src/index.js'; - -describe('@anaconda/svm', () => { - it('exposes a scaffold-only capability matrix', () => { - expect(chainId).toBe('svm'); - expect(capabilities.live).toBe(false); - expect(capabilities.supportsAutoSwap).toBe(true); - expect(executionConstraints.isLive).toBe(false); - expect(roles).toEqual(['governance', 'risk_manager', 'keeper', 'viewer']); - }); - - it('simulates routes using the fixture surface', () => { - const connector = createSvmConnector(); - const result = connector.simulateRoute({ - route: fixtures.route, - amountIn: 10, - fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, - toAssetPrice: fixtures.simulationPrice.toAssetPrice, - }); - - expect(result.chainId).toBe('svm'); - expect(result.canExecute).toBe(true); - expect(result.estimatedOutput).toBeGreaterThan(0); - expect(result.executionNotes[0]).toContain('Scaffold connector only.'); - }); - - it('blocks invalid or too-wide routes in simulation', () => { - const connector = createSvmConnector(); - const result = connector.simulateRoute({ - route: { - ...fixtures.route, - maxSlippageBps: 400, - }, - amountIn: 0, - fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, - toAssetPrice: fixtures.simulationPrice.toAssetPrice, - }); - - expect(result.canExecute).toBe(false); - }); - - it('rejects invalid pricing inputs', () => { - const connector = createSvmConnector(); - const result = connector.simulateRoute({ - route: fixtures.route, - amountIn: 10, - fromAssetPrice: fixtures.simulationPrice.fromAssetPrice, - toAssetPrice: 0, - }); - - expect(result.canExecute).toBe(false); - expect(result.estimatedOutput).toBe(0); - expect(result.executionNotes.some((note) => note.includes('Invalid toAssetPrice'))).toBe(true); - }); -}); diff --git a/lazer/cardano/guards-one/source/pnpm-lock.yaml b/lazer/cardano/guards-one/source/pnpm-lock.yaml deleted file mode 100644 index 820df2c3..00000000 --- a/lazer/cardano/guards-one/source/pnpm-lock.yaml +++ /dev/null @@ -1,1090 +0,0 @@ -lockfileVersion: '9.0' - -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - -importers: - - .: - devDependencies: - '@types/node': - specifier: ^24.0.0 - version: 24.12.0 - tsx: - specifier: ^4.20.3 - version: 4.21.0 - typescript: - specifier: ^5.8.3 - version: 5.9.3 - vitest: - specifier: ^3.2.4 - version: 3.2.4(@types/node@24.12.0)(tsx@4.21.0) - - apps/backend: - dependencies: - '@anaconda/cardano': - specifier: workspace:* - version: link:../../packages/cardano - '@anaconda/core': - specifier: workspace:* - version: link:../../packages/core - dotenv: - specifier: ^17.2.3 - version: 17.3.1 - - packages/cardano: - dependencies: - '@anaconda/core': - specifier: workspace:* - version: link:../core - - packages/core: {} - - packages/evm: - dependencies: - '@anaconda/core': - specifier: workspace:* - version: link:../core - - packages/svm: - dependencies: - '@anaconda/core': - specifier: workspace:* - version: link:../core - -packages: - - '@esbuild/aix-ppc64@0.27.4': - resolution: {integrity: sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.27.4': - resolution: {integrity: sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.27.4': - resolution: {integrity: sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==} - engines: {node: '>=18'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.27.4': - resolution: {integrity: sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==} - engines: {node: '>=18'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.27.4': - resolution: {integrity: sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==} - engines: {node: '>=18'} - cpu: [arm64] - os: [darwin] - - '@esbuild/darwin-x64@0.27.4': - resolution: {integrity: sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==} - engines: {node: '>=18'} - cpu: [x64] - os: [darwin] - - '@esbuild/freebsd-arm64@0.27.4': - resolution: {integrity: sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==} - engines: {node: '>=18'} - cpu: [arm64] - os: [freebsd] - - '@esbuild/freebsd-x64@0.27.4': - resolution: {integrity: sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [freebsd] - - '@esbuild/linux-arm64@0.27.4': - resolution: {integrity: sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==} - engines: {node: '>=18'} - cpu: [arm64] - os: [linux] - - '@esbuild/linux-arm@0.27.4': - resolution: {integrity: sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==} - engines: {node: '>=18'} - cpu: [arm] - os: [linux] - - '@esbuild/linux-ia32@0.27.4': - resolution: {integrity: sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==} - engines: {node: '>=18'} - cpu: [ia32] - os: [linux] - - '@esbuild/linux-loong64@0.27.4': - resolution: {integrity: sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==} - engines: {node: '>=18'} - cpu: [loong64] - os: [linux] - - '@esbuild/linux-mips64el@0.27.4': - resolution: {integrity: sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==} - engines: {node: '>=18'} - cpu: [mips64el] - os: [linux] - - '@esbuild/linux-ppc64@0.27.4': - resolution: {integrity: sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==} - engines: {node: '>=18'} - cpu: [ppc64] - os: [linux] - - '@esbuild/linux-riscv64@0.27.4': - resolution: {integrity: sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==} - engines: {node: '>=18'} - cpu: [riscv64] - os: [linux] - - '@esbuild/linux-s390x@0.27.4': - resolution: {integrity: sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==} - engines: {node: '>=18'} - cpu: [s390x] - os: [linux] - - '@esbuild/linux-x64@0.27.4': - resolution: {integrity: sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==} - engines: {node: '>=18'} - cpu: [x64] - os: [linux] - - '@esbuild/netbsd-arm64@0.27.4': - resolution: {integrity: sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==} - engines: {node: '>=18'} - cpu: [arm64] - os: [netbsd] - - '@esbuild/netbsd-x64@0.27.4': - resolution: {integrity: sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==} - engines: {node: '>=18'} - cpu: [x64] - os: [netbsd] - - '@esbuild/openbsd-arm64@0.27.4': - resolution: {integrity: sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openbsd] - - '@esbuild/openbsd-x64@0.27.4': - resolution: {integrity: sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==} - engines: {node: '>=18'} - cpu: [x64] - os: [openbsd] - - '@esbuild/openharmony-arm64@0.27.4': - resolution: {integrity: sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [openharmony] - - '@esbuild/sunos-x64@0.27.4': - resolution: {integrity: sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==} - engines: {node: '>=18'} - cpu: [x64] - os: [sunos] - - '@esbuild/win32-arm64@0.27.4': - resolution: {integrity: sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==} - engines: {node: '>=18'} - cpu: [arm64] - os: [win32] - - '@esbuild/win32-ia32@0.27.4': - resolution: {integrity: sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==} - engines: {node: '>=18'} - cpu: [ia32] - os: [win32] - - '@esbuild/win32-x64@0.27.4': - resolution: {integrity: sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==} - engines: {node: '>=18'} - cpu: [x64] - os: [win32] - - '@jridgewell/sourcemap-codec@1.5.5': - resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - - '@rollup/rollup-android-arm-eabi@4.60.0': - resolution: {integrity: sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==} - cpu: [arm] - os: [android] - - '@rollup/rollup-android-arm64@4.60.0': - resolution: {integrity: sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==} - cpu: [arm64] - os: [android] - - '@rollup/rollup-darwin-arm64@4.60.0': - resolution: {integrity: sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==} - cpu: [arm64] - os: [darwin] - - '@rollup/rollup-darwin-x64@4.60.0': - resolution: {integrity: sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==} - cpu: [x64] - os: [darwin] - - '@rollup/rollup-freebsd-arm64@4.60.0': - resolution: {integrity: sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==} - cpu: [arm64] - os: [freebsd] - - '@rollup/rollup-freebsd-x64@4.60.0': - resolution: {integrity: sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==} - cpu: [x64] - os: [freebsd] - - '@rollup/rollup-linux-arm-gnueabihf@4.60.0': - resolution: {integrity: sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm-musleabihf@4.60.0': - resolution: {integrity: sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==} - cpu: [arm] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-arm64-gnu@4.60.0': - resolution: {integrity: sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-arm64-musl@4.60.0': - resolution: {integrity: sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-loong64-gnu@4.60.0': - resolution: {integrity: sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==} - cpu: [loong64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-loong64-musl@4.60.0': - resolution: {integrity: sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==} - cpu: [loong64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-ppc64-gnu@4.60.0': - resolution: {integrity: sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-ppc64-musl@4.60.0': - resolution: {integrity: sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==} - cpu: [ppc64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-riscv64-gnu@4.60.0': - resolution: {integrity: sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-riscv64-musl@4.60.0': - resolution: {integrity: sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==} - cpu: [riscv64] - os: [linux] - libc: [musl] - - '@rollup/rollup-linux-s390x-gnu@4.60.0': - resolution: {integrity: sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-gnu@4.60.0': - resolution: {integrity: sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@rollup/rollup-linux-x64-musl@4.60.0': - resolution: {integrity: sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@rollup/rollup-openbsd-x64@4.60.0': - resolution: {integrity: sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==} - cpu: [x64] - os: [openbsd] - - '@rollup/rollup-openharmony-arm64@4.60.0': - resolution: {integrity: sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==} - cpu: [arm64] - os: [openharmony] - - '@rollup/rollup-win32-arm64-msvc@4.60.0': - resolution: {integrity: sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==} - cpu: [arm64] - os: [win32] - - '@rollup/rollup-win32-ia32-msvc@4.60.0': - resolution: {integrity: sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==} - cpu: [ia32] - os: [win32] - - '@rollup/rollup-win32-x64-gnu@4.60.0': - resolution: {integrity: sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==} - cpu: [x64] - os: [win32] - - '@rollup/rollup-win32-x64-msvc@4.60.0': - resolution: {integrity: sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==} - cpu: [x64] - os: [win32] - - '@types/chai@5.2.3': - resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} - - '@types/deep-eql@4.0.2': - resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} - - '@types/estree@1.0.8': - resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - - '@types/node@24.12.0': - resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} - - '@vitest/expect@3.2.4': - resolution: {integrity: sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==} - - '@vitest/mocker@3.2.4': - resolution: {integrity: sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==} - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - - '@vitest/pretty-format@3.2.4': - resolution: {integrity: sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==} - - '@vitest/runner@3.2.4': - resolution: {integrity: sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==} - - '@vitest/snapshot@3.2.4': - resolution: {integrity: sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==} - - '@vitest/spy@3.2.4': - resolution: {integrity: sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==} - - '@vitest/utils@3.2.4': - resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} - - assertion-error@2.0.1: - resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} - engines: {node: '>=12'} - - cac@6.7.14: - resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} - engines: {node: '>=8'} - - chai@5.3.3: - resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} - engines: {node: '>=18'} - - check-error@2.1.3: - resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} - engines: {node: '>= 16'} - - debug@4.4.3: - resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - - deep-eql@5.0.2: - resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} - engines: {node: '>=6'} - - dotenv@17.3.1: - resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} - engines: {node: '>=12'} - - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} - - esbuild@0.27.4: - resolution: {integrity: sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==} - engines: {node: '>=18'} - hasBin: true - - estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - - expect-type@1.3.0: - resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} - engines: {node: '>=12.0.0'} - - fdir@6.5.0: - resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} - engines: {node: '>=12.0.0'} - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - - fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - - get-tsconfig@4.13.7: - resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} - - js-tokens@9.0.1: - resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} - - loupe@3.2.1: - resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} - - magic-string@0.30.21: - resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - - ms@2.1.3: - resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - - nanoid@3.3.11: - resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - - pathe@2.0.3: - resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} - - pathval@2.0.1: - resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} - engines: {node: '>= 14.16'} - - picocolors@1.1.1: - resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - - picomatch@4.0.3: - resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} - engines: {node: '>=12'} - - postcss@8.5.8: - resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} - engines: {node: ^10 || ^12 || >=14} - - resolve-pkg-maps@1.0.0: - resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} - - rollup@4.60.0: - resolution: {integrity: sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true - - siginfo@2.0.0: - resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} - - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} - engines: {node: '>=0.10.0'} - - stackback@0.0.2: - resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} - - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} - - strip-literal@3.1.0: - resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - - tinybench@2.9.0: - resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - - tinyexec@0.3.2: - resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} - - tinyglobby@0.2.15: - resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} - engines: {node: '>=12.0.0'} - - tinypool@1.1.1: - resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} - engines: {node: ^18.0.0 || >=20.0.0} - - tinyrainbow@2.0.0: - resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} - engines: {node: '>=14.0.0'} - - tinyspy@4.0.4: - resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} - engines: {node: '>=14.0.0'} - - tsx@4.21.0: - resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} - engines: {node: '>=18.0.0'} - hasBin: true - - typescript@5.9.3: - resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} - engines: {node: '>=14.17'} - hasBin: true - - undici-types@7.16.0: - resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - - vite-node@3.2.4: - resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - - vite@7.3.1: - resolution: {integrity: sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==} - engines: {node: ^20.19.0 || >=22.12.0} - hasBin: true - peerDependencies: - '@types/node': ^20.19.0 || >=22.12.0 - jiti: '>=1.21.0' - less: ^4.0.0 - lightningcss: ^1.21.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 - jiti: - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - - vitest@3.2.4: - resolution: {integrity: sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==} - engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} - hasBin: true - peerDependencies: - '@edge-runtime/vm': '*' - '@types/debug': ^4.1.12 - '@types/node': ^18.0.0 || ^20.0.0 || >=22.0.0 - '@vitest/browser': 3.2.4 - '@vitest/ui': 3.2.4 - happy-dom: '*' - jsdom: '*' - peerDependenciesMeta: - '@edge-runtime/vm': - optional: true - '@types/debug': - optional: true - '@types/node': - optional: true - '@vitest/browser': - optional: true - '@vitest/ui': - optional: true - happy-dom: - optional: true - jsdom: - optional: true - - why-is-node-running@2.3.0: - resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} - engines: {node: '>=8'} - hasBin: true - -snapshots: - - '@esbuild/aix-ppc64@0.27.4': - optional: true - - '@esbuild/android-arm64@0.27.4': - optional: true - - '@esbuild/android-arm@0.27.4': - optional: true - - '@esbuild/android-x64@0.27.4': - optional: true - - '@esbuild/darwin-arm64@0.27.4': - optional: true - - '@esbuild/darwin-x64@0.27.4': - optional: true - - '@esbuild/freebsd-arm64@0.27.4': - optional: true - - '@esbuild/freebsd-x64@0.27.4': - optional: true - - '@esbuild/linux-arm64@0.27.4': - optional: true - - '@esbuild/linux-arm@0.27.4': - optional: true - - '@esbuild/linux-ia32@0.27.4': - optional: true - - '@esbuild/linux-loong64@0.27.4': - optional: true - - '@esbuild/linux-mips64el@0.27.4': - optional: true - - '@esbuild/linux-ppc64@0.27.4': - optional: true - - '@esbuild/linux-riscv64@0.27.4': - optional: true - - '@esbuild/linux-s390x@0.27.4': - optional: true - - '@esbuild/linux-x64@0.27.4': - optional: true - - '@esbuild/netbsd-arm64@0.27.4': - optional: true - - '@esbuild/netbsd-x64@0.27.4': - optional: true - - '@esbuild/openbsd-arm64@0.27.4': - optional: true - - '@esbuild/openbsd-x64@0.27.4': - optional: true - - '@esbuild/openharmony-arm64@0.27.4': - optional: true - - '@esbuild/sunos-x64@0.27.4': - optional: true - - '@esbuild/win32-arm64@0.27.4': - optional: true - - '@esbuild/win32-ia32@0.27.4': - optional: true - - '@esbuild/win32-x64@0.27.4': - optional: true - - '@jridgewell/sourcemap-codec@1.5.5': {} - - '@rollup/rollup-android-arm-eabi@4.60.0': - optional: true - - '@rollup/rollup-android-arm64@4.60.0': - optional: true - - '@rollup/rollup-darwin-arm64@4.60.0': - optional: true - - '@rollup/rollup-darwin-x64@4.60.0': - optional: true - - '@rollup/rollup-freebsd-arm64@4.60.0': - optional: true - - '@rollup/rollup-freebsd-x64@4.60.0': - optional: true - - '@rollup/rollup-linux-arm-gnueabihf@4.60.0': - optional: true - - '@rollup/rollup-linux-arm-musleabihf@4.60.0': - optional: true - - '@rollup/rollup-linux-arm64-gnu@4.60.0': - optional: true - - '@rollup/rollup-linux-arm64-musl@4.60.0': - optional: true - - '@rollup/rollup-linux-loong64-gnu@4.60.0': - optional: true - - '@rollup/rollup-linux-loong64-musl@4.60.0': - optional: true - - '@rollup/rollup-linux-ppc64-gnu@4.60.0': - optional: true - - '@rollup/rollup-linux-ppc64-musl@4.60.0': - optional: true - - '@rollup/rollup-linux-riscv64-gnu@4.60.0': - optional: true - - '@rollup/rollup-linux-riscv64-musl@4.60.0': - optional: true - - '@rollup/rollup-linux-s390x-gnu@4.60.0': - optional: true - - '@rollup/rollup-linux-x64-gnu@4.60.0': - optional: true - - '@rollup/rollup-linux-x64-musl@4.60.0': - optional: true - - '@rollup/rollup-openbsd-x64@4.60.0': - optional: true - - '@rollup/rollup-openharmony-arm64@4.60.0': - optional: true - - '@rollup/rollup-win32-arm64-msvc@4.60.0': - optional: true - - '@rollup/rollup-win32-ia32-msvc@4.60.0': - optional: true - - '@rollup/rollup-win32-x64-gnu@4.60.0': - optional: true - - '@rollup/rollup-win32-x64-msvc@4.60.0': - optional: true - - '@types/chai@5.2.3': - dependencies: - '@types/deep-eql': 4.0.2 - assertion-error: 2.0.1 - - '@types/deep-eql@4.0.2': {} - - '@types/estree@1.0.8': {} - - '@types/node@24.12.0': - dependencies: - undici-types: 7.16.0 - - '@vitest/expect@3.2.4': - dependencies: - '@types/chai': 5.2.3 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - tinyrainbow: 2.0.0 - - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@24.12.0)(tsx@4.21.0))': - dependencies: - '@vitest/spy': 3.2.4 - estree-walker: 3.0.3 - magic-string: 0.30.21 - optionalDependencies: - vite: 7.3.1(@types/node@24.12.0)(tsx@4.21.0) - - '@vitest/pretty-format@3.2.4': - dependencies: - tinyrainbow: 2.0.0 - - '@vitest/runner@3.2.4': - dependencies: - '@vitest/utils': 3.2.4 - pathe: 2.0.3 - strip-literal: 3.1.0 - - '@vitest/snapshot@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - magic-string: 0.30.21 - pathe: 2.0.3 - - '@vitest/spy@3.2.4': - dependencies: - tinyspy: 4.0.4 - - '@vitest/utils@3.2.4': - dependencies: - '@vitest/pretty-format': 3.2.4 - loupe: 3.2.1 - tinyrainbow: 2.0.0 - - assertion-error@2.0.1: {} - - cac@6.7.14: {} - - chai@5.3.3: - dependencies: - assertion-error: 2.0.1 - check-error: 2.1.3 - deep-eql: 5.0.2 - loupe: 3.2.1 - pathval: 2.0.1 - - check-error@2.1.3: {} - - debug@4.4.3: - dependencies: - ms: 2.1.3 - - deep-eql@5.0.2: {} - - dotenv@17.3.1: {} - - es-module-lexer@1.7.0: {} - - esbuild@0.27.4: - optionalDependencies: - '@esbuild/aix-ppc64': 0.27.4 - '@esbuild/android-arm': 0.27.4 - '@esbuild/android-arm64': 0.27.4 - '@esbuild/android-x64': 0.27.4 - '@esbuild/darwin-arm64': 0.27.4 - '@esbuild/darwin-x64': 0.27.4 - '@esbuild/freebsd-arm64': 0.27.4 - '@esbuild/freebsd-x64': 0.27.4 - '@esbuild/linux-arm': 0.27.4 - '@esbuild/linux-arm64': 0.27.4 - '@esbuild/linux-ia32': 0.27.4 - '@esbuild/linux-loong64': 0.27.4 - '@esbuild/linux-mips64el': 0.27.4 - '@esbuild/linux-ppc64': 0.27.4 - '@esbuild/linux-riscv64': 0.27.4 - '@esbuild/linux-s390x': 0.27.4 - '@esbuild/linux-x64': 0.27.4 - '@esbuild/netbsd-arm64': 0.27.4 - '@esbuild/netbsd-x64': 0.27.4 - '@esbuild/openbsd-arm64': 0.27.4 - '@esbuild/openbsd-x64': 0.27.4 - '@esbuild/openharmony-arm64': 0.27.4 - '@esbuild/sunos-x64': 0.27.4 - '@esbuild/win32-arm64': 0.27.4 - '@esbuild/win32-ia32': 0.27.4 - '@esbuild/win32-x64': 0.27.4 - - estree-walker@3.0.3: - dependencies: - '@types/estree': 1.0.8 - - expect-type@1.3.0: {} - - fdir@6.5.0(picomatch@4.0.3): - optionalDependencies: - picomatch: 4.0.3 - - fsevents@2.3.3: - optional: true - - get-tsconfig@4.13.7: - dependencies: - resolve-pkg-maps: 1.0.0 - - js-tokens@9.0.1: {} - - loupe@3.2.1: {} - - magic-string@0.30.21: - dependencies: - '@jridgewell/sourcemap-codec': 1.5.5 - - ms@2.1.3: {} - - nanoid@3.3.11: {} - - pathe@2.0.3: {} - - pathval@2.0.1: {} - - picocolors@1.1.1: {} - - picomatch@4.0.3: {} - - postcss@8.5.8: - dependencies: - nanoid: 3.3.11 - picocolors: 1.1.1 - source-map-js: 1.2.1 - - resolve-pkg-maps@1.0.0: {} - - rollup@4.60.0: - dependencies: - '@types/estree': 1.0.8 - optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.60.0 - '@rollup/rollup-android-arm64': 4.60.0 - '@rollup/rollup-darwin-arm64': 4.60.0 - '@rollup/rollup-darwin-x64': 4.60.0 - '@rollup/rollup-freebsd-arm64': 4.60.0 - '@rollup/rollup-freebsd-x64': 4.60.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.60.0 - '@rollup/rollup-linux-arm-musleabihf': 4.60.0 - '@rollup/rollup-linux-arm64-gnu': 4.60.0 - '@rollup/rollup-linux-arm64-musl': 4.60.0 - '@rollup/rollup-linux-loong64-gnu': 4.60.0 - '@rollup/rollup-linux-loong64-musl': 4.60.0 - '@rollup/rollup-linux-ppc64-gnu': 4.60.0 - '@rollup/rollup-linux-ppc64-musl': 4.60.0 - '@rollup/rollup-linux-riscv64-gnu': 4.60.0 - '@rollup/rollup-linux-riscv64-musl': 4.60.0 - '@rollup/rollup-linux-s390x-gnu': 4.60.0 - '@rollup/rollup-linux-x64-gnu': 4.60.0 - '@rollup/rollup-linux-x64-musl': 4.60.0 - '@rollup/rollup-openbsd-x64': 4.60.0 - '@rollup/rollup-openharmony-arm64': 4.60.0 - '@rollup/rollup-win32-arm64-msvc': 4.60.0 - '@rollup/rollup-win32-ia32-msvc': 4.60.0 - '@rollup/rollup-win32-x64-gnu': 4.60.0 - '@rollup/rollup-win32-x64-msvc': 4.60.0 - fsevents: 2.3.3 - - siginfo@2.0.0: {} - - source-map-js@1.2.1: {} - - stackback@0.0.2: {} - - std-env@3.10.0: {} - - strip-literal@3.1.0: - dependencies: - js-tokens: 9.0.1 - - tinybench@2.9.0: {} - - tinyexec@0.3.2: {} - - tinyglobby@0.2.15: - dependencies: - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - - tinypool@1.1.1: {} - - tinyrainbow@2.0.0: {} - - tinyspy@4.0.4: {} - - tsx@4.21.0: - dependencies: - esbuild: 0.27.4 - get-tsconfig: 4.13.7 - optionalDependencies: - fsevents: 2.3.3 - - typescript@5.9.3: {} - - undici-types@7.16.0: {} - - vite-node@3.2.4(@types/node@24.12.0)(tsx@4.21.0): - dependencies: - cac: 6.7.14 - debug: 4.4.3 - es-module-lexer: 1.7.0 - pathe: 2.0.3 - vite: 7.3.1(@types/node@24.12.0)(tsx@4.21.0) - transitivePeerDependencies: - - '@types/node' - - jiti - - less - - lightningcss - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - vite@7.3.1(@types/node@24.12.0)(tsx@4.21.0): - dependencies: - esbuild: 0.27.4 - fdir: 6.5.0(picomatch@4.0.3) - picomatch: 4.0.3 - postcss: 8.5.8 - rollup: 4.60.0 - tinyglobby: 0.2.15 - optionalDependencies: - '@types/node': 24.12.0 - fsevents: 2.3.3 - tsx: 4.21.0 - - vitest@3.2.4(@types/node@24.12.0)(tsx@4.21.0): - dependencies: - '@types/chai': 5.2.3 - '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@24.12.0)(tsx@4.21.0)) - '@vitest/pretty-format': 3.2.4 - '@vitest/runner': 3.2.4 - '@vitest/snapshot': 3.2.4 - '@vitest/spy': 3.2.4 - '@vitest/utils': 3.2.4 - chai: 5.3.3 - debug: 4.4.3 - expect-type: 1.3.0 - magic-string: 0.30.21 - pathe: 2.0.3 - picomatch: 4.0.3 - std-env: 3.10.0 - tinybench: 2.9.0 - tinyexec: 0.3.2 - tinyglobby: 0.2.15 - tinypool: 1.1.1 - tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@24.12.0)(tsx@4.21.0) - vite-node: 3.2.4(@types/node@24.12.0)(tsx@4.21.0) - why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 24.12.0 - transitivePeerDependencies: - - jiti - - less - - lightningcss - - msw - - sass - - sass-embedded - - stylus - - sugarss - - supports-color - - terser - - tsx - - yaml - - why-is-node-running@2.3.0: - dependencies: - siginfo: 2.0.0 - stackback: 0.0.2 diff --git a/lazer/cardano/guards-one/source/pnpm-workspace.yaml b/lazer/cardano/guards-one/source/pnpm-workspace.yaml deleted file mode 100644 index 0e5a0737..00000000 --- a/lazer/cardano/guards-one/source/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - - "packages/*" - - "apps/*" diff --git a/lazer/cardano/guards-one/source/tsconfig.json b/lazer/cardano/guards-one/source/tsconfig.json deleted file mode 100644 index 297a1e18..00000000 --- a/lazer/cardano/guards-one/source/tsconfig.json +++ /dev/null @@ -1,33 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2022", - "module": "NodeNext", - "moduleResolution": "NodeNext", - "strict": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "skipLibCheck": true, - "resolveJsonModule": true, - "noUncheckedIndexedAccess": true, - "exactOptionalPropertyTypes": true, - "baseUrl": ".", - "paths": { - "@anaconda/core": [ - "packages/core/src/index.ts" - ], - "@anaconda/cardano": [ - "packages/cardano/src/index.ts" - ], - "@anaconda/svm": [ - "packages/svm/src/index.ts" - ], - "@anaconda/evm": [ - "packages/evm/src/index.ts" - ] - } - }, - "include": [ - "packages/**/*.ts", - "apps/**/*.ts" - ] -} From 71b81685503b4023ee7499702552eec65ca491d7 Mon Sep 17 00:00:00 2001 From: Nicolas Fernandez Date: Sun, 22 Mar 2026 18:42:04 -0300 Subject: [PATCH 3/9] docs(submission): bootstrap guards draft entry --- lazer/cardano/guards-one/README.md | 7 ------- lazer/cardano/guards/README.md | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 7 deletions(-) delete mode 100644 lazer/cardano/guards-one/README.md create mode 100644 lazer/cardano/guards/README.md diff --git a/lazer/cardano/guards-one/README.md b/lazer/cardano/guards-one/README.md deleted file mode 100644 index 82398b10..00000000 --- a/lazer/cardano/guards-one/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# guards.one - -Draft placeholder only. - -This PR is intentionally empty for now and should not be reviewed as a final hackathon submission. - -A complete Cardano + Pyth submission will be pushed later. diff --git a/lazer/cardano/guards/README.md b/lazer/cardano/guards/README.md new file mode 100644 index 00000000..b1d250c8 --- /dev/null +++ b/lazer/cardano/guards/README.md @@ -0,0 +1,14 @@ +# Team guards.one Pythathon Submission + +## Details + +Team Name: guards.one +Submission Name: guards +Team Members: TBD +Contact: TBD + +## Project Description + +guards is a Cardano treasury protection workflow that uses Pyth price feeds to monitor liquid portfolio value, stable-denominated floors, and oracle-aware de-risking conditions. + +This is the initial draft submission only. Source code, setup steps, and the full demo flow will be added incrementally in later updates to this draft PR. From 0f3961a661d35fea9e8911894e130c7c59ec1b9c Mon Sep 17 00:00:00 2001 From: Nicolas Fernandez Date: Sun, 22 Mar 2026 18:45:29 -0300 Subject: [PATCH 4/9] docs(submission): add guards team metadata --- lazer/cardano/guards/README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/lazer/cardano/guards/README.md b/lazer/cardano/guards/README.md index b1d250c8..b3a9d723 100644 --- a/lazer/cardano/guards/README.md +++ b/lazer/cardano/guards/README.md @@ -1,14 +1,14 @@ -# Team guards.one Pythathon Submission +# Team Guards Pythathon Submission ## Details -Team Name: guards.one -Submission Name: guards -Team Members: TBD -Contact: TBD +Team Name: Guards +Submission Name: Guards One +Team Members: @f0x1777 @kevan1 @joaco05 +Contact: @f0x1777 ## Project Description -guards is a Cardano treasury protection workflow that uses Pyth price feeds to monitor liquid portfolio value, stable-denominated floors, and oracle-aware de-risking conditions. +Guards is a Cardano treasury protection workflow that uses Pyth price feeds to monitor liquid portfolio value, stable-denominated floors, and oracle-aware de-risking conditions. This is the initial draft submission only. Source code, setup steps, and the full demo flow will be added incrementally in later updates to this draft PR. From 5740a341ba6a94d7def8b2460b91f7bcf6377761 Mon Sep 17 00:00:00 2001 From: Nicolas Fernandez Date: Sun, 22 Mar 2026 18:47:29 -0300 Subject: [PATCH 5/9] docs(submission): clarify multichain positioning --- lazer/cardano/guards/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lazer/cardano/guards/README.md b/lazer/cardano/guards/README.md index b3a9d723..2af1313b 100644 --- a/lazer/cardano/guards/README.md +++ b/lazer/cardano/guards/README.md @@ -9,6 +9,6 @@ Contact: @f0x1777 ## Project Description -Guards is a Cardano treasury protection workflow that uses Pyth price feeds to monitor liquid portfolio value, stable-denominated floors, and oracle-aware de-risking conditions. +Guards is a multichain treasury protection workflow that uses Pyth price feeds to monitor liquid portfolio value, stable-denominated floors, and oracle-aware de-risking conditions. This submission focuses on the Cardano deployment surface. This is the initial draft submission only. Source code, setup steps, and the full demo flow will be added incrementally in later updates to this draft PR. From 60e0e592dfb664e57463b590458ec0d0096a6bfc Mon Sep 17 00:00:00 2001 From: Nicolas Fernandez Date: Sun, 22 Mar 2026 18:48:26 -0300 Subject: [PATCH 6/9] docs(submission): expand testing and verification notes --- lazer/cardano/guards/README.md | 62 ++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/lazer/cardano/guards/README.md b/lazer/cardano/guards/README.md index 2af1313b..80cc5285 100644 --- a/lazer/cardano/guards/README.md +++ b/lazer/cardano/guards/README.md @@ -12,3 +12,65 @@ Contact: @f0x1777 Guards is a multichain treasury protection workflow that uses Pyth price feeds to monitor liquid portfolio value, stable-denominated floors, and oracle-aware de-risking conditions. This submission focuses on the Cardano deployment surface. This is the initial draft submission only. Source code, setup steps, and the full demo flow will be added incrementally in later updates to this draft PR. + +## Testing & Verification + +### How to Test This Contribution + +The current implementation can be verified in three layers: + +1. Static checks: typecheck and unit/integration tests +2. Local simulation: deterministic breach -> de-risk -> exit -> recovery flow +3. Live oracle wiring: fetch a real signed Pyth update for Cardano preprod + +### Prerequisites + +- Node.js `>= 24.0.0` +- `pnpm` +- A `.env` file based on `.env.example` +- A valid `PYTH_API_KEY` +- Cardano preprod configuration for live verification: + - `PYTH_PREPROD_POLICY_ID` + - `CARDANO_BLOCKFROST_PROJECT_ID` + - `CARDANO_PYTH_STATE_REFERENCE` + +### Setup & Run Instructions + +```bash +pnpm install +cp .env.example .env +``` + +Fill the required environment variables, then run: + +```bash +# Static validation +pnpm typecheck +pnpm test + +# Deterministic treasury simulation +pnpm simulate + +# Fetch a live signed Pyth update for the Cardano preprod flow +pnpm pyth:fetch-live + +# Run the UI locally +pnpm --dir apps/ui dev +``` + +Local UI routes: + +- Landing: `http://localhost:3000` +- Dashboard: `http://localhost:3000/dashboard` + +### Deployment Information + +Current deployment target for the hackathon is **Cardano preprod**. + +Relevant deployment/runtime notes: + +- Pyth preprod policy id: + `d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6` +- Frontend can be deployed from `apps/ui` +- Off-chain oracle/keeper services require a Node 24 runtime plus the Cardano/Pyth environment variables listed above +- The multichain control-plane logic is shared, but this submission's live execution path is centered on Cardano preprod From e483e768f250719782ffb3aa829bea965020c245 Mon Sep 17 00:00:00 2001 From: Nicolas Fernandez Date: Sun, 22 Mar 2026 20:58:02 -0300 Subject: [PATCH 7/9] feat(submission): add merged guards source snapshot --- lazer/cardano/guards/source/.env.example | 42 + .../source/.github/copilot-instructions.md | 12 + lazer/cardano/guards/source/.gitignore | 9 + lazer/cardano/guards/source/INTERNAL_NOTES.md | 137 ++ lazer/cardano/guards/source/NEXT_STEPS.md | 175 ++ lazer/cardano/guards/source/README.md | 204 ++ .../guards/source/apps/backend/package.json | 11 + .../source/apps/backend/src/collector.ts | 1 + .../source/apps/backend/src/dashboard.ts | 399 +++ .../source/apps/backend/src/demo-state.ts | 290 +++ .../apps/backend/src/dexhunter-keeper.ts | 136 ++ .../source/apps/backend/src/dexhunter-live.ts | 300 +++ .../guards/source/apps/backend/src/env.ts | 1 + .../source/apps/backend/src/export-ui-data.ts | 16 + .../source/apps/backend/src/fixtures.ts | 5 + .../guards/source/apps/backend/src/keeper.ts | 135 ++ .../source/apps/backend/src/preview-server.ts | 88 + .../source/apps/backend/src/protocol-fee.ts | 125 + .../source/apps/backend/src/risk-engine.ts | 20 + .../source/apps/backend/src/simulate.ts | 4 + .../guards/source/apps/backend/src/storage.ts | 131 + .../source/apps/backend/src/swap-venue.ts | 100 + .../apps/backend/tests/dashboard.test.ts | 21 + .../apps/backend/tests/dexhunter-live.test.ts | 219 ++ .../source/apps/backend/tests/e2e.test.ts | 83 + .../apps/backend/tests/protocol-fee.test.ts | 106 + .../source/apps/backend/tests/storage.test.ts | 56 + .../apps/backend/tests/swap-venue.test.ts | 46 + .../guards/source/apps/blockchain/README.md | 48 + .../blockchain/cardano/contracts/README.md | 35 + .../cardano/contracts/aiken/README.md | 40 + .../cardano/contracts/aiken/aiken.toml | 13 + .../aiken/lib/guards_one/policy_vault.ak | 112 + .../contracts/aiken/lib/guards_one/types.ak | 88 + .../contracts/aiken/test/policy_vault.ak | 10 + .../aiken/validators/policy_vault.ak | 27 + .../blockchain/cardano/offchain/README.md | 30 + .../blockchain/cardano/offchain/collector.ts | 276 +++ .../apps/blockchain/cardano/offchain/env.ts | 94 + .../blockchain/cardano/offchain/fixtures.ts | 59 + .../apps/blockchain/cardano/offchain/index.ts | 4 + .../cardano/offchain/scripts/fetch-live.ts | 26 + .../apps/blockchain/cardano/offchain/types.ts | 52 + .../source/apps/blockchain/evm/README.md | 14 + .../source/apps/blockchain/package.json | 25 + .../source/apps/blockchain/src/cardano.ts | 7 + .../guards/source/apps/blockchain/src/evm.ts | 1 + .../source/apps/blockchain/src/index.ts | 4 + .../source/apps/blockchain/src/manifests.ts | 81 + .../guards/source/apps/blockchain/src/svm.ts | 1 + .../source/apps/blockchain/svm/README.md | 14 + .../blockchain/tests/cardano-offchain.test.ts | 198 ++ .../apps/blockchain/tests/manifests.test.ts | 24 + .../guards/source/apps/ui-legacy/app.js | 617 +++++ .../apps/ui-legacy/data/demo-state.json | 268 ++ .../guards/source/apps/ui-legacy/index.html | 203 ++ .../guards/source/apps/ui-legacy/styles.css | 881 +++++++ .../cardano/guards/source/apps/ui/.gitignore | 4 + .../source/apps/ui/app/dashboard/page.tsx | 299 +++ .../guards/source/apps/ui/app/globals.css | 227 ++ .../guards/source/apps/ui/app/layout.tsx | 37 + .../guards/source/apps/ui/app/page.tsx | 21 + .../apps/ui/components/accounts-table.tsx | 96 + .../source/apps/ui/components/audit-log.tsx | 60 + .../apps/ui/components/execution-timeline.tsx | 114 + .../ui/components/historical-strategy-lab.tsx | 236 ++ .../apps/ui/components/landing/animations.tsx | 90 + .../ui/components/landing/cta-section.tsx | 50 + .../apps/ui/components/landing/footer.tsx | 42 + .../apps/ui/components/landing/hero.tsx | 94 + .../ui/components/landing/how-it-works.tsx | 389 +++ .../components/landing/multichain-section.tsx | 102 + .../apps/ui/components/landing/navbar.tsx | 127 + .../ui/components/landing/pyth-banner.tsx | 64 + .../ui/components/landing/pyth-section.tsx | 117 + .../components/landing/video-background.tsx | 66 + .../source/apps/ui/components/metric-card.tsx | 57 + .../apps/ui/components/multichain-status.tsx | 73 + .../apps/ui/components/policy-cards.tsx | 103 + .../apps/ui/components/preprod-warning.tsx | 142 ++ .../source/apps/ui/components/risk-ladder.tsx | 132 + .../ui/components/runtime-control-panel.tsx | 199 ++ .../apps/ui/components/scenario-lab.tsx | 321 +++ .../source/apps/ui/components/sidebar.tsx | 201 ++ .../apps/ui/components/simulation-replay.tsx | 230 ++ .../source/apps/ui/components/swap-panel.tsx | 167 ++ .../source/apps/ui/components/topbar.tsx | 171 ++ .../ui/components/vault-bootstrap-lab.tsx | 411 ++++ .../ui/components/vault-profile-panel.tsx | 100 + .../guards/source/apps/ui/lib/demo-data.ts | 149 ++ .../source/apps/ui/lib/mock-backtest.test.ts | 77 + .../source/apps/ui/lib/mock-backtest.ts | 504 ++++ .../guards/source/apps/ui/lib/runtime.ts | 11 + .../guards/source/apps/ui/lib/stage.ts | 59 + .../guards/source/apps/ui/lib/types.ts | 94 + .../source/apps/ui/lib/vault-lab.test.ts | 117 + .../guards/source/apps/ui/lib/vault-lab.ts | 584 +++++ .../source/apps/ui/lib/wallet-session.ts | 220 ++ .../guards/source/apps/ui/next-env.d.ts | 6 + .../guards/source/apps/ui/next.config.ts | 7 + .../guards/source/apps/ui/package.json | 31 + .../guards/source/apps/ui/postcss.config.mjs | 8 + .../source/apps/ui/public/chain-cardano.png | Bin 0 -> 33209 bytes .../source/apps/ui/public/chain-ethereum.svg | 1 + .../source/apps/ui/public/chain-solana.png | Bin 0 -> 37677 bytes .../source/apps/ui/public/guards-icon.svg | 16 + .../source/apps/ui/public/guards-logo.png | Bin 0 -> 50330 bytes .../guards/source/apps/ui/public/hero-bg.jpeg | Bin 0 -> 2444911 bytes .../source/apps/ui/public/pyth-logo.png | Bin 0 -> 157792 bytes .../guards/source/apps/ui/tsconfig.json | 45 + .../source/contracts/cardano-aiken/README.md | 7 + .../source/docs/cardano-custody-model.md | 154 ++ .../docs/cardano-swap-venue-decision.md | 64 + .../source/docs/dexhunter-live-adapter.md | 45 + .../guards/source/docs/functional-v4.md | 128 + .../source/docs/landing-frontend-spec.md | 100 + .../source/docs/preprod-vault-bootstrap.md | 165 ++ .../guards/source/docs/protocol-fee-model.md | 70 + .../guards/source/docs/pyth-live-collector.md | 74 + lazer/cardano/guards/source/docs/roadmap.md | 39 + lazer/cardano/guards/source/package.json | 27 + .../source/packages/cardano/package.json | 12 + .../source/packages/cardano/src/index.ts | 2 + .../packages/cardano/src/policy-vault.ts | 379 +++ .../source/packages/cardano/src/types.ts | 65 + .../cardano/tests/policy-vault.test.ts | 421 ++++ .../guards/source/packages/core/package.json | 9 + .../source/packages/core/src/capabilities.ts | 50 + .../guards/source/packages/core/src/engine.ts | 522 ++++ .../source/packages/core/src/fixtures.ts | 130 + .../guards/source/packages/core/src/hash.ts | 131 + .../guards/source/packages/core/src/index.ts | 6 + .../guards/source/packages/core/src/math.ts | 135 ++ .../guards/source/packages/core/src/types.ts | 183 ++ .../source/packages/core/tests/engine.test.ts | 209 ++ .../source/packages/core/tests/math.test.ts | 71 + .../guards/source/packages/evm/package.json | 14 + .../source/packages/evm/src/fixtures.ts | 23 + .../guards/source/packages/evm/src/index.ts | 151 ++ .../source/packages/evm/tests/index.test.ts | 56 + .../guards/source/packages/svm/package.json | 14 + .../source/packages/svm/src/fixtures.ts | 23 + .../guards/source/packages/svm/src/index.ts | 151 ++ .../source/packages/svm/tests/index.test.ts | 56 + lazer/cardano/guards/source/pnpm-lock.yaml | 2159 +++++++++++++++++ .../cardano/guards/source/pnpm-workspace.yaml | 3 + lazer/cardano/guards/source/tsconfig.json | 43 + 147 files changed, 18486 insertions(+) create mode 100644 lazer/cardano/guards/source/.env.example create mode 100644 lazer/cardano/guards/source/.github/copilot-instructions.md create mode 100644 lazer/cardano/guards/source/.gitignore create mode 100644 lazer/cardano/guards/source/INTERNAL_NOTES.md create mode 100644 lazer/cardano/guards/source/NEXT_STEPS.md create mode 100644 lazer/cardano/guards/source/README.md create mode 100644 lazer/cardano/guards/source/apps/backend/package.json create mode 100644 lazer/cardano/guards/source/apps/backend/src/collector.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/dashboard.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/demo-state.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/dexhunter-keeper.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/dexhunter-live.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/env.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/export-ui-data.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/fixtures.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/keeper.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/preview-server.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/protocol-fee.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/risk-engine.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/simulate.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/storage.ts create mode 100644 lazer/cardano/guards/source/apps/backend/src/swap-venue.ts create mode 100644 lazer/cardano/guards/source/apps/backend/tests/dashboard.test.ts create mode 100644 lazer/cardano/guards/source/apps/backend/tests/dexhunter-live.test.ts create mode 100644 lazer/cardano/guards/source/apps/backend/tests/e2e.test.ts create mode 100644 lazer/cardano/guards/source/apps/backend/tests/protocol-fee.test.ts create mode 100644 lazer/cardano/guards/source/apps/backend/tests/storage.test.ts create mode 100644 lazer/cardano/guards/source/apps/backend/tests/swap-venue.test.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/README.md create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/contracts/README.md create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/README.md create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/aiken.toml create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/lib/guards_one/policy_vault.ak create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/lib/guards_one/types.ak create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/test/policy_vault.ak create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/validators/policy_vault.ak create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/offchain/README.md create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/offchain/collector.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/offchain/env.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/offchain/fixtures.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/offchain/index.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/offchain/scripts/fetch-live.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/cardano/offchain/types.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/evm/README.md create mode 100644 lazer/cardano/guards/source/apps/blockchain/package.json create mode 100644 lazer/cardano/guards/source/apps/blockchain/src/cardano.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/src/evm.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/src/index.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/src/manifests.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/src/svm.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/svm/README.md create mode 100644 lazer/cardano/guards/source/apps/blockchain/tests/cardano-offchain.test.ts create mode 100644 lazer/cardano/guards/source/apps/blockchain/tests/manifests.test.ts create mode 100644 lazer/cardano/guards/source/apps/ui-legacy/app.js create mode 100644 lazer/cardano/guards/source/apps/ui-legacy/data/demo-state.json create mode 100644 lazer/cardano/guards/source/apps/ui-legacy/index.html create mode 100644 lazer/cardano/guards/source/apps/ui-legacy/styles.css create mode 100644 lazer/cardano/guards/source/apps/ui/.gitignore create mode 100644 lazer/cardano/guards/source/apps/ui/app/dashboard/page.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/app/globals.css create mode 100644 lazer/cardano/guards/source/apps/ui/app/layout.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/app/page.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/accounts-table.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/audit-log.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/execution-timeline.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/historical-strategy-lab.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/landing/animations.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/landing/cta-section.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/landing/footer.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/landing/hero.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/landing/how-it-works.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/landing/multichain-section.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/landing/navbar.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/landing/pyth-banner.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/landing/pyth-section.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/landing/video-background.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/metric-card.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/multichain-status.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/policy-cards.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/preprod-warning.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/risk-ladder.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/runtime-control-panel.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/scenario-lab.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/sidebar.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/simulation-replay.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/swap-panel.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/topbar.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/vault-bootstrap-lab.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/components/vault-profile-panel.tsx create mode 100644 lazer/cardano/guards/source/apps/ui/lib/demo-data.ts create mode 100644 lazer/cardano/guards/source/apps/ui/lib/mock-backtest.test.ts create mode 100644 lazer/cardano/guards/source/apps/ui/lib/mock-backtest.ts create mode 100644 lazer/cardano/guards/source/apps/ui/lib/runtime.ts create mode 100644 lazer/cardano/guards/source/apps/ui/lib/stage.ts create mode 100644 lazer/cardano/guards/source/apps/ui/lib/types.ts create mode 100644 lazer/cardano/guards/source/apps/ui/lib/vault-lab.test.ts create mode 100644 lazer/cardano/guards/source/apps/ui/lib/vault-lab.ts create mode 100644 lazer/cardano/guards/source/apps/ui/lib/wallet-session.ts create mode 100644 lazer/cardano/guards/source/apps/ui/next-env.d.ts create mode 100644 lazer/cardano/guards/source/apps/ui/next.config.ts create mode 100644 lazer/cardano/guards/source/apps/ui/package.json create mode 100644 lazer/cardano/guards/source/apps/ui/postcss.config.mjs create mode 100644 lazer/cardano/guards/source/apps/ui/public/chain-cardano.png create mode 100644 lazer/cardano/guards/source/apps/ui/public/chain-ethereum.svg create mode 100644 lazer/cardano/guards/source/apps/ui/public/chain-solana.png create mode 100644 lazer/cardano/guards/source/apps/ui/public/guards-icon.svg create mode 100644 lazer/cardano/guards/source/apps/ui/public/guards-logo.png create mode 100644 lazer/cardano/guards/source/apps/ui/public/hero-bg.jpeg create mode 100644 lazer/cardano/guards/source/apps/ui/public/pyth-logo.png create mode 100644 lazer/cardano/guards/source/apps/ui/tsconfig.json create mode 100644 lazer/cardano/guards/source/contracts/cardano-aiken/README.md create mode 100644 lazer/cardano/guards/source/docs/cardano-custody-model.md create mode 100644 lazer/cardano/guards/source/docs/cardano-swap-venue-decision.md create mode 100644 lazer/cardano/guards/source/docs/dexhunter-live-adapter.md create mode 100644 lazer/cardano/guards/source/docs/functional-v4.md create mode 100644 lazer/cardano/guards/source/docs/landing-frontend-spec.md create mode 100644 lazer/cardano/guards/source/docs/preprod-vault-bootstrap.md create mode 100644 lazer/cardano/guards/source/docs/protocol-fee-model.md create mode 100644 lazer/cardano/guards/source/docs/pyth-live-collector.md create mode 100644 lazer/cardano/guards/source/docs/roadmap.md create mode 100644 lazer/cardano/guards/source/package.json create mode 100644 lazer/cardano/guards/source/packages/cardano/package.json create mode 100644 lazer/cardano/guards/source/packages/cardano/src/index.ts create mode 100644 lazer/cardano/guards/source/packages/cardano/src/policy-vault.ts create mode 100644 lazer/cardano/guards/source/packages/cardano/src/types.ts create mode 100644 lazer/cardano/guards/source/packages/cardano/tests/policy-vault.test.ts create mode 100644 lazer/cardano/guards/source/packages/core/package.json create mode 100644 lazer/cardano/guards/source/packages/core/src/capabilities.ts create mode 100644 lazer/cardano/guards/source/packages/core/src/engine.ts create mode 100644 lazer/cardano/guards/source/packages/core/src/fixtures.ts create mode 100644 lazer/cardano/guards/source/packages/core/src/hash.ts create mode 100644 lazer/cardano/guards/source/packages/core/src/index.ts create mode 100644 lazer/cardano/guards/source/packages/core/src/math.ts create mode 100644 lazer/cardano/guards/source/packages/core/src/types.ts create mode 100644 lazer/cardano/guards/source/packages/core/tests/engine.test.ts create mode 100644 lazer/cardano/guards/source/packages/core/tests/math.test.ts create mode 100644 lazer/cardano/guards/source/packages/evm/package.json create mode 100644 lazer/cardano/guards/source/packages/evm/src/fixtures.ts create mode 100644 lazer/cardano/guards/source/packages/evm/src/index.ts create mode 100644 lazer/cardano/guards/source/packages/evm/tests/index.test.ts create mode 100644 lazer/cardano/guards/source/packages/svm/package.json create mode 100644 lazer/cardano/guards/source/packages/svm/src/fixtures.ts create mode 100644 lazer/cardano/guards/source/packages/svm/src/index.ts create mode 100644 lazer/cardano/guards/source/packages/svm/tests/index.test.ts create mode 100644 lazer/cardano/guards/source/pnpm-lock.yaml create mode 100644 lazer/cardano/guards/source/pnpm-workspace.yaml create mode 100644 lazer/cardano/guards/source/tsconfig.json diff --git a/lazer/cardano/guards/source/.env.example b/lazer/cardano/guards/source/.env.example new file mode 100644 index 00000000..639afbfe --- /dev/null +++ b/lazer/cardano/guards/source/.env.example @@ -0,0 +1,42 @@ +APP_PUBLIC_NAME=guards.one +APP_INTERNAL_NAME=anaconda +PORT=4310 + +PYTH_API_KEY=replace_with_pyth_api_key +PYTH_PREPROD_POLICY_ID=d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6 +PYTH_API_BASE_URL=https://api.pyth.network +PYTH_METADATA_SERVICE_URL= +PYTH_PRICE_SERVICE_URL= +PYTH_STREAM_CHANNEL=fixed_rate@200ms +PYTH_PRIMARY_FEED_ID=pyth-ada-usd +PYTH_PRIMARY_SYMBOL_QUERY=ADA/USD +PYTH_PRIMARY_ASSET_TYPE=crypto +PYTH_PRIMARY_PRICE_FEED_ID= +CARDANO_PYTH_STATE_REFERENCE=replace_with_pyth_state_reference_utxo + +CARDANO_NETWORK=preprod +CARDANO_PROVIDER=blockfrost +CARDANO_PROVIDER_URL=https://cardano-preprod.blockfrost.io/api/v0 +CARDANO_BLOCKFROST_PROJECT_ID=replace_with_blockfrost_project_id +CARDANO_SWAP_PROVIDER=dexhunter +CARDANO_PROTOCOL_FEE_BPS=30 +CARDANO_MAX_TOTAL_FEE_BPS=100 +CARDANO_PROTOCOL_FEE_MODE=explicit_output +CARDANO_EXECUTION_ROUTE_ID=cardano-minswap-ada-usdm +DEXHUNTER_BASE_URL=https://api-us.dexhunterv3.app +DEXHUNTER_PARTNER_ID=replace_with_dexhunter_partner_id +DEXHUNTER_PARTNER_FEE_PERCENT=0.3 +MINSWAP_AGGREGATOR_URL=https://agg-api.minswap.org/aggregator +MINSWAP_PARTNER_CODE=replace_with_minswap_partner_code +CARDANO_EXECUTION_HOT_WALLET_ADDRESS=replace_with_execution_hot_wallet +CARDANO_EXECUTION_HOT_WALLET_SKEY_PATH=./secrets/execution-hot.skey +CARDANO_GOVERNANCE_WALLET_ADDRESS=replace_with_governance_wallet +CARDANO_GOVERNANCE_SKEY_PATH=./secrets/governance.skey + +AUDIT_DB_PATH=./data/guards-one.sqlite + +# Image generation (Nano Banana 2 / Gemini) +GEMINI_API_KEY=replace_with_gemini_api_key + +# Replicate API +REPLICATE_API_TOKEN=replace_with_replicate_api_token diff --git a/lazer/cardano/guards/source/.github/copilot-instructions.md b/lazer/cardano/guards/source/.github/copilot-instructions.md new file mode 100644 index 00000000..0ca8ff8b --- /dev/null +++ b/lazer/cardano/guards/source/.github/copilot-instructions.md @@ -0,0 +1,12 @@ +# Copilot Review Instructions + +Review this repository as a treasury automation system where oracle correctness and bounded execution matter more than stylistic preferences. + +Focus on: +- risk-ladder invariants across `Normal -> Watch -> Partial DeRisk -> Full Stable Exit -> Frozen`, plus the `Auto Re-entry` recovery path +- oracle guardrails: stale data rejection, confidence thresholds, cooldown handling, and deterministic use of snapshots +- execution safety: approved routes only, bounded sell amounts, min-buy enforcement, reason hashes, and reproducible audit trails +- multichain discipline: shared core logic must remain chain-agnostic and adapters must not leak Cardano-specific assumptions back into `packages/core` +- tests: missing edge cases, regressions in partial de-risk sizing, full exit behavior, or re-entry hysteresis + +Prioritize bugs, broken invariants, security gaps, and missing tests. De-prioritize cosmetic refactors unless they affect correctness or maintainability. diff --git a/lazer/cardano/guards/source/.gitignore b/lazer/cardano/guards/source/.gitignore new file mode 100644 index 00000000..de18100f --- /dev/null +++ b/lazer/cardano/guards/source/.gitignore @@ -0,0 +1,9 @@ +node_modules/ +.DS_Store +.pnpm-store/ +*.log +.env +.env.* +!.env.example +secrets/ +*.sqlite diff --git a/lazer/cardano/guards/source/INTERNAL_NOTES.md b/lazer/cardano/guards/source/INTERNAL_NOTES.md new file mode 100644 index 00000000..9ea94ca0 --- /dev/null +++ b/lazer/cardano/guards/source/INTERNAL_NOTES.md @@ -0,0 +1,137 @@ +# Internal Notes + +> This document captures internal context, architectural decisions, and development history that are useful for the team but not relevant to the public-facing README. Keep it updated as the project evolves. + +--- + +## Origin + +Guards was originally built for the **Buenos Aires Pythathon** (Pyth Network hackathon). The challenge was to demonstrate meaningful use of Pyth oracle data beyond simple price feeds. We chose treasury risk management because it requires multiple oracle signals (price, EMA, confidence, freshness) and turns them into real execution decisions — not just charts. + +The public branding is `guards.one`. The internal package namespace is `anaconda` (the original project codename). + +## Why Treasury Risk Management + +We evaluated several directions before settling on treasury policy enforcement: + +- **Price alerts** — too simple, no execution surface +- **DEX aggregation** — competitive space, oracle usage is shallow +- **Lending protocol** — requires deep on-chain infra, too much scope for a hackathon +- **Treasury autopilot** — needs multiple oracle signals, has a clear execution model, and solves a real problem for DAOs + +The insight was that DAOs define risk in percentages but fail when absolute fiat floors are breached. Most treasury tooling is passive. Guards makes it active. + +## Key Architectural Decisions + +### Chain-agnostic core from day one + +Even though Cardano was the only live target, we built `packages/core` with zero chain imports. This was intentional: if the core engine depends on Cardano types, adding Solana or EVM later requires a refactor. By isolating the business logic early, each chain adapter only needs to implement `TreasuryConnector`. + +**Trade-off:** More upfront abstraction work. Worth it for multichain credibility. + +### Split custody model + +We deliberately avoided the pattern where automated bots spend from the governance multisig. Instead, governance pre-approves a bounded execution bucket. The keeper can only swap within policy limits from the hot wallet. This is safer and more realistic for production DAOs. + +### Risk ladder over binary modes + +Early versions had a simple "safe/risky" toggle. We replaced it with a 6-stage risk ladder (Normal → Watch → Partial De-Risk → Full Exit → Frozen → Re-Entry) because: +- Gradual escalation reduces unnecessary trading +- Frozen state handles oracle quality degradation +- Re-entry with hysteresis prevents oscillation +- Each stage is explainable to auditors and governance + +### DexHunter as primary venue + +Chosen over Minswap because DexHunter offers: +- Partner fee infrastructure (revenue capture) +- Aggregated routing across Cardano DEXs +- API-first design suitable for automated execution + +Minswap is kept as fallback. The venue layer is abstracted so adding new venues is straightforward. + +### Static UI → Next.js migration plan + +The repository currently serves the operator demo through the static preview flow. A richer Next.js UI exists as a parallel feature branch and is intended to replace the static shell once it lands. + +Why that migration still matters: +- component-based architecture +- stricter type safety around the operator surface +- better developer experience for larger UI changes +- a cleaner path to a production-ready build pipeline + +Design direction: premium dark theme inspired by [squads.xyz](https://squads.xyz), with electric blue (#3b82f6) accents on deep black (#08090c). The goal is to feel like institutional-grade treasury tooling, not a hackathon project. + +## Development Workflow + +### Worktree-based feature branches + +We use `git worktree` for parallel feature development. Each feature gets its own directory: + +```bash +git worktree add ../anaconda- feature/ +``` + +This allows working on multiple features simultaneously without branch switching. + +### PR review flow + +- Every feature branch gets a PR against `main` +- Copilot review is requested on each PR +- Review comments are tracked in `NEXT_STEPS.md` + +### Monorepo scripts + +| Script | Purpose | +|--------|---------| +| `pnpm test` | Run vitest across all packages | +| `pnpm typecheck` | TypeScript strict mode check | +| `pnpm simulate` | End-to-end backend simulation | +| `pnpm export:ui-data` | Generate demo state JSON for UI | +| `pnpm preview` | Backend server + operator demo at :4310 | + +## PR History & Review Tracker + +| PR | Scope | Status | +|----|-------|--------| +| #1 | Documentation bootstrap | Merged | +| #2 | Core policy engine | Merged | +| #3 | SVM + EVM scaffolds | Merged | +| #4 | Cardano PolicyVault simulator | Merged | +| #5 | Backend orchestration + legacy UI | Merged | +| #7 | Cardano swap venue strategy | Merged | +| #8 | Protocol fee model | Merged | +| #9 | Premium UI overhaul (Next.js) | Open | +| #10 | Cardano custody model | Merged | +| #12 | DexHunter live adapter | Merged | + +Detailed review items and their resolution are tracked in [NEXT_STEPS.md](./NEXT_STEPS.md). + +## Tooling & AI-Assisted Development + +### Skills & MCP integrations + +| Tool | Purpose | Config | +|------|---------|--------| +| Nano Banana 2 | Logo/image generation via Gemini 3.1 Flash | `~/tools/nano-banana-2`, key in `~/.nano-banana/.env` | +| 21st.dev Magic | AI-powered UI component generation | MCP in `~/.claude.json` | +| Replicate | Additional AI model access | API token in `.env` | + +### Generated assets + +- `apps/ui/public/guards-logo.png` — Shield + "GUARDS" wordmark, white on transparent background. Used across navbar, sidebar, footer, and README. Favicon is configured separately as `/guards-icon.svg`. + +## Pending Work + +See [NEXT_STEPS.md](./NEXT_STEPS.md) for the full backlog. Key priorities: + +1. **Wire real Cardano wallet** into DexHunter execution path +2. **Port PolicyVault to Aiken** (on-chain validator) +3. **Integrate real Pyth signed updates** on preprod +4. **Surface fee breakdown** in operator UI +5. **Add CI** for tests and typecheck +6. **Deploy storage** — replace SQLite with production-grade persistence + +## Team & Contact + +This project is maintained by the SOLx-AR team. For questions or collaboration, reach out through the GitHub organization. diff --git a/lazer/cardano/guards/source/NEXT_STEPS.md b/lazer/cardano/guards/source/NEXT_STEPS.md new file mode 100644 index 00000000..2d6f9d7a --- /dev/null +++ b/lazer/cardano/guards/source/NEXT_STEPS.md @@ -0,0 +1,175 @@ +# NEXT STEPS + +This is the canonical tracker for the repository. Every new task, bug, review comment, or deployment prerequisite should be added here and checked off when done. + +## Rules +- Update this file in the same commit that changes the underlying work. +- Keep items concrete and testable. +- If something is blocked, note the blocker inline instead of deleting the task. + +## Current Review Pass + +### PR · Landing Custody / Footer +- [x] Clarify that the custody model supports native rails and external multisig integrations like Squads and Safe +- [x] Remove the public GitHub CTA from the landing page +- [x] Upgrade the footer so it acts as product navigation and runtime summary instead of a thin status strip +- [x] Remove the standalone custody/security section and absorb the important point into the main product flow +- [x] Merge the old How It Works and Risk Ladder explanations into one simpler section +- [x] Remove redundant landing explainer sections that were adding more detail than value + +### PR · Scaffold Sweep +- [x] Replace residual weak wording in the Aiken scaffold with explicit `scaffold` language +- [x] Port additional pure PolicyVault invariants from the TypeScript simulator into the Aiken helper module +- [x] Reword the pending `pyth-examples` tracker item so it refers to filling real team/contact fields + +### PR #14 · Apps Blockchain Surface +- [x] Make manifest paths stable regardless of the caller's current working directory +- [x] Upgrade the manifest tests from substring checks to real filesystem existence checks + +### PR #13 · README / Internal Notes +- [x] Align the public Quick Start with the scripts that actually exist in `main` +- [x] Remove the public README link to internal-only notes +- [x] Reword the Next.js section in `INTERNAL_NOTES.md` as a planned migration, not a merged reality +- [x] Remove non-existent root UI scripts from the internal workflow table +- [x] Keep internal asset references aligned with the committed `apps/ui/assets/*.jpeg` files + +### PR #12 · DexHunter Live Adapter +- [x] Add a DexHunter API client for estimate/build/sign with partner header auth +- [x] Add a live keeper path that executes from the hot wallet via injected signer/submission hooks +- [x] Record fee breakdown in live execution audit events +- [x] Use DexHunter build totals, not `maxSellAmount`, when recording sold amount and average price +- [x] Refuse live execution when the intent is expired or not yet valid +- [x] Remove unused `CARDANO_STABLE_TOKEN_ID` config and align docs with the actual per-call inputs + +### PR #11 · Cardano Aiken Scaffold +- [x] Replace bytearray stub calls in the validator entrypoint with fail-closed scaffold branches +- [x] Make unimplemented admin paths fail closed instead of returning `True` +- [x] Remove hard-coded Pyth policy id literals from the policy helper and compare against datum-carried config instead +- [x] Align scaffolded execution intent/result shapes more closely with the canonical TypeScript model +- [x] Replace absolute local README links with repo-relative links +- [x] Make the cooldown check reflect the intended invariant instead of a no-op timestamp comparison +- [x] Rename the stage-transition helper to document monotonic escalation instead of an identity mapping + +### PR #10 · Cardano Custody Model +- [x] Clarify that Tx 1 records the execution intent via current metadata/datum surfaces rather than promising an explicit on-chain intent artifact today + +### PR #8 · Protocol Fee Model +- [x] Nest the markdown lists correctly in the protocol-fee doc +- [x] Use explicit percent-point naming in the fee model to match the venue-config layer +- [x] Simplify fee amount rounding with numeric `toFixed` conversions + +### PR #7 · Cardano Venue Strategy +- [x] Normalize swap-provider config parsing so env values are whitespace/case tolerant +- [x] Bound venue/protocol fee inputs and reject non-finite values by clamping to safe ranges +- [x] Make fee conversion semantics explicit with both percent-points and rate helpers +- [x] Remove unrelated example secrets from `.env.example` + +### PR #9 · Premium UI Overhaul +- [x] Remove brittle `stageColor(...).split(" ")` usage and centralize `RiskStage` presentation helpers +- [x] Derive swap haircut display/math from `policy.haircutBps` +- [x] Guard simulation replay against empty frame sets and improve replay accessibility labels +- [x] Reformat `apps/ui/package.json` and add explicit UI typecheck coverage to the root pipeline +- [x] Normalize demo oracle timestamps to milliseconds and add UI-safe fallbacks for optional data +- [x] Keep the replay/demo deterministic for SSR by using fixed timestamps and a stable reference clock +- [x] Split UI ladder highlighting from raw `RiskStage` so `auto_reentry` can render distinctly from `normal` +- [x] Reuse core-derived domain types in the UI view model instead of duplicating oracle/policy primitives +- [x] Guard swap estimates and replay chart math against invalid oracle prices / zero balances +- [x] Correct UI copy that implied live Pyth integration on Cardano before the real witness path exists +- [x] Separate ladder step identity from `RiskStage` so `auto_reentry` does not alias `normal` +- [x] Point the root `preview` workflow at the Next.js app and keep the static preview server as `preview:legacy` +- [x] Declare the Node runtime floor required by Next 16 / Tailwind 4 and add `baseUrl` for UI path mapping +- [x] Align the `full_exit` label with the rest of the UI as `Full Stable Exit` +- [x] Run root UI scripts through `pnpm --dir apps/ui` so the Next.js binary resolves correctly +- [x] Give policy cards an actual border width so accent border colors render +- [x] Add an explicit `Frozen` ladder step so the UI can represent every `RiskStage` +- [x] Rebase `feature/premium-ui-overhaul` onto `main`, resolve review threads, and re-request Copilot review + +### PR #1 · Docs +- [x] Align README wording with the documentation-only state of the branch +- [x] Standardize risk-ladder terminology across README, functional spec, and Copilot instructions +- [x] Clarify `ema_price` vs `emaPrice` +- [x] Clarify the "two-transaction flow" wording in the frontend spec + +### PR #2 · Core +- [x] Value stable assets using oracle price when available so depegs affect portfolio math +- [x] Enforce route `chainId` and `live` status in route selection +- [x] Sort snapshot IDs deterministically before hashing/auditing +- [x] Remove root scripts that reference apps before those workspaces exist +- [x] Regenerate the lockfile/workspace state to match the branch contents +- [x] Add a depeg regression test for stable valuation + +### PR #3 · Multichain +- [x] Reuse core shared types instead of duplicating multichain primitives +- [x] Align SVM/EVM stage naming with the core risk ladder +- [x] Validate simulation inputs before computing outputs +- [x] Add invalid-pricing regression tests for SVM and EVM + +### PR #4 · Cardano +- [x] Replace fragile `try/catch` tests with assertions that fail when no error is thrown +- [x] Enforce `result.routeId === intent.routeId` +- [x] Derive tx validity from intent/policy instead of hardcoded constants +- [x] Reject authorization while an intent is already in flight +- [x] Wire `pythStateReference` into the tx envelope +- [x] Reuse `intent.reasonHash` in the tx metadata +- [x] Validate vault, chain, execution time window, and max sell bounds on completion +- [x] Make simulated `averagePrice` consistent with the trade math + +### PR #5 · Apps +- [x] Fix invalid CSS `min()` usage with `calc()` +- [x] Replace broken local docs links in the UI with a safe repo/demo strategy +- [x] Align fallback chip tokens with existing styles +- [x] Escape or avoid unsafe HTML injection in the UI renderer +- [x] Remove the unused backend connector field +- [x] Make audit event ordering deterministic +- [x] Harden preview path resolution against traversal and absolute-path bugs +- [x] Avoid crashing when `Host` is missing in the preview server + +### PR #6 · UI Demo +- [x] Make the watch frame deterministic by using a separate watch-only snapshot before partial de-risk +- [x] Validate ladder tone tokens before interpolating them into CSS class names + +### PR #15 · Aiken Scaffold Sweep +- [x] Tighten scaffold intent-shape validation so `max_sell_amount` and `min_buy_amount` must be strictly positive +- [x] Require `datum.current_intent_id == Some(intent.intent_id)` before scaffold completion accepts an execution result + +## Product / Stack Pending +- [x] Show a clear preprod-only / mainnet-unavailable warning as soon as the app opens +- [x] Document the real preprod vault bootstrap path and its current blockers +- [x] Add a browser-side vault bootstrap lab to the dashboard for custody, thresholds, and reference-target planning +- [x] Add a scenario lab that runs the real risk engine against interactive price and balance inputs +- [x] Add a runtime control panel that switches between mock replay and the preprod-oriented operator snapshot +- [x] Add mock wallet-session scaffolding for Cardano and SVM demo flows +- [x] Add a 7d / 15m historical replay that executes treasury strategies against deterministic mock price states +- [x] Move wallet connect into the dashboard topbar with real-provider detection plus mock fallback +- [x] Replace execution-side multichain readiness copy with active vault/company profile context +- [x] Bootstrap root `.env` and `.env.example` for Pyth/Cardano preprod and deploy settings +- [x] Add `apps/blockchain` as the team-facing collaboration surface for contracts and chain adapters +- [x] Evaluate Cardano aggregator options for integrator fee capture and choose a primary venue +- [x] Define a protocol fee model that can layer on top of venue partner fees with a hard total-fee cap +- [x] Document the Cardano custody model for multisig governance plus automated execution +- [x] Integrate `DexHunter` partner flow for automated swaps +- [ ] Keep `Minswap` as fallback execution path +- [ ] Wire real Cardano wallet/provider dependencies into the DexHunter hot-wallet execution path +- [ ] Surface venue fee + protocol fee breakdown in the operator UI and audit logs +- [ ] Add a target-reference-asset policy mode, e.g. keep ADA exposure equivalent to `XAU/USD` ounces via Pyth feeds +- [ ] Encode the hot-bucket custody rules in `PolicyVault` / `Aiken` +- [ ] Add real wallet adapters for Cardano (`CIP-30`) and SVM (`Wallet Standard`) instead of dashboard-side planning states +- [ ] Add a real frontend create-vault transaction flow on top of the preprod bootstrap builder +- [x] Move the Pyth/Cardano live collector source of truth into `apps/blockchain/cardano/offchain` +- [x] Move the Aiken scaffold source of truth into `apps/blockchain/cardano/contracts/aiken` +- [x] Encode the hot-bucket notional cap helpers in the `Aiken` `PolicyVault` scaffold +- [x] Rebuild the operator UI with a Squads-inspired treasury shell and dark dashboard layout +- [x] Add deterministic demo frames and replay controls for the breach -> de-risk -> exit -> recovery scenario +- [x] Refresh the local preview flow so the UI can be shown as a working product demo +- [ ] Port `PolicyVault` to `Aiken` +- [x] Integrate real Pyth signed updates and preprod witness flow +- [ ] Resolve the Pyth State UTxO automatically from a live Cardano provider instead of `.env` +- [ ] Integrate a live Cardano swap venue in the keeper execution path +- [ ] Replace SQLite demo persistence with deployable storage +- [ ] Add CI for tests and typecheck +- [ ] Capture final demo screenshots / recording for the hackathon pitch +- [x] Prepare the `pyth-examples` submission tree under `/lazer/cardano/my-project/` +- [x] Adapt the submission README to the hackathon template from `pyth-examples` +- [x] Open the submission PR from the fork back to `pyth-network/pyth-examples` +- [ ] Fill the final team/contact fields in the `pyth-examples` README and PR body before final submission +- [ ] Review and address feedback on the `pyth-examples` draft PR diff --git a/lazer/cardano/guards/source/README.md b/lazer/cardano/guards/source/README.md new file mode 100644 index 00000000..263cd352 --- /dev/null +++ b/lazer/cardano/guards/source/README.md @@ -0,0 +1,204 @@ +

+ Guards +

+ +

Oracle-aware treasury policy enforcement for multichain DAOs.

+ +

+ guards.one is a treasury control plane that ingests real-time oracle data, evaluates risk against configurable policy rules, and executes protective swaps automatically, auditably, and across chains. +

+ +--- + +## The Problem + +DAOs and on-chain treasuries usually define treasury mandates in percentages, but the real failure mode is breaching an absolute fiat-denominated floor. When markets move fast, manual multisig coordination is too slow to protect value. + +Most treasury tooling either tracks prices passively or requires human intervention at every step. Neither approach is enough when the goal is to defend a hard protection floor under volatile conditions. + +## How Guards Solves It + +Guards turns treasury risk rules into executable actions: + +1. Ingest oracle snapshots from Pyth +2. Evaluate treasury health using drawdown, liquid value, confidence, and freshness +3. Authorize a bounded execution intent with oracle evidence +4. Execute an approved route from a capped hot wallet +5. Anchor an audit trail with intent, result, and oracle context + +## Implemented MVP Scope + +The current repository already includes: +- a shared core policy engine with liquid-value based triggers using `price`, `emaPrice`, `confidence`, freshness, and fiat floors +- a Cardano control-plane simulator plus an `Aiken` scaffold for the on-chain port +- backend services for collector, keeper, risk engine, audit logging, and end-to-end simulation +- a Squads-inspired operator UI shell for the treasury demo +- dashboard-side runtime controls for `mock replay` vs `preprod snapshot` +- mock wallet-session scaffolding for Cardano and SVM demo flows +- a 7-day / 15-minute historical replay that runs treasury strategies and shows simulated executions +- `SVM` and `EVM` scaffolding so the business logic remains multichain-native from day one + +## Risk Ladder + +| Stage | Trigger | Action | +|-------|---------|--------| +| **Normal** | Drawdown < watch threshold | Hold allocation, monitor feeds | +| **Watch** | Drawdown > `watchDrawdownBps` | Increase monitoring, prepare routes | +| **Partial De-Risk** | Drawdown > `partialDrawdownBps` | Swap part of the risk asset into the approved stable | +| **Full Stable Exit** | Drawdown > `fullExitDrawdownBps` or floor breached | Exit the remaining risk bucket into stable | +| **Frozen** | Oracle stale or confidence too wide | Block execution until data quality recovers | +| **Auto Re-Entry** | Recovery > `reentryDrawdownBps` + cooldown elapsed | Restore risk allocation gradually | + +The engine measures both percentage drawdown and absolute protected value: + +```text +liquid_value = amount × pyth_price × (1 − haircut_bps / 10000) +``` + +That means the system reacts not only to “asset dropped X%”, but also to “the treasury fell below the fiat floor we promised to defend”. + +## Architecture + +```text +packages/core + PolicyConfig · OracleSnapshot · RiskStage · ExecutionIntent · ExecutionResult + | + +-- packages/cardano PolicyVault simulator + DexHunter live adapter + +-- packages/svm scaffold connector + +-- packages/evm scaffold connector + | +apps/blockchain/cardano/contracts + Aiken scaffold for PolicyVault + hot-bucket rules + | +apps/backend + collector · risk-engine · keeper · storage · preview-server + | +apps/blockchain/cardano/offchain + live Pyth signed-update collector · witness builder · Cardano off-chain wiring + | +apps/ui + operator dashboard · replay demo · treasury shell +``` + +## Why Pyth Is Required + +Guards treats Pyth as a core system dependency, not a cosmetic data source. + +| Signal | How Guards Uses It | +|--------|-------------------| +| `price` | Spot valuation for liquid value calculations | +| `emaPrice` | Baseline for drawdown measurement | +| `confidence` | Confidence widening can freeze execution | +| `freshness` | Stale data blocks execution | +| snapshot IDs | Carried into the audit trail for verification | + +## Execution Model + +### Custody + +Guards uses a split custody model: +- governance treasury stays under multisig control +- execution bucket holds a bounded balance under a pre-approved policy +- automated swaps never spend directly from the governance multisig + +### Venue Strategy + +| Priority | Venue | Purpose | +|----------|-------|---------| +| Primary | DexHunter | Aggregated routing plus partner fee infrastructure | +| Fallback | Minswap Aggregator | Fallback execution path | + +Revenue is modeled in two layers: venue partner fee and protocol fee, both governance-capped and accounted separately from slippage. + +## Demo Flow + +The current simulation and UI replay cover: +- `watch` +- `partial_derisk` +- `full_exit` +- `auto re-entry` + +The frontend exposes that run as a deterministic operator demo with: +- a treasury workspace shell +- account tables for the risk bucket and stable reserve +- a replayable timeline for breach -> de-risk -> exit -> recovery +- charted backend series +- audit cards rendered from backend events + +## Team Collaboration Surface + +Chain-facing implementation work should start in [apps/blockchain](./apps/blockchain/README.md). + +That surface gives the team one obvious place to iterate on: +- Cardano contracts and off-chain execution wiring +- SVM connector work +- EVM connector work + +Reusable adapters and shared business logic still live in `packages/*`, but `apps/blockchain` is the collaboration anchor for new on-chain and connector work. + +## Quick Start + +```bash +pnpm install +pnpm typecheck +pnpm test +pnpm simulate +pnpm pyth:fetch-live +pnpm preview +``` + +- Next.js preview (`pnpm preview`): `http://localhost:3000` +- Legacy static preview (`pnpm preview:legacy`): `http://localhost:4310` +- Runtime baseline: `Node >= 24.0.0` + +## Environment + +Copy `.env.example` to `.env` and fill the required values: + +```bash +cp .env.example .env +``` + +Key variables: + +| Variable | Purpose | +|----------|---------| +| `PYTH_API_KEY` | Pyth oracle API access | +| `PYTH_PREPROD_POLICY_ID` | Pyth Cardano preprod policy binding | +| `PYTH_PRIMARY_SYMBOL_QUERY` | Symbol lookup used to resolve the live Pyth Lazer feed id | +| `CARDANO_BLOCKFROST_PROJECT_ID` | Cardano preprod provider | +| `DEXHUNTER_PARTNER_ID` | DexHunter partner fee capture | + +See [.env.example](./.env.example) for the full list. + +If you only need the Cardano simulator/types, import `@anaconda/blockchain/cardano`. +If you need the live Pyth/off-chain path, import `@anaconda/blockchain/cardano/offchain`. + +## Multichain Status + +| Chain | Status | Oracle | Execution | +|-------|--------|--------|-----------| +| **Cardano** | MVP in progress | Pyth | DexHunter / Minswap path in progress | +| **Solana (SVM)** | Scaffold only | Pyth-native target | Connector scaffold | +| **Ethereum (EVM)** | Scaffold only | Pyth EVM target | Connector scaffold | + +The core policy engine is shared across all chains. Execution remains local to each connected treasury. + +## Documentation + +| Document | Description | +|----------|-------------| +| [Functional Spec](./docs/functional-v4.md) | Product specification and risk model | +| [Roadmap](./docs/roadmap.md) | Delivery plan | +| [Cardano Swap Venue](./docs/cardano-swap-venue-decision.md) | Venue selection rationale | +| [Protocol Fee Model](./docs/protocol-fee-model.md) | Revenue and fee architecture | +| [Custody Model](./docs/cardano-custody-model.md) | Split-custody design | +| [DexHunter Adapter](./docs/dexhunter-live-adapter.md) | Live swap integration | +| [Pyth Live Collector](./docs/pyth-live-collector.md) | Signed-update fetch and Cardano witness wiring | +| [Frontend Spec](./docs/landing-frontend-spec.md) | UI and UX direction | +| [Blockchain App Surface](./apps/blockchain/README.md) | Team-facing contracts and connector workspace | +| [Execution Tracker](./NEXT_STEPS.md) | Current engineering backlog | + +## License + +All rights reserved. Contact the team for licensing inquiries. diff --git a/lazer/cardano/guards/source/apps/backend/package.json b/lazer/cardano/guards/source/apps/backend/package.json new file mode 100644 index 00000000..ce3b7a4a --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/package.json @@ -0,0 +1,11 @@ +{ + "name": "@anaconda/backend", + "version": "0.0.0", + "private": true, + "type": "module", + "dependencies": { + "@anaconda/cardano": "workspace:*", + "@anaconda/core": "workspace:*", + "dotenv": "^17.2.3" + } +} diff --git a/lazer/cardano/guards/source/apps/backend/src/collector.ts b/lazer/cardano/guards/source/apps/backend/src/collector.ts new file mode 100644 index 00000000..adec527a --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/collector.ts @@ -0,0 +1 @@ +export * from "../../blockchain/cardano/offchain/collector.js"; diff --git a/lazer/cardano/guards/source/apps/backend/src/dashboard.ts b/lazer/cardano/guards/source/apps/backend/src/dashboard.ts new file mode 100644 index 00000000..4cbfadff --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/dashboard.ts @@ -0,0 +1,399 @@ +import { + baseCapabilities, + computePositionLiquidValue, + evaluateRiskLadder, + type OracleSnapshot, + type PolicyConfig, + type RouteSpec, + type TreasuryState, +} from "@anaconda/core"; +import type { TickResult } from "./keeper.js"; +import type { AuditEvent } from "./storage.js"; + +export interface DashboardSeriesPoint { + label: string; + stage: TreasuryState["stage"]; + valueFiat: number; +} + +export interface DashboardDemoFrame { + label: string; + title: string; + copy: string; + stage: TreasuryState["stage"]; + valueFiat: number; + stableRatio: number; + reason: string; +} + +export interface DashboardPayload { + generatedAtUs: number; + source: string; + workspace: { + name: string; + label: string; + chain: string; + stage: string; + threshold: string; + members: string; + hotWallet: string; + governanceWallet: string; + totalBalance: string; + primaryAsset: string; + stableAsset: string; + vaultId: string; + }; + topbarChips: Array<{ + label: string; + value: string; + tone: string; + }>; + heroMetrics: Array<{ + label: string; + value: string; + copy: string; + chip?: string; + }>; + dashboardCards: Array<{ + label: string; + value: string; + copy: string; + chip: string; + }>; + chainCards: Array<{ + chain: string; + title: string; + copy: string; + chip: string; + }>; + riskLadder: Array<{ + stage: string; + title: string; + copy: string; + tone?: string; + }>; + executionTimeline: Array<{ + title: string; + copy: string; + status: string; + }>; + auditTrail: Array<{ + title: string; + copy: string; + stamp: string; + }>; + accounts: Array<{ + label: string; + address: string; + balance: string; + fiatValue: string; + weight: string; + role: string; + bucket: string; + }>; + portfolioSeries: Array<{ + label: string; + stage: string; + value: number; + displayValue: string; + }>; + demoFrames: Array<{ + label: string; + title: string; + copy: string; + stage: string; + balance: string; + stableRatio: string; + reason: string; + }>; +} + +function formatUsd(value: number): string { + return new Intl.NumberFormat("en-US", { + style: "currency", + currency: "USD", + maximumFractionDigits: value >= 1000 ? 0 : 2, + }).format(value); +} + +function formatPercent(value: number): string { + return `${(value * 100).toFixed(1)}%`; +} + +function formatAgeUs(nowUs: number, thenUs: number): string { + const seconds = Math.max(0, Math.round((nowUs - thenUs) / 1_000_000)); + return `${seconds}s`; +} + +function formatAmount(value: number): string { + return new Intl.NumberFormat("en-US", { + maximumFractionDigits: value >= 1000 ? 0 : 2, + }).format(value); +} + +function shortAddress(value: string): string { + if (value.length <= 14) { + return value; + } + + return `${value.slice(0, 8)}...${value.slice(-6)}`; +} + +function stageLabel(stage: TreasuryState["stage"]): string { + switch (stage) { + case "normal": + return "Normal"; + case "watch": + return "Watch"; + case "partial_derisk": + return "Partial De-Risk"; + case "full_exit": + return "Full Stable Exit"; + case "frozen": + return "Frozen"; + } +} + +function stageChip(stage: TreasuryState["stage"]): string { + switch (stage) { + case "normal": + return "ok"; + case "watch": + return "warn"; + case "partial_derisk": + return "warn"; + case "full_exit": + return "danger"; + case "frozen": + return "danger"; + } +} + +function formatAuditCopy(event: AuditEvent): string { + switch (event.category) { + case "snapshot": { + const assetId = typeof event.payload.assetId === "string" ? event.payload.assetId : "asset"; + const snapshotId = + typeof event.payload.snapshotId === "string" ? event.payload.snapshotId : "unknown snapshot"; + return `Observed ${assetId.toUpperCase()} from ${snapshotId}.`; + } + case "intent": { + const kind = typeof event.payload.kind === "string" ? event.payload.kind.replaceAll("_", " ") : "intent"; + const stage = typeof event.payload.stage === "string" ? event.payload.stage.replaceAll("_", " ") : "n/a"; + return `Authorized ${kind} at stage ${stage}.`; + } + case "execution": { + const sold = typeof event.payload.soldAmount === "number" ? formatAmount(event.payload.soldAmount) : "n/a"; + const bought = typeof event.payload.boughtAmount === "number" ? formatAmount(event.payload.boughtAmount) : "n/a"; + const stage = typeof event.payload.stage === "string" ? event.payload.stage.replaceAll("_", " ") : "n/a"; + return `Settled ${stage}. Sold ${sold} and bought ${bought}.`; + } + case "rejection": { + const code = typeof event.payload.code === "string" ? event.payload.code : "unknown"; + const keeperId = typeof event.payload.keeperId === "string" ? event.payload.keeperId : "keeper"; + return `Rejected with ${code} for ${keeperId}.`; + } + } +} + +export function buildDashboardPayload(input: { + treasury: TreasuryState; + policy: PolicyConfig; + routes: RouteSpec[]; + snapshots: Record; + operations: TickResult[]; + events: AuditEvent[]; + nowUs: number; + portfolioSeries: DashboardSeriesPoint[]; + demoFrames: DashboardDemoFrame[]; +}): DashboardPayload { + const assessment = evaluateRiskLadder( + input.treasury, + input.policy, + input.snapshots, + input.routes, + input.nowUs, + ); + const latestSnapshot = input.snapshots[input.policy.primaryAssetId]; + const route = input.routes.find((candidate) => + input.policy.approvedRouteIds.includes(candidate.routeId), + ); + const topReason = assessment.reasons[0]?.message ?? "Policy is currently inside the safe band."; + const liveChains = Object.values(baseCapabilities); + const riskPosition = input.treasury.positions.find((position) => position.role === "risk"); + const stablePosition = input.treasury.positions.find((position) => position.role === "stable"); + const accounts = input.treasury.positions.map((position) => { + const snapshot = input.snapshots[position.assetId]; + const liquidValue = computePositionLiquidValue(position, snapshot, input.policy); + const weight = + assessment.metrics.totalLiquidValueFiat === 0 + ? 0 + : liquidValue / assessment.metrics.totalLiquidValueFiat; + const wallet = + position.bucket === "hot" ? input.treasury.executionHotWallet : input.treasury.governanceWallet; + + return { + label: `${position.symbol} ${position.role === "risk" ? "risk bucket" : "stable reserve"}`, + address: shortAddress(wallet), + balance: `${formatAmount(position.amount)} ${position.symbol}`, + fiatValue: formatUsd(liquidValue), + weight: formatPercent(weight), + role: position.role === "risk" ? "Risk asset" : "Stable reserve", + bucket: position.bucket === "hot" ? "Execution hot" : "Governance cold", + }; + }); + + return { + generatedAtUs: input.nowUs, + source: "backend-demo", + workspace: { + name: input.treasury.name, + label: "guards.one live desk", + chain: "Cardano preprod", + stage: stageLabel(input.treasury.stage), + threshold: `${input.treasury.governanceSigners.length} governance signers`, + members: `${input.treasury.riskManagers.length} risk manager · ${input.treasury.keepers.length} keepers`, + hotWallet: shortAddress(input.treasury.executionHotWallet), + governanceWallet: shortAddress(input.treasury.governanceWallet), + totalBalance: formatUsd(assessment.metrics.totalLiquidValueFiat), + primaryAsset: riskPosition?.symbol ?? input.policy.primaryAssetId.toUpperCase(), + stableAsset: stablePosition?.symbol ?? input.policy.stableAssetId.toUpperCase(), + vaultId: input.treasury.vaultId, + }, + topbarChips: [ + { + label: "Network status", + value: "Cardano preprod live", + tone: "live", + }, + { + label: "Approved route", + value: route ? `${route.venue} · ${route.toAssetId.toUpperCase()}` : "No route", + tone: route?.live ? "neutral" : "warn", + }, + { + label: "Execution wallet", + value: shortAddress(input.treasury.executionHotWallet), + tone: "neutral", + }, + ], + heroMetrics: [ + { + label: "Current stage", + value: stageLabel(input.treasury.stage), + copy: "Final policy state after the simulated breach and recovery run.", + chip: stageChip(input.treasury.stage), + }, + { + label: "Treasury liquid value", + value: formatUsd(assessment.metrics.totalLiquidValueFiat), + copy: "Valued from the latest Pyth price and haircut-aware liquid value math.", + }, + { + label: "Stable protection", + value: formatPercent(assessment.metrics.stableRatio), + copy: "Current treasury share parked in the approved stable reserve.", + }, + { + label: "Oracle freshness", + value: latestSnapshot + ? formatAgeUs(input.nowUs, latestSnapshot.feedUpdateTimestampUs) + : "missing", + copy: "Age of the primary feed update relative to the latest policy evaluation.", + }, + ], + dashboardCards: [ + { + label: "Protected floor", + value: formatUsd(input.policy.portfolioFloorFiat), + copy: "Minimum fiat-equivalent value the policy tries to keep defended at all times.", + chip: "ok", + }, + { + label: "Emergency floor", + value: formatUsd(input.policy.emergencyPortfolioFloorFiat), + copy: "Crossing this floor escalates the vault into a full stable exit or freeze path.", + chip: "danger", + }, + { + label: "Primary reason", + value: topReason, + copy: "Top reason emitted by the risk engine for the current snapshot set.", + chip: assessment.reasons.length > 0 ? "warn" : "ok", + }, + { + label: "Execution policy", + value: route + ? `${route.chainId.toUpperCase()} · ${route.venue} · ${route.toAssetId.toUpperCase()}` + : "No approved route", + copy: "Only allowlisted routes can spend from the execution hot wallet.", + chip: route?.live ? "ok" : "warn", + }, + ], + chainCards: liveChains.map((capability) => ({ + chain: capability.chainId.toUpperCase(), + title: capability.live ? "Live execution surface" : "Scaffolded adapter", + copy: capability.notes.join(" "), + chip: capability.live ? "live" : "scaffold", + })), + riskLadder: [ + { + stage: "Normal", + title: "Operate with full permissions", + copy: "Fresh oracle, healthy confidence, and no forced action path active.", + }, + { + stage: "Watch", + title: "Increase monitoring", + copy: "The drawdown or fiat floor approaches the first trigger band.", + tone: "partial", + }, + { + stage: "Partial De-Risk", + title: "Sell only what restores the safe floor", + copy: "The keeper sells a bounded slice of the risky bucket into the approved stable route.", + tone: "partial", + }, + { + stage: "Full Stable Exit", + title: "Move the vault fully defensive", + copy: "A deeper breach exits the risk bucket and keeps the hot wallet on one stable rail.", + tone: "full", + }, + { + stage: "Auto Re-entry", + title: "Re-risk only after hysteresis clears", + copy: "Recovery must clear a separate band and cooldown before exposure comes back.", + tone: "reentry", + }, + ], + executionTimeline: input.operations.map((operation, index) => ({ + title: `Step ${index + 1} · ${stageLabel(operation.stage)}`, + copy: operation.rejected + ? `Execution rejected with ${operation.rejected}.` + : `Intent ${operation.intentId ?? "n/a"} anchored and settled in tx ${operation.txHash ?? "n/a"}.`, + status: operation.rejected ? "rejected" : "executed", + })), + auditTrail: input.events.slice(-6).map((event) => ({ + title: `${event.category.toUpperCase()} · ${event.eventId}`, + copy: formatAuditCopy(event), + stamp: formatAgeUs(input.nowUs, event.createdAtUs), + })), + accounts, + portfolioSeries: input.portfolioSeries.map((point) => ({ + label: point.label, + stage: point.stage, + value: point.valueFiat, + displayValue: formatUsd(point.valueFiat), + })), + demoFrames: input.demoFrames.map((frame) => ({ + label: frame.label, + title: frame.title, + copy: frame.copy, + stage: frame.stage, + balance: formatUsd(frame.valueFiat), + stableRatio: formatPercent(frame.stableRatio), + reason: frame.reason, + })), + }; +} diff --git a/lazer/cardano/guards/source/apps/backend/src/demo-state.ts b/lazer/cardano/guards/source/apps/backend/src/demo-state.ts new file mode 100644 index 00000000..811afebd --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/demo-state.ts @@ -0,0 +1,290 @@ +import { + evaluateRiskLadder, + type TreasuryState, +} from "@anaconda/core"; +import type { TickResult } from "./keeper.js"; +import { + buildDashboardPayload, + type DashboardDemoFrame, + type DashboardPayload, + type DashboardSeriesPoint, +} from "./dashboard.js"; +import { PythCollector } from "./collector.js"; +import { buildDemoScenario, sampleWitness } from "./fixtures.js"; +import { CardanoKeeperService } from "./keeper.js"; +import { AuditStore } from "./storage.js"; +import { buildSnapshots } from "@anaconda/core"; + +export interface DemoState { + payload: DashboardPayload; + operations: { + partial: TickResult; + fullExit: TickResult; + reentry: TickResult; + }; + counts: ReturnType; + events: ReturnType; +} + +function publishAll( + collector: PythCollector, + snapshots: Record[string]>, +) { + for (const snapshot of Object.values(snapshots)) { + collector.publish(snapshot); + } +} + +function reasonForFrame(result: ReturnType, fallback: string): string { + return result.reasons[0]?.message ?? fallback; +} + +function point(label: string, stage: TreasuryState["stage"], valueFiat: number): DashboardSeriesPoint { + return { + label, + stage, + valueFiat, + }; +} + +function frame(input: { + label: string; + title: string; + copy: string; + stage: TreasuryState["stage"]; + valueFiat: number; + stableRatio: number; + reason: string; +}): DashboardDemoFrame { + return input; +} + +export function createDemoState(): DemoState { + const auditStore = new AuditStore(); + const collector = new PythCollector(auditStore); + const keeper = new CardanoKeeperService(sampleWitness.pythPolicyId, auditStore); + const scenario = buildDemoScenario(); + + publishAll( + collector, + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-watch", + price: 75, + emaPrice: 80, + feedUpdateTimestampUs: 180_000_000, + observedAtUs: 180_000_000, + }, + usdm: { + snapshotId: "snapshot-usdm-watch", + feedUpdateTimestampUs: 180_000_000, + observedAtUs: 180_000_000, + }, + }), + ); + let treasury = scenario.treasury; + + const baselineAssessment = evaluateRiskLadder( + treasury, + scenario.policy, + collector.current(), + scenario.routes, + 180_000_000, + ); + + publishAll( + collector, + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-partial", + price: 72, + emaPrice: 80, + feedUpdateTimestampUs: 200_000_000, + observedAtUs: 200_000_000, + }, + usdm: { + snapshotId: "snapshot-usdm-partial", + feedUpdateTimestampUs: 200_000_000, + observedAtUs: 200_000_000, + }, + }), + ); + + const partial = keeper.tick({ + treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: collector.current(), + nowUs: 200_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + treasury = partial.treasury; + + const partialAssessment = evaluateRiskLadder( + treasury, + scenario.policy, + collector.current(), + scenario.routes, + 200_000_000, + ); + + publishAll( + collector, + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-crash", + price: 39, + emaPrice: 80, + feedUpdateTimestampUs: 240_000_000, + observedAtUs: 240_000_000, + }, + usdm: { + snapshotId: "snapshot-usdm-2", + feedUpdateTimestampUs: 240_000_000, + observedAtUs: 240_000_000, + }, + }), + ); + + const fullExit = keeper.tick({ + treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: collector.current(), + nowUs: 260_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + treasury = fullExit.treasury; + + const fullExitAssessment = evaluateRiskLadder( + treasury, + scenario.policy, + collector.current(), + scenario.routes, + 260_000_000, + ); + + publishAll( + collector, + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-recovery", + price: 79, + emaPrice: 80, + confidence: 0.2, + feedUpdateTimestampUs: 340_000_000, + observedAtUs: 340_000_000, + }, + usdm: { + snapshotId: "snapshot-usdm-3", + feedUpdateTimestampUs: 340_000_000, + observedAtUs: 340_000_000, + }, + }), + ); + + const reentry = keeper.tick({ + treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: collector.current(), + nowUs: 360_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + + const reentryAssessment = evaluateRiskLadder( + reentry.treasury, + scenario.policy, + collector.current(), + scenario.routes, + 360_000_000, + ); + + const events = auditStore.listEvents(); + const portfolioSeries: DashboardSeriesPoint[] = [ + point("Watch", "watch", baselineAssessment.metrics.totalLiquidValueFiat), + point("Partial", partial.treasury.stage, partialAssessment.metrics.totalLiquidValueFiat), + point("Exit", fullExit.treasury.stage, fullExitAssessment.metrics.totalLiquidValueFiat), + point("Recovery", reentry.treasury.stage, reentryAssessment.metrics.totalLiquidValueFiat), + ]; + const demoFrames: DashboardDemoFrame[] = [ + frame({ + label: "01", + title: "Watchlist breach detected", + copy: + "ADA slips under its EMA while Pyth freshness and confidence remain healthy enough to authorize a policy action.", + stage: "watch", + valueFiat: baselineAssessment.metrics.totalLiquidValueFiat, + stableRatio: baselineAssessment.metrics.stableRatio, + reason: reasonForFrame( + baselineAssessment, + "Drawdown approaches the first policy band.", + ), + }), + frame({ + label: "02", + title: "Partial de-risk executes", + copy: + "The keeper emits an intent, swaps only the bounded amount needed, and restores the defended stable floor.", + stage: partial.treasury.stage, + valueFiat: partialAssessment.metrics.totalLiquidValueFiat, + stableRatio: partialAssessment.metrics.stableRatio, + reason: reasonForFrame( + partialAssessment, + "Partial stable target restored.", + ), + }), + frame({ + label: "03", + title: "Full stable exit after the second leg down", + copy: + "A deeper price break and thinner asset cushion push the vault into a full defensive configuration on the approved stable route.", + stage: fullExit.treasury.stage, + valueFiat: fullExitAssessment.metrics.totalLiquidValueFiat, + stableRatio: fullExitAssessment.metrics.stableRatio, + reason: reasonForFrame( + fullExitAssessment, + "Emergency floor forced the full stable path.", + ), + }), + frame({ + label: "04", + title: "Auto re-entry restores exposure", + copy: + "Recovery clears the hysteresis band, cooldown expires, and the treasury re-enters risk according to the configured target ratio.", + stage: reentry.treasury.stage, + valueFiat: reentryAssessment.metrics.totalLiquidValueFiat, + stableRatio: reentryAssessment.metrics.stableRatio, + reason: reasonForFrame( + reentryAssessment, + "Recovery cleared the re-entry guardrails.", + ), + }), + ]; + + const payload = buildDashboardPayload({ + treasury: reentry.treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: collector.current(), + operations: [partial, fullExit, reentry], + events, + nowUs: 360_000_000, + portfolioSeries, + demoFrames, + }); + + return { + payload, + operations: { + partial, + fullExit, + reentry, + }, + counts: auditStore.counts(), + events, + }; +} diff --git a/lazer/cardano/guards/source/apps/backend/src/dexhunter-keeper.ts b/lazer/cardano/guards/source/apps/backend/src/dexhunter-keeper.ts new file mode 100644 index 00000000..398e514b --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/dexhunter-keeper.ts @@ -0,0 +1,136 @@ +import { randomUUID } from "node:crypto"; +import type { PolicyConfig, RouteSpec, TreasuryState } from "@anaconda/core"; +import { + CardanoExecutionError, + PolicyVaultSimulator, + type CardanoPythWitness, +} from "@anaconda/cardano"; +import { AuditStore } from "./storage.js"; +import { + DexHunterLiveAdapter, + DexHunterLiveError, + type CardanoHotWallet, +} from "./dexhunter-live.js"; + +export interface LiveTickInput { + treasury: TreasuryState; + policy: PolicyConfig; + routes: RouteSpec[]; + snapshots: ReturnType; + nowUs: number; + keeperId: string; + witness: CardanoPythWitness; + wallet: CardanoHotWallet; + assetTokenIds: Record; + blacklistedDexes?: string[]; +} + +export interface LiveTickResult { + treasury: TreasuryState; + intentId?: string; + txHash?: string; + stage: TreasuryState["stage"]; + rejected?: string; +} + +export class CardanoDexHunterKeeperService { + private readonly simulator: PolicyVaultSimulator; + private readonly adapter: DexHunterLiveAdapter; + + constructor( + pythPolicyId: string, + private readonly auditStore: AuditStore, + adapter = new DexHunterLiveAdapter(), + ) { + this.simulator = new PolicyVaultSimulator(pythPolicyId); + this.adapter = adapter; + } + + async tick(input: LiveTickInput): Promise { + try { + const authorization = this.simulator.authorizeExecution({ + treasury: input.treasury, + policy: input.policy, + snapshots: input.snapshots, + routes: input.routes, + nowUs: input.nowUs, + keeperId: input.keeperId, + witness: input.witness, + }); + + this.auditStore.recordIntent(authorization.intent); + this.auditStore.recordEvent({ + eventId: `intent:${authorization.intent.intentId}`, + category: "intent", + payload: { + stage: authorization.intent.stage, + kind: authorization.intent.kind, + reasonHash: authorization.intent.reasonHash, + }, + createdAtUs: authorization.intent.createdAtUs, + }); + + const execution = await this.adapter.executeIntent({ + intent: authorization.intent, + routes: input.routes, + wallet: input.wallet, + assetTokenIds: input.assetTokenIds, + nowUs: input.nowUs + 1_000, + ...(input.blacklistedDexes + ? { blacklistedDexes: input.blacklistedDexes } + : {}), + }); + + const completion = this.simulator.completeExecution({ + treasury: authorization.treasury, + policy: input.policy, + intent: authorization.intent, + result: execution.result, + }); + + this.auditStore.recordExecution(execution.result); + this.auditStore.recordEvent({ + eventId: `execution:${execution.result.txHash}`, + category: "execution", + payload: { + intentId: execution.result.intentId, + txHash: execution.result.txHash, + soldAmount: execution.result.soldAmount, + boughtAmount: execution.result.boughtAmount, + stage: completion.treasury.stage, + venueFeeAmount: execution.revenueBreakdown.venueFeeAmount, + protocolFeeAmount: execution.revenueBreakdown.protocolFeeAmount, + totalFeeBps: execution.revenueBreakdown.totalFeeBps, + }, + createdAtUs: execution.result.executedAtUs, + }); + + return { + treasury: completion.treasury, + intentId: authorization.intent.intentId, + txHash: execution.result.txHash, + stage: completion.treasury.stage, + }; + } catch (error) { + const code = + error instanceof CardanoExecutionError || error instanceof DexHunterLiveError + ? error.code + : "UNKNOWN_ERROR"; + this.auditStore.recordEvent({ + eventId: `rejection:${randomUUID()}`, + category: "rejection", + payload: { + code, + keeperId: input.keeperId, + }, + createdAtUs: input.nowUs, + }); + + return { + treasury: input.treasury, + stage: input.treasury.stage, + rejected: code, + }; + } + } +} diff --git a/lazer/cardano/guards/source/apps/backend/src/dexhunter-live.ts b/lazer/cardano/guards/source/apps/backend/src/dexhunter-live.ts new file mode 100644 index 00000000..3b22210b --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/dexhunter-live.ts @@ -0,0 +1,300 @@ +import type { ExecutionIntent, ExecutionResult, RouteSpec } from "@anaconda/core"; +import { runtimeEnv } from "./env.js"; +import { + applyRevenueFees, + buildProtocolFeePolicy, + getConfiguredVenueFeePercent, + type RevenueBreakdown, +} from "./protocol-fee.js"; +import { + getCardanoSwapVenueConfig, + type CardanoSwapVenueConfig, +} from "./swap-venue.js"; + +export interface CardanoHotWallet { + address: string; + signTx(cbor: string, partialSign?: boolean): Promise; + submitTx(cbor: string): Promise; +} + +export interface DexHunterEstimatePayload { + token_in: string; + token_out: string; + amount_in: number; + slippage: number; + blacklisted_dexes: string[]; +} + +export interface DexHunterEstimateResponse { + total_output: number; + total_output_without_slippage: number; + possible_routes: Array<{ + dex: string; + amount_in: number; + expected_output: number; + }>; +} + +export interface DexHunterBuildPayload extends DexHunterEstimatePayload { + buyer_address: string; +} + +export interface DexHunterBuildResponse { + cbor: string; + total_input: number; + total_output: number; + splits: Array<{ + dex: string; + amount_in: number; + expected_output: number; + }>; +} + +export interface DexHunterSignResponse { + cbor: string; +} + +export interface DexHunterIntentExecutionParams { + intent: ExecutionIntent; + routes: RouteSpec[]; + wallet: CardanoHotWallet; + assetTokenIds: Record; + nowUs: number; + blacklistedDexes?: string[]; +} + +export interface DexHunterExecutionArtifacts { + quote: DexHunterEstimateResponse; + build: DexHunterBuildResponse; + signedCbor: string; + revenueBreakdown: RevenueBreakdown; +} + +export interface DexHunterExecutionOutcome extends DexHunterExecutionArtifacts { + result: ExecutionResult; +} + +export interface DexHunterLiveAdapterConfig { + venueConfig: CardanoSwapVenueConfig; + maxTotalFeeBps: number; + protocolFeeMode: string; +} + +export class DexHunterLiveError extends Error { + code: string; + + constructor(code: string, message: string) { + super(message); + this.code = code; + } +} + +export type FetchLike = typeof fetch; + +export class DexHunterApiClient { + constructor( + private readonly baseUrl: string, + private readonly partnerId: string, + private readonly fetchImpl: FetchLike = fetch, + ) {} + + private get headers() { + if (!this.partnerId) { + throw new DexHunterLiveError( + "DEXHUNTER_PARTNER_ID_MISSING", + "DexHunter requires an X-Partner-Id header for API requests", + ); + } + + return { + "Content-Type": "application/json", + "X-Partner-Id": this.partnerId, + }; + } + + private async postJson(path: string, body: unknown): Promise { + const response = await this.fetchImpl(`${this.baseUrl}${path}`, { + method: "POST", + headers: this.headers, + body: JSON.stringify(body), + }); + + if (!response.ok) { + throw new DexHunterLiveError( + "DEXHUNTER_HTTP_ERROR", + `DexHunter request failed (${response.status}) for ${path}`, + ); + } + + return (await response.json()) as T; + } + + estimateSwap(payload: DexHunterEstimatePayload) { + return this.postJson("/swap/estimate", payload); + } + + buildSwap(payload: DexHunterBuildPayload) { + return this.postJson("/swap/build", payload); + } + + signSwap(txCbor: string, signatures: string) { + return this.postJson("/swap/sign", { + txCbor, + signatures, + }); + } +} + +function resolveTokenId(assetId: string, assetTokenIds: Record): string { + if (assetId === "ada") { + return ""; + } + + const tokenId = assetTokenIds[assetId]; + if (!tokenId) { + throw new DexHunterLiveError( + "TOKEN_ID_MISSING", + `Asset ${assetId} is missing a DexHunter token identifier`, + ); + } + + return tokenId; +} + +function resolveRoute(intent: ExecutionIntent, routes: RouteSpec[]): RouteSpec { + const route = routes.find((candidate) => candidate.routeId === intent.routeId); + if (!route) { + throw new DexHunterLiveError( + "ROUTE_NOT_FOUND", + `Route ${intent.routeId} is not available for DexHunter execution`, + ); + } + + return route; +} + +function computeAveragePrice(soldAmount: number, boughtAmount: number): number { + if (soldAmount <= 0 || boughtAmount <= 0) { + return 0; + } + + return Number((boughtAmount / soldAmount).toFixed(8)); +} + +export class DexHunterLiveAdapter { + private readonly client: DexHunterApiClient; + private readonly config: DexHunterLiveAdapterConfig; + + constructor( + private readonly fetchImpl: FetchLike = fetch, + config: Partial = {}, + ) { + const venueConfig = config.venueConfig ?? getCardanoSwapVenueConfig(); + this.config = { + venueConfig, + maxTotalFeeBps: config.maxTotalFeeBps ?? runtimeEnv.cardanoMaxTotalFeeBps, + protocolFeeMode: config.protocolFeeMode ?? runtimeEnv.cardanoProtocolFeeMode, + }; + this.client = new DexHunterApiClient( + venueConfig.dexHunter.baseUrl, + venueConfig.dexHunter.partnerId, + this.fetchImpl, + ); + } + + buildIntentPayload(params: DexHunterIntentExecutionParams): DexHunterBuildPayload { + const route = resolveRoute(params.intent, params.routes); + + return { + buyer_address: params.wallet.address, + token_in: resolveTokenId(params.intent.sourceAssetId, params.assetTokenIds), + token_out: resolveTokenId(params.intent.destinationAssetId, params.assetTokenIds), + amount_in: params.intent.maxSellAmount, + slippage: Number((route.maxSlippageBps / 100).toFixed(2)), + blacklisted_dexes: params.blacklistedDexes ?? [], + }; + } + + estimateRevenue(totalOutput: number): RevenueBreakdown { + return applyRevenueFees( + totalOutput, + buildProtocolFeePolicy({ + provider: "dexhunter", + venueFeePercent: getConfiguredVenueFeePercent(this.config.venueConfig), + protocolFeeBps: this.config.venueConfig.protocolFeeBps, + maxTotalFeeBps: this.config.maxTotalFeeBps, + protocolFeeMode: this.config.protocolFeeMode, + }), + ); + } + + async executeIntent(params: DexHunterIntentExecutionParams): Promise { + if (this.config.venueConfig.provider !== "dexhunter") { + throw new DexHunterLiveError( + "DEXHUNTER_NOT_PRIMARY_PROVIDER", + "Current runtime configuration is not set to DexHunter", + ); + } + if (params.nowUs < params.intent.createdAtUs) { + throw new DexHunterLiveError( + "INTENT_NOT_YET_VALID", + `Execution intent ${params.intent.intentId} is not yet valid`, + ); + } + if (params.nowUs > params.intent.expiryUs) { + throw new DexHunterLiveError( + "INTENT_EXPIRED", + `Execution intent ${params.intent.intentId} has expired`, + ); + } + + const payload = this.buildIntentPayload(params); + const quote = await this.client.estimateSwap({ + token_in: payload.token_in, + token_out: payload.token_out, + amount_in: payload.amount_in, + slippage: payload.slippage, + blacklisted_dexes: payload.blacklisted_dexes, + }); + + if (quote.total_output < params.intent.minBuyAmount) { + throw new DexHunterLiveError( + "QUOTE_BELOW_MIN_BUY", + `DexHunter quote ${quote.total_output} is below min buy ${params.intent.minBuyAmount}`, + ); + } + + const build = await this.client.buildSwap(payload); + if (build.total_output < params.intent.minBuyAmount) { + throw new DexHunterLiveError( + "BUILD_BELOW_MIN_BUY", + `DexHunter build output ${build.total_output} is below min buy ${params.intent.minBuyAmount}`, + ); + } + + const signatures = await params.wallet.signTx(build.cbor, true); + const signed = await this.client.signSwap(build.cbor, signatures); + const txHash = await params.wallet.submitTx(signed.cbor); + const revenueBreakdown = this.estimateRevenue(build.total_output); + + return { + quote, + build, + signedCbor: signed.cbor, + revenueBreakdown, + result: { + intentId: params.intent.intentId, + vaultId: params.intent.vaultId, + chainId: "cardano", + sourceAssetId: params.intent.sourceAssetId, + destinationAssetId: params.intent.destinationAssetId, + soldAmount: build.total_input, + boughtAmount: build.total_output, + averagePrice: computeAveragePrice(build.total_input, build.total_output), + txHash, + executedAtUs: params.nowUs, + routeId: params.intent.routeId, + }, + }; + } +} diff --git a/lazer/cardano/guards/source/apps/backend/src/env.ts b/lazer/cardano/guards/source/apps/backend/src/env.ts new file mode 100644 index 00000000..7eaa25d3 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/env.ts @@ -0,0 +1 @@ +export { runtimeEnv } from "../../blockchain/cardano/offchain/env.js"; diff --git a/lazer/cardano/guards/source/apps/backend/src/export-ui-data.ts b/lazer/cardano/guards/source/apps/backend/src/export-ui-data.ts new file mode 100644 index 00000000..b66b3792 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/export-ui-data.ts @@ -0,0 +1,16 @@ +import { mkdir, writeFile } from "node:fs/promises"; +import path from "node:path"; +import { createDemoState } from "./demo-state.js"; +import "./env.js"; + +const outputDir = path.resolve(process.cwd(), "apps/ui/data"); +const outputFile = path.join(outputDir, "demo-state.json"); + +await mkdir(outputDir, { recursive: true }); +await writeFile( + outputFile, + `${JSON.stringify(createDemoState().payload, null, 2)}\n`, + "utf8", +); + +console.log(`Wrote ${outputFile}`); diff --git a/lazer/cardano/guards/source/apps/backend/src/fixtures.ts b/lazer/cardano/guards/source/apps/backend/src/fixtures.ts new file mode 100644 index 00000000..3f904d35 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/fixtures.ts @@ -0,0 +1,5 @@ +export { + buildDemoScenario, + buildPrimaryLiveFeedRequest, + sampleWitness, +} from "../../blockchain/cardano/offchain/fixtures.js"; diff --git a/lazer/cardano/guards/source/apps/backend/src/keeper.ts b/lazer/cardano/guards/source/apps/backend/src/keeper.ts new file mode 100644 index 00000000..7f741cf6 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/keeper.ts @@ -0,0 +1,135 @@ +import { randomUUID } from "node:crypto"; +import { + type ExecutionResult, + type PolicyConfig, + type RouteSpec, + type TreasuryState, +} from "@anaconda/core"; +import { + CardanoExecutionError, + PolicyVaultSimulator, + createCardanoConnector, + type CardanoPythWitness, +} from "@anaconda/cardano"; +import { AuditStore } from "./storage.js"; + +export interface TickInput { + treasury: TreasuryState; + policy: PolicyConfig; + routes: RouteSpec[]; + snapshots: ReturnType; + nowUs: number; + keeperId: string; + witness: CardanoPythWitness; +} + +export interface TickResult { + treasury: TreasuryState; + intentId?: string; + txHash?: string; + stage: TreasuryState["stage"]; + rejected?: string; +} + +export class CardanoKeeperService { + private readonly simulator: PolicyVaultSimulator; + + constructor( + pythPolicyId: string, + private readonly auditStore: AuditStore, + ) { + this.simulator = new PolicyVaultSimulator(pythPolicyId); + } + + tick(input: TickInput): TickResult { + const connector = createCardanoConnector(input.routes); + + try { + const authorization = this.simulator.authorizeExecution({ + treasury: input.treasury, + policy: input.policy, + snapshots: input.snapshots, + routes: input.routes, + nowUs: input.nowUs, + keeperId: input.keeperId, + witness: input.witness, + }); + + this.auditStore.recordIntent(authorization.intent); + this.auditStore.recordEvent({ + eventId: `intent:${authorization.intent.intentId}`, + category: "intent", + payload: { + stage: authorization.intent.stage, + kind: authorization.intent.kind, + reasonHash: authorization.intent.reasonHash, + }, + createdAtUs: authorization.intent.createdAtUs, + }); + + const simulated = connector.simulateRoute( + authorization.intent, + authorization.assessment.metrics.priceMap, + ); + const result: ExecutionResult = { + intentId: authorization.intent.intentId, + vaultId: authorization.intent.vaultId, + chainId: "cardano", + sourceAssetId: simulated.sourceAssetId, + destinationAssetId: simulated.destinationAssetId, + soldAmount: simulated.soldAmount, + boughtAmount: simulated.boughtAmount, + averagePrice: simulated.averagePrice, + txHash: `tx-${randomUUID()}`, + executedAtUs: input.nowUs + 1_000, + routeId: simulated.routeId, + }; + + const completion = this.simulator.completeExecution({ + treasury: authorization.treasury, + policy: input.policy, + intent: authorization.intent, + result, + }); + + this.auditStore.recordExecution(result); + this.auditStore.recordEvent({ + eventId: `execution:${result.txHash}`, + category: "execution", + payload: { + intentId: result.intentId, + txHash: result.txHash, + soldAmount: result.soldAmount, + boughtAmount: result.boughtAmount, + stage: completion.treasury.stage, + }, + createdAtUs: result.executedAtUs, + }); + + return { + treasury: completion.treasury, + intentId: authorization.intent.intentId, + txHash: result.txHash, + stage: completion.treasury.stage, + }; + } catch (error) { + const message = + error instanceof CardanoExecutionError ? error.code : "UNKNOWN_ERROR"; + this.auditStore.recordEvent({ + eventId: `rejection:${randomUUID()}`, + category: "rejection", + payload: { + code: message, + keeperId: input.keeperId, + }, + createdAtUs: input.nowUs, + }); + + return { + treasury: input.treasury, + stage: input.treasury.stage, + rejected: message, + }; + } + } +} diff --git a/lazer/cardano/guards/source/apps/backend/src/preview-server.ts b/lazer/cardano/guards/source/apps/backend/src/preview-server.ts new file mode 100644 index 00000000..4c588c80 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/preview-server.ts @@ -0,0 +1,88 @@ +import { createReadStream, existsSync } from "node:fs"; +import { stat } from "node:fs/promises"; +import { createServer } from "node:http"; +import path from "node:path"; +import { createDemoState } from "./demo-state.js"; +import { runtimeEnv } from "./env.js"; + +const port = runtimeEnv.port; +const uiRoot = path.resolve(process.cwd(), "apps/ui"); +const docsRoot = path.resolve(process.cwd(), "docs"); +const allowedDocs = new Set([ + "functional-v4.md", + "roadmap.md", + "landing-frontend-spec.md", + "cardano-swap-venue-decision.md", + "protocol-fee-model.md", + "cardano-custody-model.md", + "dexhunter-live-adapter.md", +]); + +const contentTypes: Record = { + ".html": "text/html; charset=utf-8", + ".css": "text/css; charset=utf-8", + ".js": "text/javascript; charset=utf-8", + ".json": "application/json; charset=utf-8", + ".md": "text/markdown; charset=utf-8", +}; + +function resolveUiPath(requestPath: string): string { + const safePath = requestPath === "/" ? "/index.html" : requestPath; + const relativePath = safePath.replace(/^\/+/, ""); + const resolvedPath = path.resolve(uiRoot, relativePath); + const relativeToRoot = path.relative(uiRoot, resolvedPath); + + if (relativeToRoot.startsWith("..") || path.isAbsolute(relativeToRoot)) { + return path.resolve(uiRoot, "index.html"); + } + + return resolvedPath; +} + +function resolveDocsPath(requestPath: string): string | undefined { + const fileName = requestPath.replace(/^\/docs\/+/, ""); + if (!allowedDocs.has(fileName)) { + return undefined; + } + + return path.resolve(docsRoot, fileName); +} + +const server = createServer(async (request, response) => { + const hostHeader = + typeof request.headers.host === "string" && request.headers.host.length > 0 + ? request.headers.host + : "localhost"; + const url = new URL(request.url ?? "/", `http://${hostHeader}`); + + if (url.pathname === "/api/demo-state") { + response.writeHead(200, { "content-type": "application/json; charset=utf-8" }); + response.end(JSON.stringify(createDemoState().payload, null, 2)); + return; + } + + const filePath = url.pathname.startsWith("/docs/") + ? resolveDocsPath(url.pathname) + : resolveUiPath(url.pathname); + if (!filePath || !existsSync(filePath)) { + response.writeHead(404, { "content-type": "text/plain; charset=utf-8" }); + response.end("Not found"); + return; + } + + const fileStats = await stat(filePath); + if (!fileStats.isFile()) { + response.writeHead(403, { "content-type": "text/plain; charset=utf-8" }); + response.end("Forbidden"); + return; + } + + response.writeHead(200, { + "content-type": contentTypes[path.extname(filePath)] ?? "application/octet-stream", + }); + createReadStream(filePath).pipe(response); +}); + +server.listen(port, () => { + console.log(`Preview server running at http://localhost:${port}`); +}); diff --git a/lazer/cardano/guards/source/apps/backend/src/protocol-fee.ts b/lazer/cardano/guards/source/apps/backend/src/protocol-fee.ts new file mode 100644 index 00000000..c1b14815 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/protocol-fee.ts @@ -0,0 +1,125 @@ +import type { CardanoSwapVenueConfig } from "./swap-venue.js"; +import { feeBpsToPercentPoints } from "./swap-venue.js"; + +export type ProtocolFeeMode = "none" | "explicit_output" | "post_swap_reconciliation"; + +export interface ProtocolFeePolicy { + provider: CardanoSwapVenueConfig["provider"]; + venueFeePercentPoints: number; + protocolFeeBps: number; + protocolFeePercentPoints: number; + maxTotalFeeBps: number; + maxTotalFeePercentPoints: number; + protocolFeeMode: ProtocolFeeMode; +} + +export interface RevenueBreakdown { + grossAmount: number; + venueFeeAmount: number; + protocolFeeAmount: number; + netAmount: number; + venueFeePercentPoints: number; + requestedProtocolFeePercentPoints: number; + effectiveProtocolFeePercentPoints: number; + totalFeePercentPoints: number; + totalFeeBps: number; + capped: boolean; + capExceededByVenueFee: boolean; + protocolFeeMode: ProtocolFeeMode; +} + +function clampPercent(value: number): number { + if (!Number.isFinite(value) || value < 0) { + return 0; + } + + return Number(value.toFixed(6)); +} + +export function resolveProtocolFeeMode(value: string): ProtocolFeeMode { + switch (value) { + case "none": + case "post_swap_reconciliation": + return value; + default: + return "explicit_output"; + } +} + +export function buildProtocolFeePolicy(input: { + provider: CardanoSwapVenueConfig["provider"]; + venueFeePercent: number; + protocolFeeBps: number; + maxTotalFeeBps: number; + protocolFeeMode: string; +}): ProtocolFeePolicy { + const protocolFeeBps = Math.max(0, Math.round(input.protocolFeeBps)); + const maxTotalFeeBps = Math.max(0, Math.round(input.maxTotalFeeBps)); + + return { + provider: input.provider, + venueFeePercentPoints: clampPercent(input.venueFeePercent), + protocolFeeBps, + protocolFeePercentPoints: feeBpsToPercentPoints(protocolFeeBps), + maxTotalFeeBps, + maxTotalFeePercentPoints: feeBpsToPercentPoints(maxTotalFeeBps), + protocolFeeMode: resolveProtocolFeeMode(input.protocolFeeMode), + }; +} + +export function getConfiguredVenueFeePercent(config: CardanoSwapVenueConfig): number { + if (config.provider === "dexhunter") { + return clampPercent(config.dexHunter.partnerFeePercentPoints); + } + + return 0; +} + +export function applyRevenueFees( + grossAmount: number, + policy: ProtocolFeePolicy, +): RevenueBreakdown { + const normalizedGrossAmount = Number.isFinite(grossAmount) && grossAmount > 0 ? grossAmount : 0; + const requestedProtocolFeePercentPoints = + policy.protocolFeeMode === "none" ? 0 : policy.protocolFeePercentPoints; + const totalRequestedFeePercentPoints = clampPercent( + policy.venueFeePercentPoints + requestedProtocolFeePercentPoints, + ); + const totalAllowedFeePercentPoints = policy.maxTotalFeePercentPoints; + const capExceededByVenueFee = policy.venueFeePercentPoints > totalAllowedFeePercentPoints; + const effectiveProtocolFeePercentPoints = clampPercent( + Math.max(0, totalAllowedFeePercentPoints - policy.venueFeePercentPoints), + ); + const capped = + totalRequestedFeePercentPoints > totalAllowedFeePercentPoints || capExceededByVenueFee; + const protocolFeePercentPoints = capped + ? effectiveProtocolFeePercentPoints + : requestedProtocolFeePercentPoints; + const totalFeePercentPoints = clampPercent( + policy.venueFeePercentPoints + protocolFeePercentPoints, + ); + const venueFeeAmount = Number( + ((normalizedGrossAmount * policy.venueFeePercentPoints) / 100).toFixed(8), + ); + const protocolFeeAmount = Number( + ((normalizedGrossAmount * protocolFeePercentPoints) / 100).toFixed(8), + ); + const netAmount = Number( + Math.max(0, normalizedGrossAmount - venueFeeAmount - protocolFeeAmount).toFixed(8), + ); + + return { + grossAmount: normalizedGrossAmount, + venueFeeAmount, + protocolFeeAmount, + netAmount, + venueFeePercentPoints: policy.venueFeePercentPoints, + requestedProtocolFeePercentPoints, + effectiveProtocolFeePercentPoints: protocolFeePercentPoints, + totalFeePercentPoints, + totalFeeBps: Math.round(totalFeePercentPoints * 100), + capped, + capExceededByVenueFee, + protocolFeeMode: policy.protocolFeeMode, + }; +} diff --git a/lazer/cardano/guards/source/apps/backend/src/risk-engine.ts b/lazer/cardano/guards/source/apps/backend/src/risk-engine.ts new file mode 100644 index 00000000..c014c069 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/risk-engine.ts @@ -0,0 +1,20 @@ +import { + evaluateRiskLadder, + type OracleSnapshot, + type PolicyConfig, + type RiskAssessment, + type RouteSpec, + type TreasuryState, +} from "@anaconda/core"; + +export class RiskEngine { + evaluate( + treasury: TreasuryState, + policy: PolicyConfig, + snapshots: Record, + routes: RouteSpec[], + nowUs: number, + ): RiskAssessment { + return evaluateRiskLadder(treasury, policy, snapshots, routes, nowUs); + } +} diff --git a/lazer/cardano/guards/source/apps/backend/src/simulate.ts b/lazer/cardano/guards/source/apps/backend/src/simulate.ts new file mode 100644 index 00000000..09aa04dd --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/simulate.ts @@ -0,0 +1,4 @@ +import "./env.js"; +import { createDemoState } from "./demo-state.js"; + +console.log(JSON.stringify(createDemoState(), null, 2)); diff --git a/lazer/cardano/guards/source/apps/backend/src/storage.ts b/lazer/cardano/guards/source/apps/backend/src/storage.ts new file mode 100644 index 00000000..19ce6c40 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/storage.ts @@ -0,0 +1,131 @@ +import { DatabaseSync } from "node:sqlite"; +import type { ExecutionIntent, ExecutionResult, OracleSnapshot } from "@anaconda/core"; + +export interface AuditEvent { + eventId: string; + category: "snapshot" | "intent" | "execution" | "rejection"; + payload: Record; + createdAtUs: number; +} + +export class AuditStore { + private readonly db: DatabaseSync; + + constructor(location = ":memory:") { + this.db = new DatabaseSync(location); + this.db.exec(` + CREATE TABLE IF NOT EXISTS snapshots ( + snapshot_id TEXT PRIMARY KEY, + asset_id TEXT NOT NULL, + payload TEXT NOT NULL, + observed_at_us INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS intents ( + intent_id TEXT PRIMARY KEY, + vault_id TEXT NOT NULL, + payload TEXT NOT NULL, + created_at_us INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS executions ( + tx_hash TEXT PRIMARY KEY, + intent_id TEXT NOT NULL, + payload TEXT NOT NULL, + executed_at_us INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS audit_events ( + event_id TEXT PRIMARY KEY, + category TEXT NOT NULL, + payload TEXT NOT NULL, + created_at_us INTEGER NOT NULL + ); + `); + } + + recordSnapshot(snapshot: OracleSnapshot) { + this.db + .prepare(` + INSERT OR REPLACE INTO snapshots (snapshot_id, asset_id, payload, observed_at_us) + VALUES (?, ?, ?, ?) + `) + .run( + snapshot.snapshotId, + snapshot.assetId, + JSON.stringify(snapshot), + snapshot.observedAtUs, + ); + } + + recordIntent(intent: ExecutionIntent) { + this.db + .prepare(` + INSERT OR REPLACE INTO intents (intent_id, vault_id, payload, created_at_us) + VALUES (?, ?, ?, ?) + `) + .run(intent.intentId, intent.vaultId, JSON.stringify(intent), intent.createdAtUs); + } + + recordExecution(result: ExecutionResult) { + this.db + .prepare(` + INSERT OR REPLACE INTO executions (tx_hash, intent_id, payload, executed_at_us) + VALUES (?, ?, ?, ?) + `) + .run(result.txHash, result.intentId, JSON.stringify(result), result.executedAtUs); + } + + recordEvent(event: AuditEvent) { + this.db + .prepare(` + INSERT OR REPLACE INTO audit_events (event_id, category, payload, created_at_us) + VALUES (?, ?, ?, ?) + `) + .run( + event.eventId, + event.category, + JSON.stringify(event.payload), + event.createdAtUs, + ); + } + + listEvents(): AuditEvent[] { + const rows = this.db + .prepare( + `SELECT event_id, category, payload, created_at_us FROM audit_events ORDER BY created_at_us, event_id`, + ) + .all() as Array<{ + event_id: string; + category: AuditEvent["category"]; + payload: string; + created_at_us: number; + }>; + + return rows.map((row) => ({ + eventId: row.event_id, + category: row.category, + payload: JSON.parse(row.payload) as Record, + createdAtUs: row.created_at_us, + })); + } + + counts() { + const [snapshots] = this.db + .prepare(`SELECT COUNT(*) AS count FROM snapshots`) + .all() as Array<{ count: number }>; + const [intents] = this.db + .prepare(`SELECT COUNT(*) AS count FROM intents`) + .all() as Array<{ count: number }>; + const [executions] = this.db + .prepare(`SELECT COUNT(*) AS count FROM executions`) + .all() as Array<{ count: number }>; + const [events] = this.db + .prepare(`SELECT COUNT(*) AS count FROM audit_events`) + .all() as Array<{ count: number }>; + + return { + snapshots: snapshots?.count ?? 0, + intents: intents?.count ?? 0, + executions: executions?.count ?? 0, + events: events?.count ?? 0, + }; + } +} diff --git a/lazer/cardano/guards/source/apps/backend/src/swap-venue.ts b/lazer/cardano/guards/source/apps/backend/src/swap-venue.ts new file mode 100644 index 00000000..782283be --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/src/swap-venue.ts @@ -0,0 +1,100 @@ +import { runtimeEnv } from "./env.js"; + +export type CardanoSwapProvider = "dexhunter" | "minswap"; + +const MAX_FEE_BPS = 10_000; +const MAX_PERCENT_POINTS = 100; + +export interface CardanoSwapVenueConfig { + provider: CardanoSwapProvider; + protocolFeeBps: number; + protocolFeePercentPoints: number; + protocolFeeRate: number; + dexHunter: { + baseUrl: string; + partnerId: string; + partnerFeePercentPoints: number; + partnerFeeRate: number; + requiresPartnerHeader: boolean; + }; + minswap: { + aggregatorUrl: string; + partnerCode: string; + supportsPartnerTracking: boolean; + }; +} + +export function resolveCardanoSwapProvider(value: string): CardanoSwapProvider { + const normalized = value.trim().toLowerCase(); + return normalized === "minswap" ? "minswap" : "dexhunter"; +} + +function clampBps(value: number): number { + if (!Number.isFinite(value)) { + return 0; + } + + return Math.min(MAX_FEE_BPS, Math.max(0, Math.round(value))); +} + +function clampPercentPoints(value: number): number { + if (!Number.isFinite(value)) { + return 0; + } + + return Math.min(MAX_PERCENT_POINTS, Math.max(0, Number(value.toFixed(4)))); +} + +export function feeBpsToPercentPoints(bps: number): number { + return clampBps(bps) / 100; +} + +export function feeBpsToRate(bps: number): number { + return clampBps(bps) / 10_000; +} + +export function buildCardanoSwapVenueConfig(input: { + provider: string; + protocolFeeBps: number; + dexHunterBaseUrl: string; + dexHunterPartnerId: string; + dexHunterPartnerFeePercent: number; + minswapAggregatorUrl: string; + minswapPartnerCode: string; +}): CardanoSwapVenueConfig { + const protocolFeeBps = clampBps(input.protocolFeeBps); + const dexHunterPartnerFeePercentPoints = clampPercentPoints( + input.dexHunterPartnerFeePercent, + ); + + return { + provider: resolveCardanoSwapProvider(input.provider), + protocolFeeBps, + protocolFeePercentPoints: feeBpsToPercentPoints(protocolFeeBps), + protocolFeeRate: feeBpsToRate(protocolFeeBps), + dexHunter: { + baseUrl: input.dexHunterBaseUrl, + partnerId: input.dexHunterPartnerId, + partnerFeePercentPoints: dexHunterPartnerFeePercentPoints, + partnerFeeRate: dexHunterPartnerFeePercentPoints / 100, + requiresPartnerHeader: input.dexHunterPartnerId.length > 0, + }, + minswap: { + aggregatorUrl: input.minswapAggregatorUrl, + partnerCode: input.minswapPartnerCode, + supportsPartnerTracking: input.minswapPartnerCode.length > 0, + }, + }; +} + +export function getCardanoSwapVenueConfig(): CardanoSwapVenueConfig { + return buildCardanoSwapVenueConfig({ + provider: runtimeEnv.cardanoSwapProvider, + protocolFeeBps: runtimeEnv.cardanoProtocolFeeBps, + dexHunterBaseUrl: runtimeEnv.dexHunterBaseUrl, + dexHunterPartnerId: runtimeEnv.dexHunterPartnerId, + dexHunterPartnerFeePercent: runtimeEnv.dexHunterPartnerFeePercent, + minswapAggregatorUrl: runtimeEnv.minswapAggregatorUrl, + minswapPartnerCode: runtimeEnv.minswapPartnerCode, + }); +} diff --git a/lazer/cardano/guards/source/apps/backend/tests/dashboard.test.ts b/lazer/cardano/guards/source/apps/backend/tests/dashboard.test.ts new file mode 100644 index 00000000..c1792d8d --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/tests/dashboard.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from "vitest"; +import { createDemoState } from "../src/demo-state.js"; + +describe("dashboard payload", () => { + it("builds a UI payload from the simulated backend state", () => { + const state = createDemoState(); + + expect(state.payload.source).toBe("backend-demo"); + expect(state.payload.workspace.name).toContain("Treasury"); + expect(state.payload.topbarChips).toHaveLength(3); + expect(state.payload.heroMetrics).toHaveLength(4); + expect(state.payload.dashboardCards).toHaveLength(4); + expect(state.payload.chainCards).toHaveLength(3); + expect(state.payload.accounts).toHaveLength(2); + expect(state.payload.portfolioSeries).toHaveLength(4); + expect(state.payload.demoFrames).toHaveLength(4); + expect(state.payload.executionTimeline).toHaveLength(3); + expect(state.payload.auditTrail.length).toBeGreaterThan(0); + expect(state.operations.reentry.stage).toBe("normal"); + }); +}); diff --git a/lazer/cardano/guards/source/apps/backend/tests/dexhunter-live.test.ts b/lazer/cardano/guards/source/apps/backend/tests/dexhunter-live.test.ts new file mode 100644 index 00000000..785dd15b --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/tests/dexhunter-live.test.ts @@ -0,0 +1,219 @@ +import { describe, expect, it, vi } from "vitest"; +import { PolicyVaultSimulator } from "@anaconda/cardano"; +import { buildDemoScenario, sampleWitness } from "../src/fixtures.js"; +import { + DexHunterLiveAdapter, + DexHunterLiveError, + type CardanoHotWallet, +} from "../src/dexhunter-live.js"; +import { CardanoDexHunterKeeperService } from "../src/dexhunter-keeper.js"; +import { AuditStore } from "../src/storage.js"; +import { buildCardanoSwapVenueConfig } from "../src/swap-venue.js"; + +function createMockWallet(): CardanoHotWallet { + return { + address: "addr_test_hot_1", + signTx: vi.fn(async () => "witness-set"), + submitTx: vi.fn(async () => "tx-live-123"), + }; +} + +function createFetchMock() { + return vi.fn(async (input: string | URL, init?: RequestInit) => { + const url = input.toString(); + const body = JSON.parse((init?.body as string | undefined) ?? "{}"); + + if (url.endsWith("/swap/estimate")) { + return new Response( + JSON.stringify({ + total_output: 5100, + total_output_without_slippage: 5150, + possible_routes: [{ dex: "MINSWAPV2", amount_in: body.amount_in, expected_output: 5100 }], + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + + if (url.endsWith("/swap/build")) { + return new Response( + JSON.stringify({ + cbor: "cbor-build-123", + total_input: body.amount_in, + total_output: 5100, + splits: [{ dex: "MINSWAPV2", amount_in: body.amount_in, expected_output: 5100 }], + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + + if (url.endsWith("/swap/sign")) { + return new Response( + JSON.stringify({ cbor: "cbor-signed-123" }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + } + + return new Response("not found", { status: 404 }); + }); +} + +function createAdapter(fetchMock: typeof fetch) { + return new DexHunterLiveAdapter(fetchMock, { + venueConfig: buildCardanoSwapVenueConfig({ + provider: "dexhunter", + protocolFeeBps: 30, + dexHunterBaseUrl: "https://api-us.dexhunterv3.app", + dexHunterPartnerId: "partner-live-test", + dexHunterPartnerFeePercent: 0.3, + minswapAggregatorUrl: "https://agg-api.minswap.org/aggregator", + minswapPartnerCode: "guards-one", + }), + maxTotalFeeBps: 100, + protocolFeeMode: "explicit_output", + }); +} + +describe("dexhunter live adapter", () => { + it("executes an authorized intent through estimate/build/sign/submit", async () => { + const scenario = buildDemoScenario(); + const authorization = new PolicyVaultSimulator(sampleWitness.pythPolicyId).authorizeExecution({ + treasury: scenario.treasury, + policy: scenario.policy, + snapshots: scenario.snapshots, + routes: scenario.routes, + nowUs: 200_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + const fetchMock = createFetchMock(); + const wallet = createMockWallet(); + const adapter = createAdapter(fetchMock as typeof fetch); + + const outcome = await adapter.executeIntent({ + intent: authorization.intent, + routes: scenario.routes, + wallet, + assetTokenIds: { usdm: "usdm-token-id" }, + nowUs: 200_001_000, + }); + + expect(outcome.result.txHash).toBe("tx-live-123"); + expect(outcome.result.soldAmount).toBe(authorization.intent.maxSellAmount); + expect(outcome.result.boughtAmount).toBe(5100); + expect(outcome.revenueBreakdown.totalFeeBps).toBeGreaterThanOrEqual(30); + expect(fetchMock).toHaveBeenCalledTimes(3); + }); + + it("rejects a quote below the minimum buy amount", async () => { + const scenario = buildDemoScenario(); + const authorization = new PolicyVaultSimulator(sampleWitness.pythPolicyId).authorizeExecution({ + treasury: scenario.treasury, + policy: scenario.policy, + snapshots: scenario.snapshots, + routes: scenario.routes, + nowUs: 200_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + const fetchMock = vi.fn(async () => + new Response( + JSON.stringify({ + total_output: 1, + total_output_without_slippage: 1, + possible_routes: [], + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ), + ); + const wallet = createMockWallet(); + const adapter = createAdapter(fetchMock as typeof fetch); + + await expect( + adapter.executeIntent({ + intent: authorization.intent, + routes: scenario.routes, + wallet, + assetTokenIds: { usdm: "usdm-token-id" }, + nowUs: 200_001_000, + }), + ).rejects.toMatchObject({ code: "QUOTE_BELOW_MIN_BUY" } satisfies Partial); + }); + + it("rejects execution when the intent has expired", async () => { + const scenario = buildDemoScenario(); + const authorization = new PolicyVaultSimulator(sampleWitness.pythPolicyId).authorizeExecution({ + treasury: scenario.treasury, + policy: scenario.policy, + snapshots: scenario.snapshots, + routes: scenario.routes, + nowUs: 200_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + const adapter = createAdapter(createFetchMock() as typeof fetch); + + await expect( + adapter.executeIntent({ + intent: authorization.intent, + routes: scenario.routes, + wallet: createMockWallet(), + assetTokenIds: { usdm: "usdm-token-id" }, + nowUs: authorization.intent.expiryUs + 1, + }), + ).rejects.toMatchObject({ code: "INTENT_EXPIRED" } satisfies Partial); + }); + + it("rejects execution before the intent becomes valid", async () => { + const scenario = buildDemoScenario(); + const authorization = new PolicyVaultSimulator(sampleWitness.pythPolicyId).authorizeExecution({ + treasury: scenario.treasury, + policy: scenario.policy, + snapshots: scenario.snapshots, + routes: scenario.routes, + nowUs: 200_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + const adapter = createAdapter(createFetchMock() as typeof fetch); + + await expect( + adapter.executeIntent({ + intent: authorization.intent, + routes: scenario.routes, + wallet: createMockWallet(), + assetTokenIds: { usdm: "usdm-token-id" }, + nowUs: authorization.intent.createdAtUs - 1, + }), + ).rejects.toMatchObject({ code: "INTENT_NOT_YET_VALID" } satisfies Partial); + }); + + it("records a live execution path in the keeper audit log", async () => { + const scenario = buildDemoScenario(); + const auditStore = new AuditStore(); + const fetchMock = createFetchMock(); + const wallet = createMockWallet(); + const keeper = new CardanoDexHunterKeeperService( + sampleWitness.pythPolicyId, + auditStore, + createAdapter(fetchMock as typeof fetch), + ); + + const result = await keeper.tick({ + treasury: scenario.treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: scenario.snapshots, + nowUs: 200_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + wallet, + assetTokenIds: { usdm: "usdm-token-id" }, + }); + + expect(result.txHash).toBe("tx-live-123"); + const executionEvent = auditStore + .listEvents() + .find((event) => event.category === "execution"); + expect(executionEvent?.payload.totalFeeBps).toBeDefined(); + }); +}); diff --git a/lazer/cardano/guards/source/apps/backend/tests/e2e.test.ts b/lazer/cardano/guards/source/apps/backend/tests/e2e.test.ts new file mode 100644 index 00000000..d911b220 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/tests/e2e.test.ts @@ -0,0 +1,83 @@ +import { describe, expect, it } from "vitest"; +import { buildSnapshots } from "@anaconda/core"; +import { PythCollector } from "../src/collector.js"; +import { createDemoState } from "../src/demo-state.js"; +import { buildDemoScenario, sampleWitness } from "../src/fixtures.js"; +import { CardanoKeeperService } from "../src/keeper.js"; +import { AuditStore } from "../src/storage.js"; + +describe("backend e2e flow", () => { + it("runs partial de-risk, full exit, reentry and records an audit trail", () => { + const state = createDemoState(); + const { partial, fullExit, reentry } = state.operations; + + expect(partial.stage).toBe("partial_derisk"); + expect(partial.txHash).toBeDefined(); + expect(fullExit.stage).toBe("full_exit"); + expect(fullExit.txHash).toBeDefined(); + expect(reentry.stage).toBe("normal"); + expect(reentry.txHash).toBeDefined(); + expect(state.counts.snapshots).toBeGreaterThanOrEqual(6); + expect(state.counts.intents).toBe(3); + expect(state.counts.executions).toBe(3); + expect(state.counts.events).toBeGreaterThanOrEqual(9); + }); + + it("records a rejection when oracle freshness is invalid", () => { + const scenario = buildDemoScenario(); + const auditStore = new AuditStore(); + const collector = new PythCollector(auditStore); + const keeper = new CardanoKeeperService(sampleWitness.pythPolicyId, auditStore); + + for (const snapshot of Object.values( + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-stale", + feedUpdateTimestampUs: 1_000_000, + observedAtUs: 1_000_000, + }, + }), + )) { + collector.publish(snapshot); + } + + const result = keeper.tick({ + treasury: scenario.treasury, + policy: scenario.policy, + routes: scenario.routes, + snapshots: collector.current(), + nowUs: 500_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + + expect(result.rejected).toBe("STALE_FEED"); + expect(auditStore.counts().events).toBeGreaterThanOrEqual(3); + }); + + it("records a rejection when no approved route exists for execution", () => { + const scenario = buildDemoScenario(); + const auditStore = new AuditStore(); + const collector = new PythCollector(auditStore); + const keeper = new CardanoKeeperService(sampleWitness.pythPolicyId, auditStore); + + for (const snapshot of Object.values(scenario.snapshots)) { + collector.publish(snapshot); + } + + const result = keeper.tick({ + treasury: scenario.treasury, + policy: { + ...scenario.policy, + approvedRouteIds: [], + }, + routes: scenario.routes, + snapshots: collector.current(), + nowUs: 200_000_000, + keeperId: "keeper-1", + witness: sampleWitness, + }); + + expect(result.rejected).toBe("NO_EXECUTABLE_INTENT"); + }); +}); diff --git a/lazer/cardano/guards/source/apps/backend/tests/protocol-fee.test.ts b/lazer/cardano/guards/source/apps/backend/tests/protocol-fee.test.ts new file mode 100644 index 00000000..9b703290 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/tests/protocol-fee.test.ts @@ -0,0 +1,106 @@ +import { describe, expect, it } from "vitest"; +import { + applyRevenueFees, + buildProtocolFeePolicy, + getConfiguredVenueFeePercent, + resolveProtocolFeeMode, +} from "../src/protocol-fee.js"; +import { buildCardanoSwapVenueConfig } from "../src/swap-venue.js"; + +describe("protocol fee model", () => { + it("defaults unknown modes to explicit output", () => { + expect(resolveProtocolFeeMode("something-else")).toBe("explicit_output"); + expect(resolveProtocolFeeMode("none")).toBe("none"); + expect(resolveProtocolFeeMode("post_swap_reconciliation")).toBe( + "post_swap_reconciliation", + ); + }); + + it("reads configured venue fee from dexhunter", () => { + const config = buildCardanoSwapVenueConfig({ + provider: "dexhunter", + protocolFeeBps: 30, + dexHunterBaseUrl: "https://api-us.dexhunterv3.app", + dexHunterPartnerId: "partner-123", + dexHunterPartnerFeePercent: 0.3, + minswapAggregatorUrl: "https://agg-api.minswap.org/aggregator", + minswapPartnerCode: "guards-one", + }); + + expect(getConfiguredVenueFeePercent(config)).toBe(0.3); + }); + + it("applies venue fee plus protocol fee when within cap", () => { + const breakdown = applyRevenueFees( + 1_000, + buildProtocolFeePolicy({ + provider: "dexhunter", + venueFeePercent: 0.3, + protocolFeeBps: 70, + maxTotalFeeBps: 100, + protocolFeeMode: "explicit_output", + }), + ); + + expect(breakdown.venueFeeAmount).toBe(3); + expect(breakdown.protocolFeeAmount).toBe(7); + expect(breakdown.netAmount).toBe(990); + expect(breakdown.totalFeeBps).toBe(100); + expect(breakdown.totalFeePercentPoints).toBe(1); + expect(breakdown.capped).toBe(false); + }); + + it("caps protocol fee when venue fee plus protocol fee exceed the max", () => { + const breakdown = applyRevenueFees( + 1_000, + buildProtocolFeePolicy({ + provider: "dexhunter", + venueFeePercent: 0.45, + protocolFeeBps: 80, + maxTotalFeeBps: 100, + protocolFeeMode: "explicit_output", + }), + ); + + expect(breakdown.requestedProtocolFeePercentPoints).toBe(0.8); + expect(breakdown.effectiveProtocolFeePercentPoints).toBe(0.55); + expect(breakdown.protocolFeeAmount).toBe(5.5); + expect(breakdown.totalFeeBps).toBe(100); + expect(breakdown.capped).toBe(true); + }); + + + it("flags when the venue fee alone exceeds the cap", () => { + const breakdown = applyRevenueFees( + 1_000, + buildProtocolFeePolicy({ + provider: "dexhunter", + venueFeePercent: 1.2, + protocolFeeBps: 20, + maxTotalFeeBps: 100, + protocolFeeMode: "explicit_output", + }), + ); + + expect(breakdown.capExceededByVenueFee).toBe(true); + expect(breakdown.effectiveProtocolFeePercentPoints).toBe(0); + expect(breakdown.totalFeeBps).toBe(120); + expect(breakdown.capped).toBe(true); + }); + + it("disables protocol fee when the mode is none", () => { + const breakdown = applyRevenueFees( + 500, + buildProtocolFeePolicy({ + provider: "minswap", + venueFeePercent: 0, + protocolFeeBps: 90, + maxTotalFeeBps: 100, + protocolFeeMode: "none", + }), + ); + + expect(breakdown.protocolFeeAmount).toBe(0); + expect(breakdown.totalFeeBps).toBe(0); + }); +}); diff --git a/lazer/cardano/guards/source/apps/backend/tests/storage.test.ts b/lazer/cardano/guards/source/apps/backend/tests/storage.test.ts new file mode 100644 index 00000000..47c037bb --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/tests/storage.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, it } from "vitest"; +import { buildSnapshots } from "@anaconda/core"; +import { PythCollector } from "../src/collector.js"; +import { AuditStore } from "../src/storage.js"; + +describe("audit store and collector", () => { + it("records snapshots and returns event counts", () => { + const store = new AuditStore(); + const collector = new PythCollector(store); + const snapshots = buildSnapshots(); + + collector.publish(snapshots.ada!); + collector.publish(snapshots.usdm!); + + const current = collector.current(); + expect(current.ada!.snapshotId).toBe("snapshot-ada"); + expect(store.counts().snapshots).toBe(2); + expect(store.counts().events).toBe(2); + expect(store.listEvents()[0]?.category).toBe("snapshot"); + }); + + it("overwrites the latest snapshot per asset in collector state", () => { + const collector = new PythCollector(); + collector.publish(buildSnapshots().ada!); + collector.publish( + buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-2", + observedAtUs: 2_000_000, + feedUpdateTimestampUs: 2_000_000, + }, + }).ada!, + ); + + expect(collector.current().ada!.snapshotId).toBe("snapshot-ada-2"); + }); + + it("orders audit events deterministically when timestamps tie", () => { + const store = new AuditStore(); + + store.recordEvent({ + eventId: "event-b", + category: "snapshot", + payload: { order: 2 }, + createdAtUs: 1_000_000, + }); + store.recordEvent({ + eventId: "event-a", + category: "snapshot", + payload: { order: 1 }, + createdAtUs: 1_000_000, + }); + + expect(store.listEvents().map((event) => event.eventId)).toEqual(["event-a", "event-b"]); + }); +}); diff --git a/lazer/cardano/guards/source/apps/backend/tests/swap-venue.test.ts b/lazer/cardano/guards/source/apps/backend/tests/swap-venue.test.ts new file mode 100644 index 00000000..5bc8d108 --- /dev/null +++ b/lazer/cardano/guards/source/apps/backend/tests/swap-venue.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { + buildCardanoSwapVenueConfig, + feeBpsToPercentPoints, + feeBpsToRate, + resolveCardanoSwapProvider, +} from "../src/swap-venue.js"; + +describe("swap venue config", () => { + it("normalizes provider values and defaults unknown providers to dexhunter", () => { + expect(resolveCardanoSwapProvider("something-else")).toBe("dexhunter"); + expect(resolveCardanoSwapProvider("dexhunter")).toBe("dexhunter"); + expect(resolveCardanoSwapProvider("minswap")).toBe("minswap"); + expect(resolveCardanoSwapProvider(" MINSWAP ")).toBe("minswap"); + }); + + it("normalizes fee bps to percent points and rate", () => { + expect(feeBpsToPercentPoints(30)).toBe(0.3); + expect(feeBpsToRate(30)).toBe(0.003); + expect(feeBpsToPercentPoints(0)).toBe(0); + expect(feeBpsToPercentPoints(-10)).toBe(0); + expect(feeBpsToPercentPoints(Number.NaN)).toBe(0); + expect(feeBpsToPercentPoints(20_000)).toBe(100); + }); + + it("builds venue config with bounded partner metadata", () => { + const config = buildCardanoSwapVenueConfig({ + provider: " MINSWAP ", + protocolFeeBps: 45, + dexHunterBaseUrl: "https://api-us.dexhunterv3.app", + dexHunterPartnerId: "partner-123", + dexHunterPartnerFeePercent: Number.NaN, + minswapAggregatorUrl: "https://agg-api.minswap.org/aggregator", + minswapPartnerCode: "guards-one", + }); + + expect(config.provider).toBe("minswap"); + expect(config.protocolFeeBps).toBe(45); + expect(config.protocolFeePercentPoints).toBe(0.45); + expect(config.protocolFeeRate).toBe(0.0045); + expect(config.dexHunter.requiresPartnerHeader).toBe(true); + expect(config.dexHunter.partnerFeePercentPoints).toBe(0); + expect(config.dexHunter.partnerFeeRate).toBe(0); + expect(config.minswap.supportsPartnerTracking).toBe(true); + }); +}); diff --git a/lazer/cardano/guards/source/apps/blockchain/README.md b/lazer/cardano/guards/source/apps/blockchain/README.md new file mode 100644 index 00000000..aec0f5e8 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/README.md @@ -0,0 +1,48 @@ +# apps/blockchain + +This workspace is the team-facing entrypoint for all chain, contract, oracle-witness and execution integration work. + +The current implementation still uses `packages/*` as the source of truth for reusable adapters and simulators. This app exists so the team has one obvious place to iterate on: + +- Cardano smart contracts / `Aiken` +- off-chain transaction builders +- Pyth witness integration +- swap-venue adapters +- future SVM / EVM execution connectors + +## Current rule + +Start new blockchain-facing work under `apps/blockchain/*`. + +Use `packages/*` for shared libraries and stable adapter code that needs to be imported by backend/UI/tests. + +## Layout + +- `cardano/contracts`: target home for `Aiken` validators and related test vectors +- `cardano/offchain`: target home for transaction builders, witness preparation and execution services +- `svm`: collaboration surface for Solana/SVM work +- `evm`: collaboration surface for EVM work +- `src/manifests.ts`: machine-readable map of the current source of truth and next planned artifacts + +## Today vs next + +Today: +- Cardano simulator/spec: `packages/cardano` +- Cardano Aiken scaffold: `apps/blockchain/cardano/contracts/aiken` +- backend orchestration: `apps/backend` +- live Pyth collector + witness wiring: `apps/blockchain/cardano/offchain` +- multichain scaffolding: `packages/svm`, `packages/evm` + +Import the live collector explicitly from `@anaconda/blockchain/cardano/offchain`. +The generic `@anaconda/blockchain/cardano` entrypoint stays side-effect free and does not load dotenv or the Pyth SDK. + +Next: +- move more live off-chain execution work into `apps/blockchain/cardano/offchain` +- keep reusable types/engines in `packages/core` + +## Team workflow + +1. Open a feature branch from `main`. +2. Put new chain-facing files under `apps/blockchain/*`. +3. If the change becomes reusable across apps, promote it into `packages/*` in a follow-up commit. +4. Update `NEXT_STEPS.md` when introducing or closing any blockchain task. diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/README.md b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/README.md new file mode 100644 index 00000000..a4c9fc3a --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/README.md @@ -0,0 +1,35 @@ +# Cardano Contracts + +This directory now holds the live Cardano on-chain collaboration surface. + +Current source of truth here: + +- `aiken/aiken.toml` +- `aiken/lib/guards_one/types.ak` +- `aiken/lib/guards_one/policy_vault.ak` +- `aiken/validators/policy_vault.ak` +- `aiken/test/policy_vault.ak` + +What this scaffold already captures: + +- datum/redeemer modeling for `PolicyVault` +- keeper allowlist and route/asset approval helpers +- in-flight intent continuity +- hot-bucket max-notional guard helpers +- completion bounds for `sold_amount` / `bought_amount` + +Still pending: + +- live validator body wiring +- compiled artifacts / addresses for preprod +- witness and datum/redeemer test vectors +- explicit execution-bucket validator scaffold + +Reference simulator/spec: + +- `packages/cardano/src/policy-vault.ts` +- `packages/cardano/src/types.ts` + +Operational guide: + +- [`docs/preprod-vault-bootstrap.md`](../../../../docs/preprod-vault-bootstrap.md) diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/README.md b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/README.md new file mode 100644 index 00000000..b6034cfb --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/README.md @@ -0,0 +1,40 @@ +# guards.one Cardano Aiken Scaffold + +This directory is the bounded scaffold for the on-chain port of the current `PolicyVault` / hot-bucket model. + +It mirrors the invariants currently enforced in TypeScript in [`packages/cardano/src/policy-vault.ts`](../../../../../packages/cardano/src/policy-vault.ts) and the custody model described in [`docs/cardano-custody-model.md`](../../../../../docs/cardano-custody-model.md). + +## Scope + +- Typed datum/redeemer skeletons for the vault state machine. +- Validator entrypoints that document the intended policy checks. +- No live deployment wiring yet. +- No Aiken tooling invocation in this workspace yet. + +## What this scaffold captures + +- governance signers and keeper allowlists +- approved route ids and asset allowlists +- cooldown and stage transitions +- Pyth witness presence in the authorize path +- bounded hot-bucket execution rather than direct multisig spending +- current-intent continuity plus bounded result settlement + +## What is intentionally missing + +- compiled scripts +- address derivation +- Plutus serialization details +- real on-chain Pyth verification +- live DEX/aggregator integration + +## Files + +- `lib/guards_one/types.ak`: datum, redeemer, and helper types. +- `lib/guards_one/policy_vault.ak`: scaffolded policy logic for the vault state machine. +- `validators/policy_vault.ak`: validator entrypoint wiring. +- `test/policy_vault.ak`: scenario catalog for the planned Aiken test suite. + +## Next step + +Finish the validator entrypoint wiring, then connect the generated script to the keeper flow once Aiken is available in CI. diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/aiken.toml b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/aiken.toml new file mode 100644 index 00000000..24168c72 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/aiken.toml @@ -0,0 +1,13 @@ +name = "guards-one-cardano-aiken" +version = "0.1.0" +description = "Bounded Aiken scaffold for the guards.one PolicyVault and hot-bucket model" + +# This scaffold mirrors the current TypeScript simulator and custody docs. +# Aiken tooling is not installed in this workspace, so the validator bodies are intentionally skeletal. + +[dependencies] +# Intentionally empty for now. Add official Aiken packages when the real on-chain port starts. + +[config] +# Deployment scaffold config for the policy-vault port. +policy_id = "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6" diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/lib/guards_one/policy_vault.ak b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/lib/guards_one/policy_vault.ak new file mode 100644 index 00000000..e8b2f222 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/lib/guards_one/policy_vault.ak @@ -0,0 +1,112 @@ +use guards_one/types.{ExecutionIntent, ExecutionResult, PolicyDatum, PythWitness, RiskStage} + +// Scaffold policy module. +// The actual Aiken port should mirror the runtime checks currently implemented in +// `packages/cardano/src/policy-vault.ts` and the custody model in +// `docs/cardano-custody-model.md`. + +pub fn is_keeper_authorized(datum: PolicyDatum, keeper: ByteArray) -> Bool { + list.has(datum.keepers, keeper) +} + +pub fn has_intent_in_flight(datum: PolicyDatum) -> Bool { + when datum.current_intent_id is { + Some(_) -> True + None -> False + } +} + +pub fn is_route_approved(datum: PolicyDatum, route_id: ByteArray) -> Bool { + list.has(datum.approved_route_ids, route_id) +} + +pub fn is_asset_approved(datum: PolicyDatum, asset_id: ByteArray) -> Bool { + asset_id == datum.primary_asset_id || asset_id == datum.stable_asset_id +} + +pub fn is_intent_shape_valid(intent: ExecutionIntent) -> Bool { + intent.max_sell_amount > 0 + && intent.min_buy_amount > 0 + && intent.created_at_us <= intent.expiry_us + && intent.source_asset_id != intent.destination_asset_id +} + +pub fn is_intent_asset_pair_approved(datum: PolicyDatum, intent: ExecutionIntent) -> Bool { + is_asset_approved(datum, intent.source_asset_id) + && is_asset_approved(datum, intent.destination_asset_id) +} + +pub fn is_hot_bucket_intent_bounded(datum: PolicyDatum, intent: ExecutionIntent) -> Bool { + intent.max_sell_amount <= datum.hot_bucket_max_notional +} + +pub fn is_result_matching_intent(intent: ExecutionIntent, result: ExecutionResult) -> Bool { + result.intent_id == intent.intent_id + && result.chain_id == intent.chain_id + && result.source_asset_id == intent.source_asset_id + && result.destination_asset_id == intent.destination_asset_id + && result.route_id == intent.route_id +} + +pub fn is_result_in_window(intent: ExecutionIntent, result: ExecutionResult) -> Bool { + result.executed_at_us >= intent.created_at_us + && result.executed_at_us <= intent.expiry_us +} + +pub fn is_result_within_bounds(intent: ExecutionIntent, result: ExecutionResult) -> Bool { + result.sold_amount >= 0 + && result.bought_amount >= 0 + && result.sold_amount <= intent.max_sell_amount + && result.bought_amount >= intent.min_buy_amount +} + +pub fn is_hot_bucket_result_bounded(datum: PolicyDatum, result: ExecutionResult) -> Bool { + result.sold_amount <= datum.hot_bucket_max_notional +} + +pub fn authorize_execution( + datum: PolicyDatum, + witness: PythWitness, + keeper: ByteArray, + now_us: Int, +) -> Bool { + // Intentionally skeletal: the real validator will check the oracle witness, + // cooldown, route allowlist, and hot-bucket limits before producing an intent. + is_keeper_authorized(datum, keeper) + && !has_intent_in_flight(datum) + && witness.pyth_policy_id == datum.pyth_policy_id + && now_us >= datum.last_transition_us + && now_us - datum.last_transition_us >= datum.cooldown_us +} + +pub fn complete_execution( + datum: PolicyDatum, + intent: ExecutionIntent, + result: ExecutionResult, +) -> Bool { + // The scaffold now mirrors the pure completion invariants from the TypeScript + // simulator even before the tx-context plumbing is available. + datum.vault_id == intent.vault_id + && result.vault_id == datum.vault_id + && datum.current_intent_id == Some(intent.intent_id) + && is_route_approved(datum, result.route_id) + && is_intent_shape_valid(intent) + && is_intent_asset_pair_approved(datum, intent) + && is_hot_bucket_intent_bounded(datum, intent) + && is_result_matching_intent(intent, result) + && is_result_in_window(intent, result) + && is_result_within_bounds(intent, result) + && is_hot_bucket_result_bounded(datum, result) +} + +pub fn next_escalation_stage(current: RiskStage) -> RiskStage { + when current is { + // Re-entry is modeled as a separate policy path; this helper only documents + // the monotonic escalation ladder for the scaffold. + Normal -> Watch + Watch -> PartialDerisk + PartialDerisk -> FullExit + Frozen -> Frozen + FullExit -> Frozen + } +} diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/lib/guards_one/types.ak b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/lib/guards_one/types.ak new file mode 100644 index 00000000..379fe896 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/lib/guards_one/types.ak @@ -0,0 +1,88 @@ +// Bounded Aiken scaffold for guards.one. +// This file intentionally models the data shapes first so the future validator port +// can stay aligned with the current TypeScript simulator. + +pub type RiskStage { + Normal + Watch + PartialDerisk + FullExit + Frozen +} + +pub type ExecutionKind { + DeriskSwap + ReentrySwap +} + +pub type PolicyDatum { + vault_id: ByteArray, + pyth_policy_id: ByteArray, + stage: RiskStage, + last_transition_us: Int, + governance_signers: List, + keepers: List, + approved_route_ids: List, + stable_asset_id: ByteArray, + primary_asset_id: ByteArray, + current_intent_id: Option, + hot_bucket_max_notional: Int, + max_stale_us: Int, + max_confidence_bps: Int, + cooldown_us: Int, +} + +pub type PolicyRedeemer { + AuthorizeExecution, + CompleteExecution, + UpdatePolicy, + Resume, + EmergencyWithdraw, +} + +pub type PythWitness { + pyth_policy_id: ByteArray, + pyth_state_reference: ByteArray, + signed_update_hex: ByteArray, +} + +pub type ExecutionIntent { + intent_id: ByteArray, + vault_id: ByteArray, + chain_id: ByteArray, + kind: ExecutionKind, + stage: RiskStage, + source_asset_id: ByteArray, + destination_asset_id: ByteArray, + route_id: ByteArray, + max_sell_amount: Int, + min_buy_amount: Int, + expiry_us: Int, + created_at_us: Int, + reason_hash: ByteArray, + snapshot_ids: List, +} + +pub type ExecutionResult { + intent_id: ByteArray, + vault_id: ByteArray, + chain_id: ByteArray, + source_asset_id: ByteArray, + destination_asset_id: ByteArray, + sold_amount: Int, + bought_amount: Int, + average_price: Int, + route_id: ByteArray, + executed_at_us: Int, + tx_hash: ByteArray, +} + +pub type PolicyCheckFailure { + KeeperNotAuthorized + PythPolicyMismatch + IntentAlreadyInFlight + RouteNotApproved + AssetNotApproved + CooldownActive + FreezeRequired +} diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/test/policy_vault.ak b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/test/policy_vault.ak new file mode 100644 index 00000000..514ad2ee --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/test/policy_vault.ak @@ -0,0 +1,10 @@ +// Scenario catalog for the future Aiken PolicyVault test suite. +// +// This file stays commentary-only until Aiken tooling is available in the +// workspace. Once the toolchain lands, add property tests for: +// - keeper allowlist checks +// - route allowlist checks +// - pyth policy id witness checks +// - cooldown enforcement +// - hot-bucket bounds +// - authorize -> complete intent continuity diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/validators/policy_vault.ak b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/validators/policy_vault.ak new file mode 100644 index 00000000..8255e307 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/contracts/aiken/validators/policy_vault.ak @@ -0,0 +1,27 @@ +use guards_one/types.{PolicyDatum, PolicyRedeemer} + +// Scaffold validator entrypoint. +// Aiken tooling is not installed in this workspace, so this file documents the +// intended structure instead of providing a compiled on-chain script. + +validator { + fn policy(datum: PolicyDatum, redeemer: PolicyRedeemer, ctx: _) -> Bool { + when redeemer is { + AuthorizeExecution -> + // Fail closed until the real oracle witness and keeper checks are + // wired in. This keeps the scaffold type-safe and non-spendable. + False + + CompleteExecution -> + // Fail closed until the real intent/result structures are wired into + // the validator entrypoint. + False + + // These branches intentionally fail closed until real authorization + // logic exists, to avoid accidental unrestricted spending. + UpdatePolicy -> False + Resume -> False + EmergencyWithdraw -> False + } + } +} diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/README.md b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/README.md new file mode 100644 index 00000000..be251a8b --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/README.md @@ -0,0 +1,30 @@ +# Cardano Offchain + +This directory now holds the live Cardano off-chain Pyth wiring and the first execution-facing modules that the team should extend. + +Current source of truth in this directory: + +- `env.ts`: Cardano/Pyth runtime env loading +- `collector.ts`: in-memory snapshot collector plus live `PythLazer` signed-update fetcher +- `fixtures.ts`: demo witness + primary feed request builder +- `scripts/fetch-live.ts`: local CLI to fetch a real signed update and snapshot from Pyth + +Still pending migration into this directory: + +- `apps/backend/src/keeper.ts` +- `apps/backend/src/swap-venue.ts` +- `apps/backend/src/protocol-fee.ts` +- `apps/backend/src/dexhunter-live.ts` +- `apps/backend/src/dexhunter-keeper.ts` + +Quick check: + +```bash +pnpm pyth:fetch-live +``` + +That command uses `.env`, fetches a real signed update from Pyth Pro/Lazer, and prints the snapshot plus the Cardano witness envelope (`pythPolicyId`, `pythStateReference`, `signedUpdateHexLength`). + +Operational guide: + +- [`docs/preprod-vault-bootstrap.md`](../../../../docs/preprod-vault-bootstrap.md) diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/collector.ts b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/collector.ts new file mode 100644 index 00000000..737de66e --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/collector.ts @@ -0,0 +1,276 @@ +import { PythLazerClient, type Channel } from "@pythnetwork/pyth-lazer-sdk"; +import type { OracleSnapshot } from "@anaconda/core"; +import type { CardanoPythWitness } from "@anaconda/cardano"; +import { runtimeEnv } from "./env.js"; +import type { + PythLazerClientLike, + PythLiveCollectorConfig, + PythLiveFeedRequest, + PythSignedUpdateResult, + PythSymbolMatch, + SnapshotAuditSink, +} from "./types.js"; + +export class PythLiveCollectorError extends Error { + constructor( + readonly code: string, + message: string, + ) { + super(message); + } +} + +function normalizeSymbol(value: string): string { + return value.replace(/[^a-z0-9]/gi, "").toLowerCase(); +} + +function parseRequiredNumber( + value: number | string | undefined, + field: string, +): number { + const parsed = typeof value === "string" ? Number(value) : value; + if (!Number.isFinite(parsed)) { + throw new PythLiveCollectorError( + "PYTH_FIELD_MISSING", + `Pyth update is missing numeric field ${field}`, + ); + } + + return parsed as number; +} + +function normalizeTimestampUs(value: number): number { + if (value >= 1_000_000_000_000_000) { + return Math.trunc(value); + } + + if (value >= 1_000_000_000_000) { + return Math.trunc(value * 1_000); + } + + if (value >= 1_000_000_000) { + return Math.trunc(value * 1_000_000); + } + + return Math.trunc(value); +} + +function buildSnapshotId(assetId: string, observedAtUs: number): string { + return `snapshot:${assetId}:${observedAtUs}`; +} + +function normalizeSignedUpdateHex(value: string): string { + if (value.startsWith("0x") || value.startsWith("0X")) { + return value; + } + + return `0x${value}`; +} + +function createWitness( + signedUpdateHex: string, + config: Pick, +): CardanoPythWitness { + return { + pythPolicyId: config.pythPolicyId, + pythStateReference: config.pythStateReference, + signedUpdateHex, + }; +} + +function selectBestSymbolMatch( + matches: PythSymbolMatch[], + request: PythLiveFeedRequest, +): PythSymbolMatch | undefined { + const query = normalizeSymbol(request.symbolQuery ?? request.symbol); + const exact = matches.find((candidate) => { + const fields = [candidate.symbol, candidate.name, candidate.description].map(normalizeSymbol); + return fields.includes(query); + }); + if (exact) { + return exact; + } + + return matches.find((candidate) => { + const candidateSymbol = normalizeSymbol(candidate.symbol); + return candidateSymbol.endsWith(query) || candidateSymbol.includes(query); + }); +} + +export class PythCollector { + private readonly snapshots = new Map(); + + constructor(protected readonly auditSink?: SnapshotAuditSink) {} + + publish(snapshot: OracleSnapshot) { + this.snapshots.set(snapshot.assetId, snapshot); + this.auditSink?.recordSnapshot?.(snapshot); + this.auditSink?.recordEvent?.({ + eventId: `snapshot:${snapshot.snapshotId}`, + category: "snapshot", + payload: { + assetId: snapshot.assetId, + snapshotId: snapshot.snapshotId, + observedAtUs: snapshot.observedAtUs, + }, + createdAtUs: snapshot.observedAtUs, + }); + } + + current(): Record { + return Object.fromEntries(this.snapshots.entries()); + } +} + +export class PythLiveCollector extends PythCollector { + private readonly priceFeedIdCache = new Map(); + + constructor( + private readonly client: PythLazerClientLike, + private readonly config: PythLiveCollectorConfig, + auditSink?: SnapshotAuditSink, + ) { + super(auditSink); + } + + static async create( + auditSink?: SnapshotAuditSink, + config: Partial = {}, + ) { + if (!runtimeEnv.pythApiKey) { + throw new PythLiveCollectorError( + "PYTH_API_KEY_MISSING", + "PYTH_API_KEY must be configured before creating the live collector", + ); + } + + const client = await PythLazerClient.create({ + token: runtimeEnv.pythApiKey, + webSocketPoolConfig: {}, + ...(runtimeEnv.pythMetadataServiceUrl + ? { metadataServiceUrl: runtimeEnv.pythMetadataServiceUrl } + : {}), + ...(runtimeEnv.pythPriceServiceUrl + ? { priceServiceUrl: runtimeEnv.pythPriceServiceUrl } + : {}), + }); + + return new PythLiveCollector( + client, + { + channel: (config.channel ?? runtimeEnv.pythStreamChannel) as Channel, + pythPolicyId: config.pythPolicyId ?? runtimeEnv.pythPreprodPolicyId, + pythStateReference: + config.pythStateReference ?? runtimeEnv.cardanoPythStateReference, + }, + auditSink, + ); + } + + async resolvePriceFeedId(request: PythLiveFeedRequest): Promise { + if (request.priceFeedId != null) { + return request.priceFeedId; + } + + const cacheKey = `${request.assetType ?? ""}:${request.symbolQuery ?? request.symbol}`; + const cached = this.priceFeedIdCache.get(cacheKey); + if (cached !== undefined) { + return cached; + } + + const matches = await this.client.getSymbols({ + query: request.symbolQuery ?? request.symbol, + ...(request.assetType ? { asset_type: request.assetType } : {}), + }); + + const match = selectBestSymbolMatch(matches, request); + if (!match) { + throw new PythLiveCollectorError( + "PYTH_SYMBOL_NOT_FOUND", + `Unable to resolve a Pyth Lazer price feed for ${request.symbolQuery ?? request.symbol}`, + ); + } + + this.priceFeedIdCache.set(cacheKey, match.pyth_lazer_id); + return match.pyth_lazer_id; + } + + async fetchSignedUpdate(request: PythLiveFeedRequest): Promise { + const resolvedPriceFeedId = await this.resolvePriceFeedId(request); + const raw = await this.client.getLatestPrice({ + channel: this.config.channel, + priceFeedIds: [resolvedPriceFeedId], + properties: [ + "price", + "emaPrice", + "confidence", + "publisherCount", + "exponent", + "marketSession", + "feedUpdateTimestamp", + ], + formats: ["solana"], + parsed: true, + jsonBinaryEncoding: "hex", + }); + + const rawSignedUpdateHex = raw.solana?.data; + if (!rawSignedUpdateHex) { + throw new PythLiveCollectorError( + "PYTH_SIGNED_UPDATE_MISSING", + `Pyth response for ${request.symbol} did not include a solana/Cardano signed update`, + ); + } + const signedUpdateHex = normalizeSignedUpdateHex(rawSignedUpdateHex); + + const parsed = raw.parsed?.priceFeeds.find( + (candidate) => candidate.priceFeedId === resolvedPriceFeedId, + ); + if (!parsed) { + throw new PythLiveCollectorError( + "PYTH_PARSED_FEED_MISSING", + `Pyth response did not include parsed payload for feed ${resolvedPriceFeedId}`, + ); + } + + const observedAtUs = normalizeTimestampUs( + parseRequiredNumber(raw.parsed?.timestampUs, "timestampUs"), + ); + const feedUpdateTimestampUs = normalizeTimestampUs( + parseRequiredNumber(parsed.feedUpdateTimestamp, "feedUpdateTimestamp"), + ); + + const snapshot: OracleSnapshot = { + snapshotId: buildSnapshotId(request.assetId, observedAtUs), + feedId: request.feedId, + assetId: request.assetId, + symbol: request.symbol, + price: parseRequiredNumber(parsed.price, "price"), + emaPrice: parseRequiredNumber(parsed.emaPrice, "emaPrice"), + confidence: parseRequiredNumber(parsed.confidence, "confidence"), + exponent: parseRequiredNumber(parsed.exponent, "exponent"), + feedUpdateTimestampUs, + observedAtUs, + ...(parsed.publisherCount != null + ? { publisherCount: parsed.publisherCount } + : {}), + ...(parsed.marketSession ? { marketSession: parsed.marketSession } : {}), + }; + + const witness = createWitness(signedUpdateHex, this.config); + + return { + snapshot, + witness, + signedUpdateHex, + resolvedPriceFeedId, + raw, + }; + } + + async fetchAndPublish(request: PythLiveFeedRequest): Promise { + const result = await this.fetchSignedUpdate(request); + this.publish(result.snapshot); + return result; + } +} diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/env.ts b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/env.ts new file mode 100644 index 00000000..2877ea78 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/env.ts @@ -0,0 +1,94 @@ +import path from "node:path"; +import { config as loadDotenv } from "dotenv"; + +loadDotenv({ path: path.resolve(process.cwd(), ".env"), quiet: true }); + +function readString(name: string, fallback = ""): string { + const value = process.env[name]; + return typeof value === "string" && value.length > 0 ? value : fallback; +} + +function readNumber(name: string, fallback: number): number { + const value = process.env[name]; + if (!value) { + return fallback; + } + + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +} + +function readOptionalNumber(name: string): number | undefined { + const value = process.env[name]; + if (!value) { + return undefined; + } + + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +export const runtimeEnv = { + appPublicName: readString("APP_PUBLIC_NAME", "guards.one"), + appInternalName: readString("APP_INTERNAL_NAME", "anaconda"), + port: readNumber("PORT", 4310), + pythApiKey: readString("PYTH_API_KEY"), + pythPreprodPolicyId: readString( + "PYTH_PREPROD_POLICY_ID", + "d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6", + ), + pythApiBaseUrl: readString("PYTH_API_BASE_URL", "https://api.pyth.network"), + pythMetadataServiceUrl: readString("PYTH_METADATA_SERVICE_URL"), + pythPriceServiceUrl: readString("PYTH_PRICE_SERVICE_URL"), + pythStreamChannel: readString("PYTH_STREAM_CHANNEL", "fixed_rate@200ms"), + pythPrimaryFeedId: readString("PYTH_PRIMARY_FEED_ID", "pyth-ada-usd"), + pythPrimarySymbolQuery: readString("PYTH_PRIMARY_SYMBOL_QUERY", "ADA/USD"), + pythPrimaryAssetType: readString("PYTH_PRIMARY_ASSET_TYPE", "crypto"), + pythPrimaryPriceFeedId: readOptionalNumber("PYTH_PRIMARY_PRICE_FEED_ID"), + cardanoNetwork: readString("CARDANO_NETWORK", "preprod"), + cardanoProvider: readString("CARDANO_PROVIDER", "blockfrost"), + cardanoProviderUrl: readString( + "CARDANO_PROVIDER_URL", + "https://cardano-preprod.blockfrost.io/api/v0", + ), + cardanoBlockfrostProjectId: readString("CARDANO_BLOCKFROST_PROJECT_ID"), + cardanoPythStateReference: readString( + "CARDANO_PYTH_STATE_REFERENCE", + "pyth-state-ref", + ), + cardanoSwapProvider: readString("CARDANO_SWAP_PROVIDER", "dexhunter"), + cardanoProtocolFeeBps: readNumber("CARDANO_PROTOCOL_FEE_BPS", 30), + cardanoMaxTotalFeeBps: readNumber("CARDANO_MAX_TOTAL_FEE_BPS", 100), + cardanoProtocolFeeMode: readString( + "CARDANO_PROTOCOL_FEE_MODE", + "explicit_output", + ), + cardanoExecutionRouteId: readString( + "CARDANO_EXECUTION_ROUTE_ID", + "cardano-minswap-ada-usdm", + ), + dexHunterBaseUrl: readString( + "DEXHUNTER_BASE_URL", + "https://api-us.dexhunterv3.app", + ), + dexHunterPartnerId: readString("DEXHUNTER_PARTNER_ID"), + dexHunterPartnerFeePercent: readNumber("DEXHUNTER_PARTNER_FEE_PERCENT", 0.3), + minswapAggregatorUrl: readString( + "MINSWAP_AGGREGATOR_URL", + "https://agg-api.minswap.org/aggregator", + ), + minswapPartnerCode: readString("MINSWAP_PARTNER_CODE"), + cardanoExecutionHotWalletAddress: readString( + "CARDANO_EXECUTION_HOT_WALLET_ADDRESS", + ), + cardanoExecutionHotWalletSkeyPath: readString( + "CARDANO_EXECUTION_HOT_WALLET_SKEY_PATH", + "./secrets/execution-hot.skey", + ), + cardanoGovernanceWalletAddress: readString("CARDANO_GOVERNANCE_WALLET_ADDRESS"), + cardanoGovernanceSkeyPath: readString( + "CARDANO_GOVERNANCE_SKEY_PATH", + "./secrets/governance.skey", + ), + auditDbPath: readString("AUDIT_DB_PATH", "./data/guards-one.sqlite"), +} as const; diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/fixtures.ts b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/fixtures.ts new file mode 100644 index 00000000..22a5e852 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/fixtures.ts @@ -0,0 +1,59 @@ +import { + buildSnapshots, + samplePolicy, + sampleRoutes, + sampleTreasury, +} from "@anaconda/core"; +import type { CardanoPythWitness } from "@anaconda/cardano"; +import type { PythLiveFeedRequest } from "./types.js"; +import { runtimeEnv } from "./env.js"; + +export const sampleWitness: CardanoPythWitness = { + pythPolicyId: runtimeEnv.pythPreprodPolicyId, + pythStateReference: runtimeEnv.cardanoPythStateReference, + signedUpdateHex: "0xfeedbeef", +}; + +export function buildPrimaryLiveFeedRequest(): PythLiveFeedRequest { + const primaryPosition = sampleTreasury.positions.find( + (position) => position.assetId === samplePolicy.primaryAssetId, + ); + + const request: PythLiveFeedRequest = { + assetId: samplePolicy.primaryAssetId, + feedId: runtimeEnv.pythPrimaryFeedId, + symbol: primaryPosition ? `${primaryPosition.symbol}/USD` : "ADA/USD", + symbolQuery: runtimeEnv.pythPrimarySymbolQuery, + }; + + const assetType = runtimeEnv.pythPrimaryAssetType; + if (assetType) { + request.assetType = assetType as NonNullable; + } + + if (runtimeEnv.pythPrimaryPriceFeedId != null) { + request.priceFeedId = runtimeEnv.pythPrimaryPriceFeedId; + } + + return request; +} + +export function buildDemoScenario() { + return { + treasury: structuredClone(sampleTreasury), + policy: structuredClone(samplePolicy), + routes: structuredClone(sampleRoutes), + snapshots: buildSnapshots({ + ada: { + snapshotId: "snapshot-ada-live", + feedUpdateTimestampUs: 180_000_000, + observedAtUs: 180_000_000, + }, + usdm: { + snapshotId: "snapshot-usdm-live", + feedUpdateTimestampUs: 180_000_000, + observedAtUs: 180_000_000, + }, + }), + }; +} diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/index.ts b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/index.ts new file mode 100644 index 00000000..5295d41f --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/index.ts @@ -0,0 +1,4 @@ +export * from "./collector.js"; +export * from "./env.js"; +export * from "./fixtures.js"; +export * from "./types.js"; diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/scripts/fetch-live.ts b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/scripts/fetch-live.ts new file mode 100644 index 00000000..69f9227f --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/scripts/fetch-live.ts @@ -0,0 +1,26 @@ +import { buildPrimaryLiveFeedRequest, PythLiveCollector } from "../index.js"; + +const collector = await PythLiveCollector.create(); +const request = buildPrimaryLiveFeedRequest(); +const result = await collector.fetchAndPublish(request); + +console.log( + JSON.stringify( + { + feed: { + assetId: request.assetId, + feedId: request.feedId, + symbol: request.symbol, + resolvedPriceFeedId: result.resolvedPriceFeedId, + }, + snapshot: result.snapshot, + witness: { + pythPolicyId: result.witness.pythPolicyId, + pythStateReference: result.witness.pythStateReference, + signedUpdateHexLength: result.witness.signedUpdateHex.length, + }, + }, + null, + 2, + ), +); diff --git a/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/types.ts b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/types.ts new file mode 100644 index 00000000..a3f18858 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/cardano/offchain/types.ts @@ -0,0 +1,52 @@ +import type { + AssetType, + Channel, + JsonUpdate, + PythLazerClient, + SymbolResponse, +} from "@pythnetwork/pyth-lazer-sdk"; +import type { OracleSnapshot } from "@anaconda/core"; +import type { CardanoPythWitness } from "@anaconda/cardano"; + +export interface SnapshotAuditEvent { + eventId: string; + category: "snapshot"; + payload: Record; + createdAtUs: number; +} + +export interface SnapshotAuditSink { + recordSnapshot?(snapshot: OracleSnapshot): void; + recordEvent?(event: SnapshotAuditEvent): void; +} + +export interface PythLiveFeedRequest { + assetId: string; + feedId: string; + symbol: string; + symbolQuery?: string; + priceFeedId?: number; + assetType?: AssetType; +} + +export interface PythSignedUpdateResult { + snapshot: OracleSnapshot; + witness: CardanoPythWitness; + signedUpdateHex: string; + resolvedPriceFeedId: number; + raw: JsonUpdate; +} + +export interface PythLiveCollectorConfig { + channel: Channel; + pythPolicyId: string; + pythStateReference: string; +} + +export interface PythLazerClientLike + extends Pick {} + +export type PythSymbolMatch = Pick< + SymbolResponse, + "pyth_lazer_id" | "symbol" | "name" | "description" | "asset_type" +>; diff --git a/lazer/cardano/guards/source/apps/blockchain/evm/README.md b/lazer/cardano/guards/source/apps/blockchain/evm/README.md new file mode 100644 index 00000000..f6900955 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/evm/README.md @@ -0,0 +1,14 @@ +# EVM Surface + +This directory is the collaboration surface for EVM execution work. + +Current scaffold: + +- `packages/evm/src/index.ts` +- `packages/evm/src/fixtures.ts` + +Planned work: + +- Safe-aware treasury execution +- DEX route integration +- Pyth EVM verification flow diff --git a/lazer/cardano/guards/source/apps/blockchain/package.json b/lazer/cardano/guards/source/apps/blockchain/package.json new file mode 100644 index 00000000..1dc5f785 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/package.json @@ -0,0 +1,25 @@ +{ + "name": "@anaconda/blockchain", + "version": "0.0.0", + "private": true, + "type": "module", + "engines": { + "node": ">=24.0.0" + }, + "exports": { + ".": "./src/index.ts", + "./cardano": "./src/cardano.ts", + "./cardano/offchain": "./cardano/offchain/index.ts", + "./svm": "./src/svm.ts", + "./evm": "./src/evm.ts", + "./manifests": "./src/manifests.ts" + }, + "dependencies": { + "@anaconda/cardano": "workspace:*", + "@anaconda/core": "workspace:*", + "@anaconda/evm": "workspace:*", + "@anaconda/svm": "workspace:*", + "@pythnetwork/pyth-lazer-sdk": "^6.2.1", + "dotenv": "^17.2.3" + } +} diff --git a/lazer/cardano/guards/source/apps/blockchain/src/cardano.ts b/lazer/cardano/guards/source/apps/blockchain/src/cardano.ts new file mode 100644 index 00000000..efd6a545 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/src/cardano.ts @@ -0,0 +1,7 @@ +export * from "@anaconda/cardano"; +export { + buildSnapshots, + samplePolicy, + sampleRoutes, + sampleTreasury, +} from "@anaconda/core"; diff --git a/lazer/cardano/guards/source/apps/blockchain/src/evm.ts b/lazer/cardano/guards/source/apps/blockchain/src/evm.ts new file mode 100644 index 00000000..ee20cf69 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/src/evm.ts @@ -0,0 +1 @@ +export * from "@anaconda/evm"; diff --git a/lazer/cardano/guards/source/apps/blockchain/src/index.ts b/lazer/cardano/guards/source/apps/blockchain/src/index.ts new file mode 100644 index 00000000..616ee134 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/src/index.ts @@ -0,0 +1,4 @@ +export * from "./manifests.js"; +export * as cardanoSurface from "./cardano.js"; +export * as svmSurface from "./svm.js"; +export * as evmSurface from "./evm.js"; diff --git a/lazer/cardano/guards/source/apps/blockchain/src/manifests.ts b/lazer/cardano/guards/source/apps/blockchain/src/manifests.ts new file mode 100644 index 00000000..fbb3fa7c --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/src/manifests.ts @@ -0,0 +1,81 @@ +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +export interface CollaborationSurface { + chain: "cardano" | "svm" | "evm"; + appRoot: string; + contractsRoot?: string; + offchainRoot?: string; + adapterPackage: string; + currentSourceOfTruth: string[]; + nextArtifacts: string[]; +} + +const manifestDir = path.dirname(fileURLToPath(import.meta.url)); +const repoRoot = path.resolve(manifestDir, "../../.."); + +function fromRoot(...segments: string[]): string { + return path.join(repoRoot, ...segments); +} + +export const blockchainSurface: Record< + CollaborationSurface["chain"], + CollaborationSurface +> = { + cardano: { + chain: "cardano", + appRoot: fromRoot("apps", "blockchain", "cardano"), + contractsRoot: fromRoot("apps", "blockchain", "cardano", "contracts"), + offchainRoot: fromRoot("apps", "blockchain", "cardano", "offchain"), + adapterPackage: "@anaconda/cardano", + currentSourceOfTruth: [ + fromRoot("packages", "cardano", "src", "policy-vault.ts"), + fromRoot("packages", "cardano", "src", "types.ts"), + fromRoot("apps", "blockchain", "cardano", "offchain", "collector.ts"), + fromRoot("apps", "blockchain", "cardano", "offchain", "env.ts"), + fromRoot("apps", "blockchain", "cardano", "contracts", "aiken", "lib", "guards_one", "policy_vault.ak"), + fromRoot("apps", "blockchain", "cardano", "contracts", "aiken", "validators", "policy_vault.ak"), + fromRoot("apps", "backend", "src", "keeper.ts"), + ], + nextArtifacts: [ + "Aiken validator for PolicyVault", + "Execution hot-bucket validator rules", + "DexHunter live execution wiring", + "Automatic Pyth State UTxO resolution from Cardano provider", + ], + }, + svm: { + chain: "svm", + appRoot: fromRoot("apps", "blockchain", "svm"), + adapterPackage: "@anaconda/svm", + currentSourceOfTruth: [ + fromRoot("packages", "svm", "src", "index.ts"), + fromRoot("packages", "svm", "src", "fixtures.ts"), + ], + nextArtifacts: [ + "Wallet Standard connector", + "Jupiter route adapter", + "Pyth-native execution path", + ], + }, + evm: { + chain: "evm", + appRoot: fromRoot("apps", "blockchain", "evm"), + adapterPackage: "@anaconda/evm", + currentSourceOfTruth: [ + fromRoot("packages", "evm", "src", "index.ts"), + fromRoot("packages", "evm", "src", "fixtures.ts"), + ], + nextArtifacts: [ + "Safe-aware treasury connector", + "Uniswap route adapter", + "Pyth EVM verification path", + ], + }, +}; + +export const collaborationGuide = { + entrypoint: fromRoot("apps", "blockchain"), + rule: + "New chain-facing work should start in apps/blockchain/* so the team has one obvious collaboration surface. Shared primitives stay in packages/* until a breaking migration is planned.", +}; diff --git a/lazer/cardano/guards/source/apps/blockchain/src/svm.ts b/lazer/cardano/guards/source/apps/blockchain/src/svm.ts new file mode 100644 index 00000000..f9564874 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/src/svm.ts @@ -0,0 +1 @@ +export * from "@anaconda/svm"; diff --git a/lazer/cardano/guards/source/apps/blockchain/svm/README.md b/lazer/cardano/guards/source/apps/blockchain/svm/README.md new file mode 100644 index 00000000..352d2612 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/svm/README.md @@ -0,0 +1,14 @@ +# SVM Surface + +This directory is the collaboration surface for Solana/SVM execution work. + +Current scaffold: + +- `packages/svm/src/index.ts` +- `packages/svm/src/fixtures.ts` + +Planned work: + +- wallet connector +- Jupiter integration +- Pyth-native execution flow diff --git a/lazer/cardano/guards/source/apps/blockchain/tests/cardano-offchain.test.ts b/lazer/cardano/guards/source/apps/blockchain/tests/cardano-offchain.test.ts new file mode 100644 index 00000000..1b441122 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/tests/cardano-offchain.test.ts @@ -0,0 +1,198 @@ +import { describe, expect, it, vi } from "vitest"; +import { + PythCollector, + PythLiveCollector, + PythLiveCollectorError, + type PythLiveFeedRequest, + type SnapshotAuditSink, +} from "@anaconda/blockchain/cardano/offchain"; +import type { OracleSnapshot } from "@anaconda/core"; +import type { SymbolResponse } from "@pythnetwork/pyth-lazer-sdk"; + +const request: PythLiveFeedRequest = { + assetId: "ada", + feedId: "pyth-ada-usd", + symbol: "ADA/USD", + symbolQuery: "ADA/USD", + assetType: "crypto", +}; + +function buildCollectorPayload() { + return { + parsed: { + timestampUs: "1700000000200000", + priceFeeds: [ + { + priceFeedId: 17, + price: "45230000", + emaPrice: "46000000", + confidence: 1200, + exponent: -8, + publisherCount: 14, + marketSession: "active", + feedUpdateTimestamp: 1_700_000_000, + }, + ], + }, + solana: { + encoding: "hex" as const, + data: "deadbeef", + }, + }; +} + +function buildSymbolMatch(): SymbolResponse { + return { + pyth_lazer_id: 17, + symbol: "Crypto.ADA/USD", + name: "ADA/USD", + description: "Cardano spot", + asset_type: "crypto", + exponent: -8, + min_publishers: 1, + min_channel: "fixed_rate@200ms", + state: "active", + schedule: "24/7", + }; +} + +describe("PythCollector", () => { + it("stores snapshots and emits snapshot audit events", () => { + const sink: SnapshotAuditSink = { + recordSnapshot: vi.fn(), + recordEvent: vi.fn(), + }; + const collector = new PythCollector(sink); + const snapshot: OracleSnapshot = { + snapshotId: "snapshot:ada:1", + assetId: "ada", + feedId: "pyth-ada-usd", + symbol: "ADA/USD", + price: 1, + emaPrice: 1, + confidence: 0, + exponent: 0, + feedUpdateTimestampUs: 1, + observedAtUs: 1, + }; + + collector.publish(snapshot); + + expect(collector.current()).toEqual({ ada: snapshot }); + expect(sink.recordSnapshot).toHaveBeenCalledWith(snapshot); + expect(sink.recordEvent).toHaveBeenCalledWith({ + eventId: "snapshot:snapshot:ada:1", + category: "snapshot", + payload: { + assetId: "ada", + snapshotId: "snapshot:ada:1", + observedAtUs: 1, + }, + createdAtUs: 1, + }); + }); +}); + +describe("PythLiveCollector", () => { + it("resolves a symbol, fetches a signed update, and publishes a snapshot", async () => { + const sink: SnapshotAuditSink = { + recordSnapshot: vi.fn(), + recordEvent: vi.fn(), + }; + const client = { + getSymbols: vi.fn(async () => [buildSymbolMatch()]), + getLatestPrice: vi.fn(async () => buildCollectorPayload()), + }; + + const collector = new PythLiveCollector( + client, + { + channel: "fixed_rate@200ms", + pythPolicyId: "policy-id", + pythStateReference: "state-ref", + }, + sink, + ); + + const result = await collector.fetchAndPublish(request); + + expect(client.getSymbols).toHaveBeenCalledTimes(1); + expect(client.getLatestPrice).toHaveBeenCalledWith({ + channel: "fixed_rate@200ms", + priceFeedIds: [17], + properties: [ + "price", + "emaPrice", + "confidence", + "publisherCount", + "exponent", + "marketSession", + "feedUpdateTimestamp", + ], + formats: ["solana"], + parsed: true, + jsonBinaryEncoding: "hex", + }); + expect(result.witness).toEqual({ + pythPolicyId: "policy-id", + pythStateReference: "state-ref", + signedUpdateHex: "0xdeadbeef", + }); + expect(result.snapshot).toMatchObject({ + assetId: "ada", + feedId: "pyth-ada-usd", + symbol: "ADA/USD", + price: 45_230_000, + emaPrice: 46_000_000, + confidence: 1200, + exponent: -8, + publisherCount: 14, + marketSession: "active", + observedAtUs: 1_700_000_000_200_000, + feedUpdateTimestampUs: 1_700_000_000_000_000, + }); + expect(collector.current().ada?.snapshotId).toBe( + result.snapshot.snapshotId, + ); + expect(sink.recordSnapshot).toHaveBeenCalledOnce(); + expect(sink.recordEvent).toHaveBeenCalledOnce(); + }); + + it("caches symbol resolution across multiple fetches", async () => { + const client = { + getSymbols: vi.fn(async () => [buildSymbolMatch()]), + getLatestPrice: vi.fn(async () => buildCollectorPayload()), + }; + + const collector = new PythLiveCollector(client, { + channel: "fixed_rate@200ms", + pythPolicyId: "policy-id", + pythStateReference: "state-ref", + }); + + await collector.fetchSignedUpdate(request); + await collector.fetchSignedUpdate(request); + + expect(client.getSymbols).toHaveBeenCalledTimes(1); + expect(client.getLatestPrice).toHaveBeenCalledTimes(2); + }); + + it("fails closed when the signed update payload is missing", async () => { + const client = { + getSymbols: vi.fn(async () => [buildSymbolMatch()]), + getLatestPrice: vi.fn(async () => ({ + parsed: buildCollectorPayload().parsed, + })), + }; + + const collector = new PythLiveCollector(client, { + channel: "fixed_rate@200ms", + pythPolicyId: "policy-id", + pythStateReference: "state-ref", + }); + + await expect(collector.fetchSignedUpdate(request)).rejects.toMatchObject({ + code: "PYTH_SIGNED_UPDATE_MISSING", + } satisfies Partial); + }); +}); diff --git a/lazer/cardano/guards/source/apps/blockchain/tests/manifests.test.ts b/lazer/cardano/guards/source/apps/blockchain/tests/manifests.test.ts new file mode 100644 index 00000000..475bd602 --- /dev/null +++ b/lazer/cardano/guards/source/apps/blockchain/tests/manifests.test.ts @@ -0,0 +1,24 @@ +import fs from "node:fs"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { blockchainSurface, collaborationGuide } from "../src/manifests.js"; + +describe("blockchain collaboration surface", () => { + it("anchors the app-level blockchain workspace under apps/blockchain", () => { + expect(collaborationGuide.entrypoint.endsWith(path.join("apps", "blockchain"))).toBe(true); + expect(fs.existsSync(collaborationGuide.entrypoint)).toBe(true); + expect(fs.statSync(collaborationGuide.entrypoint).isDirectory()).toBe(true); + }); + + it("declares concrete Cardano collaboration paths", () => { + expect(blockchainSurface.cardano.contractsRoot).toContain( + path.join("apps", "blockchain", "cardano", "contracts"), + ); + expect(fs.existsSync(blockchainSurface.cardano.contractsRoot!)).toBe(true); + expect(fs.statSync(blockchainSurface.cardano.contractsRoot!).isDirectory()).toBe(true); + for (const currentSource of blockchainSurface.cardano.currentSourceOfTruth) { + expect(fs.existsSync(currentSource)).toBe(true); + expect(fs.statSync(currentSource).isFile()).toBe(true); + } + }); +}); diff --git a/lazer/cardano/guards/source/apps/ui-legacy/app.js b/lazer/cardano/guards/source/apps/ui-legacy/app.js new file mode 100644 index 00000000..117f9c75 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui-legacy/app.js @@ -0,0 +1,617 @@ +const fallbackState = { + source: "static-fallback", + workspace: { + name: "Anaconda Treasury Demo", + label: "guards.one live desk", + chain: "Cardano preprod", + stage: "Normal", + threshold: "3 governance signers", + members: "1 risk manager · 2 keepers", + hotWallet: "addr_test...hot_1", + governanceWallet: "addr_test...gov_1", + totalBalance: "$6,646", + primaryAsset: "ADA", + stableAsset: "USDM", + vaultId: "vault-anaconda-demo", + }, + topbarChips: [ + { label: "Network status", value: "Cardano preprod live", tone: "live" }, + { label: "Approved route", value: "Minswap · USDM", tone: "neutral" }, + { label: "Execution wallet", value: "addr_tes...hot_1", tone: "neutral" }, + ], + heroMetrics: [ + { + label: "Current stage", + value: "Normal", + copy: "Final policy state after the simulated breach and recovery run.", + chip: "ok", + }, + { + label: "Treasury liquid value", + value: "$6,646", + copy: "Valued from the latest Pyth price and haircut-aware liquid value math.", + }, + { + label: "Stable protection", + value: "36.0%", + copy: "Current treasury share parked in the approved stable reserve.", + }, + { + label: "Oracle freshness", + value: "20s", + copy: "Age of the primary feed update relative to the latest policy evaluation.", + }, + ], + dashboardCards: [ + { + label: "Protected floor", + value: "$4,500", + copy: "Minimum fiat-equivalent value the policy tries to keep defended at all times.", + chip: "ok", + }, + { + label: "Emergency floor", + value: "$3,400", + copy: "Crossing this floor escalates the vault into a full stable exit or freeze path.", + chip: "danger", + }, + { + label: "Primary reason", + value: "Cooldown prevents an automatic state transition", + copy: "Top reason emitted by the risk engine for the current snapshot set.", + chip: "warn", + }, + { + label: "Execution policy", + value: "CARDANO · Minswap · USDM", + copy: "Only allowlisted routes can spend from the execution hot wallet.", + chip: "ok", + }, + ], + chainCards: [ + { + chain: "CARDANO", + title: "Live execution surface", + copy: "Cardano is the only live execution target in the MVP. Execution uses a two-step authorize-and-swap flow.", + chip: "live", + }, + { + chain: "SVM", + title: "Scaffolded adapter", + copy: "Scaffolding only in the MVP. Designed to share the same policy and role model.", + chip: "scaffold", + }, + { + chain: "EVM", + title: "Scaffolded adapter", + copy: "Scaffolding only in the MVP. Connectors remain simulation-first until phase 2.", + chip: "scaffold", + }, + ], + riskLadder: [ + { + stage: "Normal", + title: "Operate with full permissions", + copy: "Fresh oracle, healthy confidence, and no forced action path active.", + }, + { + stage: "Watch", + title: "Increase monitoring", + copy: "The drawdown or fiat floor approaches the first trigger band.", + tone: "partial", + }, + { + stage: "Partial De-Risk", + title: "Sell only what restores the safe floor", + copy: "The keeper sells a bounded slice of the risky bucket into the approved stable route.", + tone: "partial", + }, + { + stage: "Full Stable Exit", + title: "Move the vault fully defensive", + copy: "A deeper breach exits the risk bucket and keeps the hot wallet on one stable rail.", + tone: "full", + }, + { + stage: "Auto Re-entry", + title: "Re-risk only after hysteresis clears", + copy: "Recovery must clear a separate band and cooldown before exposure comes back.", + tone: "reentry", + }, + ], + executionTimeline: [ + { + title: "Step 1 · Partial De-Risk", + copy: "Intent intent-1 anchored and settled in tx tx-1.", + status: "executed", + }, + { + title: "Step 2 · Full Stable Exit", + copy: "Intent intent-2 anchored and settled in tx tx-2.", + status: "executed", + }, + { + title: "Step 3 · Normal", + copy: "Intent intent-3 anchored and settled in tx tx-3.", + status: "executed", + }, + ], + auditTrail: [ + { + title: "EXECUTION · tx-915d", + copy: "Settled normal. Sold 4,440 and bought 5,553.", + stamp: "0s", + }, + ], + accounts: [ + { + label: "ADA risk bucket", + address: "addr_tes...hot_1", + balance: "5,553 ADA", + fiatValue: "$4,266", + weight: "64.2%", + role: "Risk asset", + bucket: "Execution hot", + }, + { + label: "USDM stable reserve", + address: "addr_tes...hot_1", + balance: "2,393 USDM", + fiatValue: "$2,393", + weight: "35.8%", + role: "Stable reserve", + bucket: "Execution hot", + }, + ], + portfolioSeries: [ + { label: "Watch", stage: "watch", value: 8400, displayValue: "$8,400" }, + { label: "Partial", stage: "partial_derisk", value: 7310, displayValue: "$7,310" }, + { label: "Exit", stage: "full_exit", value: 4580, displayValue: "$4,580" }, + { label: "Recovery", stage: "normal", value: 6646, displayValue: "$6,646" }, + ], + demoFrames: [ + { + label: "01", + title: "Watchlist breach detected", + copy: "ADA slips under its EMA while Pyth freshness and confidence remain healthy enough to authorize a policy action.", + stage: "watch", + balance: "$8,400", + stableRatio: "17.9%", + reason: "ADA liquid value fell below its protected floor", + }, + { + label: "02", + title: "Partial de-risk executes", + copy: "The keeper emits an intent, swaps only the bounded amount needed, and restores the defended stable floor.", + stage: "partial_derisk", + balance: "$7,310", + stableRatio: "44.0%", + reason: "Partial stable target restored.", + }, + { + label: "03", + title: "Full stable exit after the second leg down", + copy: "A deeper price break and thinner asset cushion push the vault into a full defensive configuration on the approved stable route.", + stage: "full_exit", + balance: "$4,580", + stableRatio: "100.0%", + reason: "Emergency floor forced the full stable path.", + }, + { + label: "04", + title: "Auto re-entry restores exposure", + copy: "Recovery clears the hysteresis band, cooldown expires, and the treasury re-enters risk according to the configured target ratio.", + stage: "normal", + balance: "$6,646", + stableRatio: "36.0%", + reason: "Recovery cleared the re-entry guardrails.", + }, + ], +}; + +const workspaceCard = document.querySelector("#workspace-card"); +const topbarChips = document.querySelector("#topbar-chips"); +const heroMetrics = document.querySelector("#hero-metrics"); +const dashboardCards = document.querySelector("#dashboard-cards"); +const chainList = document.querySelector("#chain-list"); +const ladderList = document.querySelector("#risk-ladder-list"); +const executionTimeline = document.querySelector("#execution-timeline"); +const auditTrail = document.querySelector("#audit-trail"); +const accountsBody = document.querySelector("#accounts-body"); +const overviewStage = document.querySelector("#overview-stage"); +const overviewBalance = document.querySelector("#overview-balance"); +const overviewCopy = document.querySelector("#overview-copy"); +const demoTitle = document.querySelector("#demo-title"); +const demoStage = document.querySelector("#demo-stage"); +const demoCopy = document.querySelector("#demo-copy"); +const frameStrip = document.querySelector("#frame-strip"); +const portfolioChart = document.querySelector("#portfolio-chart"); +const frameBalance = document.querySelector("#frame-balance"); +const frameStableRatio = document.querySelector("#frame-stable-ratio"); +const frameReason = document.querySelector("#frame-reason"); +const replayButton = document.querySelector("#replay-button"); + +const allowedTones = new Set(["ok", "warn", "danger", "live", "neutral", "executed", "rejected"]); +const allowedStages = new Set(["normal", "watch", "partial_derisk", "full_exit", "frozen"]); +const allowedLadderTones = new Set(["", "partial", "full", "reentry"]); +const stageFramesToLadderCount = [2, 3, 4, 5]; +let replayTimer = null; +let currentState = fallbackState; +let activeFrameIndex = Math.max(0, fallbackState.demoFrames.length - 1); + +async function loadState() { + const candidates = ["/api/demo-state", "./data/demo-state.json"]; + + for (const url of candidates) { + try { + const response = await fetch(url); + if (response.ok) { + return await response.json(); + } + } catch { + // Try the next source. + } + } + + return fallbackState; +} + +function escapeHTML(value) { + if (value === null || value === undefined) { + return ""; + } + + return String(value) + .replaceAll("&", "&") + .replaceAll("<", "<") + .replaceAll(">", ">") + .replaceAll('"', """) + .replaceAll("'", "'"); +} + +function safeToken(value, allowed, fallback) { + return allowed.has(value) ? value : fallback; +} + +function toneForStage(stage) { + switch (safeToken(stage, allowedStages, "watch")) { + case "normal": + return "ok"; + case "watch": + return "warn"; + case "partial_derisk": + return "warn"; + case "full_exit": + return "danger"; + case "frozen": + return "danger"; + default: + return "neutral"; + } +} + +function humanizeStage(stage) { + return String(stage).replaceAll("_", " "); +} + +function renderWorkspace(workspace) { + return ` +
+
G1
+
+

${escapeHTML(workspace.label)}

+ ${escapeHTML(workspace.name)} +
+
+
+ Total balance + ${escapeHTML(workspace.totalBalance)} +
+
+
+ Stage + ${escapeHTML(workspace.stage)} +
+
+ Chain + ${escapeHTML(workspace.chain)} +
+
+ Primary + ${escapeHTML(workspace.primaryAsset)} +
+
+ Stable + ${escapeHTML(workspace.stableAsset)} +
+
+
+

${escapeHTML(workspace.threshold)}

+

${escapeHTML(workspace.members)}

+

Hot wallet ${escapeHTML(workspace.hotWallet)}

+

Vault ${escapeHTML(workspace.vaultId)}

+
+ `; +} + +function renderTopbarChip({ label, value, tone }) { + const safeTone = safeToken(tone, allowedTones, "neutral"); + return ` +
+ ${escapeHTML(label)} + ${escapeHTML(value)} +
+ `; +} + +function renderMetric({ label, value, copy, chip }) { + const safeChip = safeToken(chip ?? "neutral", allowedTones, "neutral"); + return ` +
+
+ ${escapeHTML(label)} + ${chip ? `` : ""} +
+ ${escapeHTML(value)} +

${escapeHTML(copy)}

+
+ `; +} + +function renderPolicyCard({ label, value, copy, chip }) { + const safeChip = safeToken(chip, allowedTones, "neutral"); + return ` +
+
+ ${escapeHTML(label)} + +
+ ${escapeHTML(value)} +

${escapeHTML(copy)}

+
+ `; +} + +function renderChain({ chain, title, copy, chip }) { + const safeChip = chip === "live" ? "live" : "warn"; + return ` +
+
+ ${escapeHTML(chain)} + ${escapeHTML(chip === "live" ? "Live" : "Scaffold")} +
+ ${escapeHTML(title)} +

${escapeHTML(copy)}

+
+ `; +} + +function renderLadder({ stage, title, copy, tone = "" }, index) { + const safeTone = safeToken(tone ?? "", allowedLadderTones, ""); + const toneClass = safeTone ? ` ladder-${safeTone}` : ""; + return ` +
+ ${escapeHTML(stage)} + ${escapeHTML(title)} +

${escapeHTML(copy)}

+
+ `; +} + +function renderTimelineItem({ title, copy, status }, index) { + const safeStatus = status === "rejected" ? "rejected" : "executed"; + return ` +
+
+ ${escapeHTML(title)} + ${escapeHTML(safeStatus)} +
+

${escapeHTML(copy)}

+
+ `; +} + +function renderAuditItem({ title, copy, stamp }) { + return ` +
+
+ ${escapeHTML(title)} + ${escapeHTML(stamp)} ago +
+

${escapeHTML(copy)}

+
+ `; +} + +function renderAccountRow({ label, address, balance, fiatValue, weight, role, bucket }) { + return ` + + + + + ${escapeHTML(balance)} + ${escapeHTML(fiatValue)} + ${escapeHTML(weight)} + + `; +} + +function renderFramePill(frame, index, activeIndex) { + const safeTone = toneForStage(frame.stage); + const activeClass = index === activeIndex ? " active" : ""; + return ` + + `; +} + +function renderChart(series, activeIndex) { + if (!Array.isArray(series) || series.length === 0) { + return ""; + } + + const width = 640; + const height = 280; + const padX = 36; + const padTop = 18; + const padBottom = 44; + const values = series.map((point) => Number(point.value) || 0); + const min = Math.min(...values); + const max = Math.max(...values); + const span = Math.max(1, max - min); + const innerWidth = width - padX * 2; + const innerHeight = height - padTop - padBottom; + + const x = (index) => padX + (innerWidth * index) / Math.max(1, series.length - 1); + const y = (value) => padTop + (1 - (value - min) / span) * innerHeight; + + const linePath = series + .map((point, index) => `${index === 0 ? "M" : "L"} ${x(index).toFixed(2)} ${y(point.value).toFixed(2)}`) + .join(" "); + const areaPath = `${linePath} L ${x(series.length - 1).toFixed(2)} ${(height - padBottom).toFixed(2)} L ${x(0).toFixed(2)} ${(height - padBottom).toFixed(2)} Z`; + + const grid = Array.from({ length: 3 }, (_, index) => { + const yPos = padTop + (innerHeight * index) / 2; + return ``; + }).join(""); + + const points = series + .map((point, index) => { + const tone = toneForStage(point.stage); + const activeClass = index === activeIndex ? " active" : ""; + return ` + + + ${escapeHTML(point.label)} + + `; + }) + .join(""); + + const activeValue = series[activeIndex]?.displayValue ?? ""; + const activeX = x(activeIndex); + const activeY = y(series[activeIndex]?.value ?? 0); + + return ` + + + + + + + ${grid} + + + ${points} + + + ${escapeHTML(activeValue)} + + `; +} + +function setStagePill(element, label, stage) { + const tone = toneForStage(stage); + element.textContent = label; + element.className = `status-pill tone-${tone}`; +} + +function applyFrame(state, index) { + const frames = Array.isArray(state.demoFrames) ? state.demoFrames : []; + if (frames.length === 0) { + return; + } + + activeFrameIndex = Math.max(0, Math.min(index, frames.length - 1)); + const frame = frames[activeFrameIndex]; + demoTitle.textContent = frame.title; + demoCopy.textContent = frame.copy; + setStagePill(demoStage, humanizeStage(frame.stage), frame.stage); + frameBalance.textContent = frame.balance; + frameStableRatio.textContent = frame.stableRatio; + frameReason.textContent = frame.reason; + frameStrip.innerHTML = frames.map((item, frameIndex) => renderFramePill(item, frameIndex, activeFrameIndex)).join(""); + portfolioChart.innerHTML = renderChart(state.portfolioSeries ?? [], activeFrameIndex); + + const timelineItems = executionTimeline.querySelectorAll(".timeline-item"); + timelineItems.forEach((item, timelineIndex) => { + item.classList.toggle("is-complete", timelineIndex < activeFrameIndex - 1); + item.classList.toggle("is-active", timelineIndex === activeFrameIndex - 1); + }); + + const ladderCards = ladderList.querySelectorAll(".ladder-card"); + const activeCount = stageFramesToLadderCount[Math.min(activeFrameIndex, stageFramesToLadderCount.length - 1)] ?? ladderCards.length; + ladderCards.forEach((item, ladderIndex) => { + item.classList.toggle("is-active", ladderIndex < activeCount); + }); +} + +function startReplay() { + if (replayTimer) { + clearInterval(replayTimer); + replayTimer = null; + } + + applyFrame(currentState, 0); + replayTimer = window.setInterval(() => { + const nextIndex = activeFrameIndex + 1; + if (nextIndex >= currentState.demoFrames.length) { + clearInterval(replayTimer); + replayTimer = null; + return; + } + + applyFrame(currentState, nextIndex); + }, 1400); +} + +function renderState(state) { + currentState = state; + workspaceCard.innerHTML = renderWorkspace(state.workspace); + topbarChips.innerHTML = state.topbarChips.map(renderTopbarChip).join(""); + heroMetrics.innerHTML = state.heroMetrics.map(renderMetric).join(""); + dashboardCards.innerHTML = state.dashboardCards.map(renderPolicyCard).join(""); + chainList.innerHTML = state.chainCards.map(renderChain).join(""); + ladderList.innerHTML = state.riskLadder.map(renderLadder).join(""); + executionTimeline.innerHTML = state.executionTimeline.map(renderTimelineItem).join(""); + auditTrail.innerHTML = (state.auditTrail ?? []).map(renderAuditItem).join(""); + accountsBody.innerHTML = state.accounts.map(renderAccountRow).join(""); + + overviewBalance.textContent = state.workspace.totalBalance; + overviewCopy.textContent = state.dashboardCards[2]?.value ?? "Policy is currently inside the safe band."; + setStagePill(overviewStage, state.workspace.stage, state.demoFrames.at(-1)?.stage ?? "normal"); + applyFrame(state, Math.max(0, state.demoFrames.length - 1)); +} + +frameStrip.addEventListener("click", (event) => { + const target = event.target.closest("[data-frame-index]"); + if (!(target instanceof HTMLElement)) { + return; + } + + const index = Number(target.dataset.frameIndex); + if (!Number.isFinite(index)) { + return; + } + + if (replayTimer) { + clearInterval(replayTimer); + replayTimer = null; + } + + applyFrame(currentState, index); +}); + +replayButton.addEventListener("click", () => { + startReplay(); +}); + +loadState().then((state) => { + renderState(state); +}); diff --git a/lazer/cardano/guards/source/apps/ui-legacy/data/demo-state.json b/lazer/cardano/guards/source/apps/ui-legacy/data/demo-state.json new file mode 100644 index 00000000..726f5b53 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui-legacy/data/demo-state.json @@ -0,0 +1,268 @@ +{ + "generatedAtUs": 360000000, + "source": "backend-demo", + "workspace": { + "name": "Anaconda Treasury Demo", + "label": "guards.one live desk", + "chain": "Cardano preprod", + "stage": "Normal", + "threshold": "3 governance signers", + "members": "1 risk manager · 2 keepers", + "hotWallet": "addr_tes..._hot_1", + "governanceWallet": "addr_tes..._gov_1", + "totalBalance": "$6,646", + "primaryAsset": "ADA", + "stableAsset": "USDM", + "vaultId": "vault-anaconda-demo" + }, + "topbarChips": [ + { + "label": "Network status", + "value": "Cardano preprod live", + "tone": "live" + }, + { + "label": "Approved route", + "value": "Minswap · USDM", + "tone": "neutral" + }, + { + "label": "Execution wallet", + "value": "addr_tes..._hot_1", + "tone": "neutral" + } + ], + "heroMetrics": [ + { + "label": "Current stage", + "value": "Normal", + "copy": "Final policy state after the simulated breach and recovery run.", + "chip": "ok" + }, + { + "label": "Treasury liquid value", + "value": "$6,646", + "copy": "Valued from the latest Pyth price and haircut-aware liquid value math." + }, + { + "label": "Stable protection", + "value": "36.0%", + "copy": "Current treasury share parked in the approved stable reserve." + }, + { + "label": "Oracle freshness", + "value": "20s", + "copy": "Age of the primary feed update relative to the latest policy evaluation." + } + ], + "dashboardCards": [ + { + "label": "Protected floor", + "value": "$4,500", + "copy": "Minimum fiat-equivalent value the policy tries to keep defended at all times.", + "chip": "ok" + }, + { + "label": "Emergency floor", + "value": "$3,400", + "copy": "Crossing this floor escalates the vault into a full stable exit or freeze path.", + "chip": "danger" + }, + { + "label": "Primary reason", + "value": "Cooldown prevents an automatic state transition", + "copy": "Top reason emitted by the risk engine for the current snapshot set.", + "chip": "warn" + }, + { + "label": "Execution policy", + "value": "CARDANO · Minswap · USDM", + "copy": "Only allowlisted routes can spend from the execution hot wallet.", + "chip": "ok" + } + ], + "chainCards": [ + { + "chain": "CARDANO", + "title": "Live execution surface", + "copy": "Cardano is the only live execution target in the MVP. Execution uses a two-step authorize-and-swap flow.", + "chip": "live" + }, + { + "chain": "SVM", + "title": "Scaffolded adapter", + "copy": "Scaffolding only in the MVP. Designed to share the same policy and role model.", + "chip": "scaffold" + }, + { + "chain": "EVM", + "title": "Scaffolded adapter", + "copy": "Scaffolding only in the MVP. Connectors remain simulation-first until phase 2.", + "chip": "scaffold" + } + ], + "riskLadder": [ + { + "stage": "Normal", + "title": "Operate with full permissions", + "copy": "Fresh oracle, healthy confidence, and no forced action path active." + }, + { + "stage": "Watch", + "title": "Increase monitoring", + "copy": "The drawdown or fiat floor approaches the first trigger band.", + "tone": "partial" + }, + { + "stage": "Partial De-Risk", + "title": "Sell only what restores the safe floor", + "copy": "The keeper sells a bounded slice of the risky bucket into the approved stable route.", + "tone": "partial" + }, + { + "stage": "Full Stable Exit", + "title": "Move the vault fully defensive", + "copy": "A deeper breach exits the risk bucket and keeps the hot wallet on one stable rail.", + "tone": "full" + }, + { + "stage": "Auto Re-entry", + "title": "Re-risk only after hysteresis clears", + "copy": "Recovery must clear a separate band and cooldown before exposure comes back.", + "tone": "reentry" + } + ], + "executionTimeline": [ + { + "title": "Step 1 · Partial De-Risk", + "copy": "Intent 815ede7a-6cf0-49fe-939f-262dfa1b6a4c anchored and settled in tx tx-8eb5d881-9efe-4ae5-9cb8-11d7a5d06424.", + "status": "executed" + }, + { + "title": "Step 2 · Full Stable Exit", + "copy": "Intent 27535a6c-6d30-4a06-9e8b-655c620913c8 anchored and settled in tx tx-b713b68d-6d8e-42d3-8ec6-f20e66c0a61f.", + "status": "executed" + }, + { + "title": "Step 3 · Normal", + "copy": "Intent 8205bd55-2e1e-452d-b0ca-5c1889e371d4 anchored and settled in tx tx-41e2236d-9cd7-4f9b-958b-b9de83246025.", + "status": "executed" + } + ], + "auditTrail": [ + { + "title": "INTENT · intent:27535a6c-6d30-4a06-9e8b-655c620913c8", + "copy": "Authorized derisk swap at stage full exit.", + "stamp": "100s" + }, + { + "title": "EXECUTION · execution:tx-b713b68d-6d8e-42d3-8ec6-f20e66c0a61f", + "copy": "Settled full exit. Sold 5,466 and bought 2,106.", + "stamp": "100s" + }, + { + "title": "SNAPSHOT · snapshot:snapshot-ada-recovery", + "copy": "Observed ADA from snapshot-ada-recovery.", + "stamp": "20s" + }, + { + "title": "SNAPSHOT · snapshot:snapshot-usdm-3", + "copy": "Observed USDM from snapshot-usdm-3.", + "stamp": "20s" + }, + { + "title": "INTENT · intent:8205bd55-2e1e-452d-b0ca-5c1889e371d4", + "copy": "Authorized reentry swap at stage normal.", + "stamp": "0s" + }, + { + "title": "EXECUTION · execution:tx-41e2236d-9cd7-4f9b-958b-b9de83246025", + "copy": "Settled normal. Sold 4,440 and bought 5,553.", + "stamp": "0s" + } + ], + "accounts": [ + { + "label": "ADA risk bucket", + "address": "addr_tes..._hot_1", + "balance": "5,553 ADA", + "fiatValue": "$4,255", + "weight": "64.0%", + "role": "Risk asset", + "bucket": "Execution hot" + }, + { + "label": "USDM stable reserve", + "address": "addr_tes..._hot_1", + "balance": "2,391 USDM", + "fiatValue": "$2,391", + "weight": "36.0%", + "role": "Stable reserve", + "bucket": "Execution hot" + } + ], + "portfolioSeries": [ + { + "label": "Watch", + "stage": "watch", + "value": 8775, + "displayValue": "$8,775" + }, + { + "label": "Partial", + "stage": "partial_derisk", + "value": 8542.754226799423, + "displayValue": "$8,543" + }, + { + "label": "Exit", + "stage": "full_exit", + "value": 6831.30402061, + "displayValue": "$6,831" + }, + { + "label": "Recovery", + "stage": "normal", + "value": 6646.407945987443, + "displayValue": "$6,646" + } + ], + "demoFrames": [ + { + "label": "01", + "title": "Watchlist breach detected", + "copy": "ADA slips under its EMA while Pyth freshness and confidence remain healthy enough to authorize a policy action.", + "stage": "watch", + "balance": "$8,775", + "stableRatio": "17.1%", + "reason": "Primary asset drawdown crossed watch threshold" + }, + { + "label": "02", + "title": "Partial de-risk executes", + "copy": "The keeper emits an intent, swaps only the bounded amount needed, and restores the defended stable floor.", + "stage": "partial_derisk", + "balance": "$8,543", + "stableRatio": "55.3%", + "reason": "Primary asset drawdown crossed watch threshold" + }, + { + "label": "03", + "title": "Full stable exit after the second leg down", + "copy": "A deeper price break and thinner asset cushion push the vault into a full defensive configuration on the approved stable route.", + "stage": "full_exit", + "balance": "$6,831", + "stableRatio": "100.0%", + "reason": "Primary asset drawdown crossed watch threshold" + }, + { + "label": "04", + "title": "Auto re-entry restores exposure", + "copy": "Recovery clears the hysteresis band, cooldown expires, and the treasury re-enters risk according to the configured target ratio.", + "stage": "normal", + "balance": "$6,646", + "stableRatio": "36.0%", + "reason": "Cooldown prevents an automatic state transition" + } + ] +} diff --git a/lazer/cardano/guards/source/apps/ui-legacy/index.html b/lazer/cardano/guards/source/apps/ui-legacy/index.html new file mode 100644 index 00000000..51321e13 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui-legacy/index.html @@ -0,0 +1,203 @@ + + + + + + + guards.one + + + + + + +
+ + +
+
+
+

Cardano live · Pyth-backed controls

+

Treasury Control Center

+
+
+
+ +
+
+
+
+
+

Overview

+

Liquid treasury value

+
+ +
+ +
+
+

Current total balance

+
+

+
+
+ + Open runbook + Read spec +
+
+ +
+
+ +
+
+
+

Simulation

+

Demo frame

+
+ +
+ +

+
+
+ +
+
+
+ Frame balance + +
+
+ Stable ratio + +
+
+ Trigger + +
+
+
+
+ +
+
+
+
+

Accounts

+

Hot and cold treasury buckets

+
+
+
+ + + + + + + + + + +
AccountBalanceFiat valueWeight
+
+
+ +
+
+
+
+

Policy

+

Guardrails

+
+
+
+
+ +
+
+
+

Multichain

+

Execution surfaces

+
+
+
+
+
+
+ +
+
+
+
+

Risk ladder

+

Escalation path

+
+
+
+
+ +
+
+
+

Execution

+

Runbook timeline

+
+
+
+
+
+ +
+
+
+

Audit log

+

Deterministic replay trail

+
+
+
+
+
+
+
+ + + + diff --git a/lazer/cardano/guards/source/apps/ui-legacy/styles.css b/lazer/cardano/guards/source/apps/ui-legacy/styles.css new file mode 100644 index 00000000..46c703e9 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui-legacy/styles.css @@ -0,0 +1,881 @@ +:root { + color-scheme: dark; + --bg: #17181d; + --bg-soft: #1c1e25; + --panel: #212329; + --panel-2: #272932; + --panel-3: #1b1d24; + --line: #31343d; + --line-soft: #292c36; + --text: #f4f2ec; + --muted: #9b9ca4; + --muted-strong: #c6c8ce; + --green: #22c55e; + --yellow: #f0bf5f; + --red: #ef6f6c; + --white-chip: #f6f3ee; + --shadow: 0 20px 56px rgba(0, 0, 0, 0.28); +} + +* { + box-sizing: border-box; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + min-height: 100vh; + background: + radial-gradient(circle at top right, rgba(255, 255, 255, 0.06), transparent 30%), + radial-gradient(circle at top left, rgba(255, 255, 255, 0.03), transparent 20%), + linear-gradient(180deg, #18191f 0%, #15161b 100%); + color: var(--text); + font-family: "Manrope", system-ui, sans-serif; +} + +body::before { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent 22%); + opacity: 0.4; +} + +a { + color: inherit; + text-decoration: none; +} + +button, +a { + transition: 160ms ease; +} + +button { + font: inherit; +} + +.app-shell { + display: grid; + grid-template-columns: 308px minmax(0, 1fr); + min-height: 100vh; +} + +.sidebar { + position: sticky; + top: 0; + align-self: start; + min-height: 100vh; + padding: 28px 20px; + border-right: 1px solid var(--line-soft); + background: rgba(22, 24, 31, 0.9); + backdrop-filter: blur(18px); + display: grid; + gap: 18px; +} + +.brand { + display: flex; + align-items: center; + gap: 14px; + padding: 4px 10px 16px; +} + +.brand-mark { + width: 40px; + height: 40px; + border-radius: 14px; + background: linear-gradient(180deg, #fcfaf6, #d8d5cd); + display: inline-flex; + align-items: center; + justify-content: center; + box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06); +} + +.brand-mark span { + width: 18px; + height: 18px; + border: 2px solid #1a1b20; + border-radius: 5px; +} + +.brand-copy { + display: grid; + gap: 2px; +} + +.brand-copy strong { + font-size: 1.15rem; + letter-spacing: -0.04em; +} + +.brand-copy small, +.side-label, +.workspace-kicker, +.panel-eyebrow, +.eyebrow, +.audit-stamp, +.frame-label, +.ladder-stage, +.metric-top span, +.card-top span, +.summary-block span, +.account-cell span, +.workspace-balance-row span, +.workspace-meta-grid span, +.workspace-foot p, +th { + font-family: "IBM Plex Mono", monospace; +} + +.brand-copy small, +.side-label, +.workspace-kicker, +.panel-eyebrow, +.eyebrow, +.audit-stamp, +.frame-label, +.ladder-stage, +.metric-top span, +.card-top span, +.summary-block span, +.account-cell span, +.workspace-balance-row span, +.workspace-meta-grid span, +.workspace-foot p { + color: var(--muted); + font-size: 0.72rem; + letter-spacing: 0.06em; + text-transform: uppercase; +} + +.panel, +.panel-soft { + background: linear-gradient(180deg, rgba(35, 37, 45, 0.96), rgba(29, 31, 38, 0.96)); + border: 1px solid var(--line); + border-radius: 24px; + box-shadow: var(--shadow); +} + +.panel-soft { + padding: 18px; + box-shadow: none; +} + +.workspace-card { + display: grid; + gap: 16px; +} + +.workspace-head { + display: flex; + align-items: center; + gap: 14px; +} + +.workspace-avatar { + width: 52px; + height: 52px; + border-radius: 16px; + background: linear-gradient(180deg, #f8f5ef, #d5d2cb); + color: #17181d; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 800; + letter-spacing: -0.04em; +} + +.workspace-head strong, +.workspace-balance-row strong, +.workspace-meta-grid strong, +.workspace-balance, +.balance-value, +.metric-card strong, +.policy-card strong, +.chain-card strong, +.ladder-card strong, +.summary-block strong, +.account-cell strong, +.timeline-item strong, +.audit-item strong, +.topbar h1, +.balance-panel h2, +.chart-panel h2, +.panel h2 { + letter-spacing: -0.04em; +} + +.workspace-balance-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 0 10px; + border-top: 1px solid var(--line-soft); + border-bottom: 1px solid var(--line-soft); +} + +.workspace-balance-row strong { + font-size: 1.8rem; +} + +.workspace-meta-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; +} + +.workspace-meta-grid article { + padding: 12px; + border-radius: 16px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--line-soft); + display: grid; + gap: 6px; +} + +.workspace-foot { + display: grid; + gap: 8px; +} + +.workspace-foot p { + margin: 0; +} + +.sidebar-nav { + display: grid; + gap: 10px; +} + +.nav-item { + min-height: 52px; + padding: 0 16px; + border-radius: 18px; + border: 1px solid transparent; + display: flex; + align-items: center; + color: var(--muted-strong); + background: transparent; + font-weight: 600; +} + +.nav-item:hover, +.nav-item:focus-visible, +.nav-item.active { + background: var(--panel-3); + border-color: var(--line); + color: var(--text); +} + +.sidebar-foot { + align-self: end; +} + +.doc-links { + display: grid; + gap: 10px; +} + +.doc-links a { + padding: 12px 14px; + border-radius: 14px; + border: 1px solid var(--line-soft); + background: rgba(255, 255, 255, 0.03); + color: var(--muted-strong); +} + +.doc-links a:hover, +.doc-links a:focus-visible { + color: var(--text); + border-color: #484b56; +} + +.workspace { + padding: 24px 28px 36px; +} + +.topbar { + display: flex; + justify-content: space-between; + align-items: start; + gap: 18px; + margin-bottom: 22px; +} + +.eyebrow { + margin: 0 0 10px; +} + +.topbar h1 { + margin: 0; + font-size: clamp(2.15rem, 3.8vw, 3.5rem); +} + +.topbar-chips { + display: flex; + flex-wrap: wrap; + justify-content: end; + gap: 12px; +} + +.status-chip, +.status-pill, +.chip { + display: inline-flex; + align-items: center; + gap: 10px; + border-radius: 16px; + border: 1px solid var(--line); +} + +.status-chip { + min-height: 52px; + padding: 0 18px; + background: var(--panel-3); +} + +.status-chip strong { + font-size: 1rem; +} + +.status-chip span { + color: var(--muted); + font-size: 0.8rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.status-pill, +.chip { + min-height: 34px; + padding: 0 12px; + background: rgba(255, 255, 255, 0.04); + font-size: 0.84rem; + font-weight: 700; +} + +.status-pill.subtle { + background: var(--panel-3); +} + +.tone-live::before, +.status-pill::before, +.mini-dot { + content: ""; + width: 8px; + height: 8px; + border-radius: 999px; + background: currentColor; + display: inline-block; +} + +.tone-live { + color: var(--green); +} + +.tone-neutral { + color: var(--muted-strong); +} + +.tone-ok { + color: var(--green); +} + +.tone-warn { + color: var(--yellow); +} + +.tone-danger { + color: var(--red); +} + +.content { + display: grid; + gap: 20px; +} + +.overview-grid, +.content-grid, +.split-grid { + display: grid; + gap: 20px; +} + +.overview-grid { + grid-template-columns: 1.15fr 1fr; +} + +.content-grid { + grid-template-columns: 1.2fr 0.88fr; +} + +.split-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.side-stack { + display: grid; + gap: 20px; +} + +.panel { + padding: 24px; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: start; + gap: 18px; + margin-bottom: 22px; +} + +.panel-header.compact { + margin-bottom: 18px; +} + +.panel-header h2, +.balance-panel h2, +.chart-panel h2 { + margin: 0; + font-size: clamp(1.35rem, 2.4vw, 2rem); +} + +.panel-copy, +.balance-copy, +.metric-card p, +.policy-card p, +.chain-card p, +.ladder-card p, +.timeline-item p, +.audit-item p { + margin: 0; + color: var(--muted); + line-height: 1.6; +} + +.balance-row { + display: flex; + justify-content: space-between; + align-items: end; + gap: 20px; + padding-bottom: 22px; + border-bottom: 1px solid var(--line-soft); +} + +.balance-label { + margin: 0 0 8px; + color: var(--muted); +} + +.balance-value { + font-size: clamp(2.8rem, 5vw, 4.6rem); + font-weight: 800; + line-height: 0.95; +} + +.action-row { + display: flex; + flex-wrap: wrap; + justify-content: end; + gap: 12px; +} + +.button { + min-height: 48px; + padding: 0 18px; + border-radius: 16px; + border: 1px solid transparent; + display: inline-flex; + align-items: center; + justify-content: center; + font-weight: 700; + cursor: pointer; +} + +.button-primary { + background: var(--white-chip); + color: #17181d; +} + +.button-secondary { + background: rgba(255, 255, 255, 0.03); + border-color: var(--line); + color: var(--text); +} + +.button:hover, +.button:focus-visible { + transform: translateY(-1px); + border-color: #555966; +} + +.metric-grid, +.policy-grid, +.chain-list, +.ladder-list, +.timeline-list, +.audit-list { + display: grid; + gap: 14px; +} + +.metric-grid { + margin-top: 22px; + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.metric-card, +.policy-card, +.chain-card, +.ladder-card, +.timeline-item, +.audit-item { + padding: 18px; + border-radius: 18px; + border: 1px solid var(--line-soft); + background: rgba(255, 255, 255, 0.03); +} + +.metric-card { + min-height: 142px; + display: grid; + align-content: space-between; + gap: 10px; +} + +.metric-card strong { + font-size: 1.55rem; +} + +.metric-top, +.card-top { + display: flex; + justify-content: space-between; + align-items: center; + gap: 12px; +} + +.policy-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); +} + +.policy-card, +.chain-card, +.ladder-card, +.timeline-item, +.audit-item { + display: grid; + gap: 10px; +} + +.policy-card strong, +.chain-card strong, +.ladder-card strong, +.timeline-item strong, +.audit-item strong { + font-size: 1.06rem; +} + +.frame-strip { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + margin-bottom: 18px; +} + +.frame-pill { + width: 100%; + padding: 14px; + border-radius: 18px; + border: 1px solid var(--line-soft); + background: rgba(255, 255, 255, 0.03); + color: inherit; + text-align: left; + display: grid; + gap: 6px; + cursor: pointer; +} + +.frame-pill strong { + font-size: 0.96rem; + letter-spacing: -0.03em; +} + +.frame-pill small { + font-size: 0.78rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.frame-pill.active, +.frame-pill:hover, +.frame-pill:focus-visible { + border-color: #5a5e69; + background: rgba(255, 255, 255, 0.06); +} + +.chart-shell { + min-height: 280px; + border-radius: 22px; + border: 1px solid var(--line-soft); + background: + radial-gradient(circle at top center, rgba(255, 255, 255, 0.05), transparent 36%), + linear-gradient(180deg, rgba(255, 255, 255, 0.03), rgba(255, 255, 255, 0.01)); + padding: 8px; + overflow: hidden; +} + +#portfolio-chart { + width: 100%; + height: 280px; + display: block; +} + +.chart-grid-line { + stroke: rgba(255, 255, 255, 0.1); + stroke-width: 1; +} + +.chart-area { + fill: url(#chartAreaGradient); +} + +.chart-line { + fill: none; + stroke: rgba(255, 255, 255, 0.9); + stroke-width: 2.6; + stroke-linecap: round; + stroke-linejoin: round; +} + +.chart-point { + stroke: #17181d; + stroke-width: 3; +} + +.chart-point.tone-ok, +.chart-point.active.tone-ok { + fill: var(--green); +} + +.chart-point.tone-warn, +.chart-point.active.tone-warn { + fill: var(--yellow); +} + +.chart-point.tone-danger, +.chart-point.active.tone-danger { + fill: var(--red); +} + +.chart-label, +.chart-value { + fill: var(--muted-strong); + font-family: "IBM Plex Mono", monospace; +} + +.chart-label { + font-size: 11px; +} + +.chart-bubble rect { + fill: rgba(255, 255, 255, 0.08); + stroke: rgba(255, 255, 255, 0.16); +} + +.chart-value { + font-size: 12px; +} + +.demo-summary { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 14px; + margin-top: 18px; +} + +.summary-block { + padding: 16px; + border-radius: 18px; + border: 1px solid var(--line-soft); + background: rgba(255, 255, 255, 0.03); + display: grid; + gap: 10px; +} + +.summary-block strong { + font-size: 1.18rem; +} + +.summary-reason strong { + font-size: 0.96rem; + line-height: 1.5; +} + +.table-wrap { + overflow-x: auto; +} + +.table-wrap table { + width: 100%; + border-collapse: collapse; +} + +th, +td { + text-align: left; + padding: 14px 10px; + border-bottom: 1px solid var(--line-soft); +} + +th { + color: var(--muted); + font-size: 0.72rem; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.account-cell { + display: grid; + gap: 6px; +} + +.account-cell strong { + font-size: 1rem; +} + +.ladder-list { + grid-template-columns: repeat(5, minmax(0, 1fr)); +} + +.ladder-card { + min-height: 188px; + align-content: start; + opacity: 0.62; +} + +.ladder-card.is-active { + opacity: 1; + border-color: #565a67; + background: rgba(255, 255, 255, 0.05); +} + +.ladder-card.ladder-partial { + border-top: 1px solid rgba(240, 191, 95, 0.45); +} + +.ladder-card.ladder-full { + border-top: 1px solid rgba(239, 111, 108, 0.45); +} + +.ladder-card.ladder-reentry { + border-top: 1px solid rgba(34, 197, 94, 0.45); +} + +.timeline-item, +.audit-item { + position: relative; +} + +.timeline-item.is-active, +.audit-item:hover { + border-color: #565a67; + background: rgba(255, 255, 255, 0.05); +} + +.timeline-item.is-complete { + opacity: 0.72; +} + +.chip { + font-size: 0.74rem; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +.chip-live { + color: var(--green); +} + +.chip-warn { + color: var(--yellow); +} + +@media (max-width: 1400px) { + .overview-grid, + .content-grid, + .split-grid { + grid-template-columns: 1fr; + } + + .ladder-list { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 1080px) { + .app-shell { + grid-template-columns: 1fr; + } + + .sidebar { + position: static; + min-height: auto; + border-right: 0; + border-bottom: 1px solid var(--line-soft); + } + + .metric-grid, + .policy-grid, + .frame-strip, + .demo-summary { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (max-width: 720px) { + .workspace { + padding: 18px; + } + + .sidebar { + padding: 18px; + } + + .topbar, + .balance-row, + .panel-header { + flex-direction: column; + align-items: stretch; + } + + .topbar-chips, + .action-row { + justify-content: stretch; + } + + .metric-grid, + .policy-grid, + .frame-strip, + .demo-summary, + .ladder-list, + .workspace-meta-grid { + grid-template-columns: 1fr; + } + + .status-chip, + .button, + .nav-item { + width: 100%; + } + + .balance-value { + font-size: 2.8rem; + } +} diff --git a/lazer/cardano/guards/source/apps/ui/.gitignore b/lazer/cardano/guards/source/apps/ui/.gitignore new file mode 100644 index 00000000..a35d14fd --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/.gitignore @@ -0,0 +1,4 @@ +.next/ +node_modules/ +out/ +*.tsbuildinfo diff --git a/lazer/cardano/guards/source/apps/ui/app/dashboard/page.tsx b/lazer/cardano/guards/source/apps/ui/app/dashboard/page.tsx new file mode 100644 index 00000000..dc4172e8 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/app/dashboard/page.tsx @@ -0,0 +1,299 @@ +"use client"; + +import { useState } from "react"; +import { motion, AnimatePresence } from "framer-motion"; +import { Sidebar } from "@/components/sidebar"; +import { Topbar } from "@/components/topbar"; +import { MetricCard } from "@/components/metric-card"; +import { AccountsTable } from "@/components/accounts-table"; +import { RiskLadder } from "@/components/risk-ladder"; +import { PolicyCards } from "@/components/policy-cards"; +import { ExecutionTimeline } from "@/components/execution-timeline"; +import { VaultProfilePanel } from "@/components/vault-profile-panel"; +import { SwapPanel } from "@/components/swap-panel"; +import { SimulationReplay } from "@/components/simulation-replay"; +import { AuditLog } from "@/components/audit-log"; +import { + PreprodWarningBanner, + PreprodWarningModal, +} from "@/components/preprod-warning"; +import { VaultBootstrapLab } from "@/components/vault-bootstrap-lab"; +import { ScenarioLab } from "@/components/scenario-lab"; +import { HistoricalStrategyLab } from "@/components/historical-strategy-lab"; +import { + RuntimeControlPanel, + type DashboardMode, +} from "@/components/runtime-control-panel"; +import type { RiskLadderStep } from "@/lib/types"; +import { demoState } from "@/lib/demo-data"; +import { + connectPreferredWallet, + type WalletSession, +} from "@/lib/wallet-session"; +import { + mockDatasetOptions, + type MockDatasetId, +} from "@/lib/mock-backtest"; +import { + buildBootstrapDraft, + buildPolicyViewFromDraft, +} from "@/lib/vault-lab"; + +const sectionTransition = { + initial: { opacity: 0, y: 20 }, + animate: { opacity: 1, y: 0 }, + exit: { opacity: 0, y: -10 }, + transition: { duration: 0.35, ease: "easeOut" as const }, +}; + +export default function Dashboard() { + const [activeSection, setActiveSection] = useState("overview"); + const [mode, setMode] = useState("mock"); + const [dataset, setDataset] = useState("ada_treasury_base"); + const [walletSession, setWalletSession] = useState(null); + const [connectingWallet, setConnectingWallet] = useState(false); + const [bootstrapDraft, setBootstrapDraft] = useState(() => + buildBootstrapDraft(demoState.policy), + ); + + const data = demoState; + const policyView = buildPolicyViewFromDraft(bootstrapDraft); + const datasetLabel = + mockDatasetOptions.find((option) => option.id === dataset)?.label ?? dataset; + const stablePosition = data.positions.find((p) => p.role === "stable"); + const riskPosition = data.positions.find((p) => p.role === "risk"); + const ladderStep: RiskLadderStep = data.vault.ladderStep ?? data.vault.stage; + + async function handleConnectWallet() { + if (connectingWallet) { + return; + } + + if (data.vault.chain === "evm") { + if (typeof window !== "undefined") { + window.alert( + "EVM wallet connect is not enabled in this UI yet. Use Cardano or SVM mode for the current demo.", + ); + } + return; + } + + setConnectingWallet(true); + try { + const chain = data.vault.chain === "svm" ? "svm" : "cardano"; + const session = await connectPreferredWallet(chain); + setWalletSession(session); + } finally { + setConnectingWallet(false); + } + } + + return ( +
+ + {/* Ambient background gradient */} +
+ + + +
+ setWalletSession(null)} + /> +
+ +
+
+ +
+ + + {/* Overview */} + {(activeSection === "overview" || activeSection === "accounts") && ( + + {activeSection === "overview" && ( + <> + {/* Hero Metrics */} +
+ + + 700 + ? "red" + : data.metrics.drawdownBps > 300 + ? "yellow" + : "default" + } + /> + +
+ + {/* Oracle Details */} + +
+
+
+

+ Oracle Feed +

+

+ {data.oracle.feedId} +

+
+ + + + + + Live + +
+
+ {[ + { label: "Spot Price", value: `$${data.oracle.price.toFixed(4)}` }, + { label: "EMA Price", value: `$${data.oracle.emaPrice.toFixed(4)}` }, + { label: "Confidence", value: `±$${data.oracle.confidence.toFixed(4)}` }, + { label: "Symbol", value: data.oracle.symbol, accent: true }, + ].map((item) => ( +
+

{item.label}

+

+ {item.value} +

+
+ ))} +
+ + + )} + + + + )} + + {/* Policy */} + {activeSection === "policy" && ( + + + + + )} + + {/* Risk Ladder */} + {activeSection === "risk" && ( + + {mode === "mock" ? ( + + ) : ( + + )} + + + + )} + + {/* Execution */} + {activeSection === "execution" && ( + + + + + )} + + {/* Swap */} + {activeSection === "swap" && ( + + + + )} + + {/* Audit */} + {activeSection === "audit" && ( + + + + )} + +
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/app/globals.css b/lazer/cardano/guards/source/apps/ui/app/globals.css new file mode 100644 index 00000000..4d34cb75 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/app/globals.css @@ -0,0 +1,227 @@ +@import "tailwindcss"; + +@theme { + /* Guards Design System — Ultra Premium */ + --color-bg: #070612; + --color-bg-soft: #0c0b18; + --color-panel: #0f0e1c; + --color-panel-hover: #14132a; + --color-panel-active: #1a1833; + --color-surface: #121120; + --color-line: #1e1d30; + --color-line-soft: #16152a; + + --color-text: #f0eef8; + --color-text-secondary: #9896aa; + --color-text-muted: #5e5c72; + + --color-accent: #7c6ff7; + --color-accent-hover: #9b8fff; + --color-accent-muted: #7c6ff718; + --color-accent-dim: #7c6ff70c; + + --color-blue: #3b82f6; + --color-blue-muted: #3b82f618; + + --color-green: #22c55e; + --color-green-muted: #22c55e14; + --color-yellow: #f0bf5f; + --color-yellow-muted: #f0bf5f14; + --color-red: #ef6f6c; + --color-red-muted: #ef6f6c14; + + --font-sans: "Manrope", system-ui, sans-serif; + --font-mono: "IBM Plex Mono", "SF Mono", monospace; +} + +@layer base { + * { + border-color: var(--color-line); + } + + body { + background-color: var(--color-bg); + color: var(--color-text); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: "cv02", "cv03", "cv04", "cv11"; + } + + ::-webkit-scrollbar { + width: 5px; + height: 5px; + } + ::-webkit-scrollbar-track { + background: transparent; + } + ::-webkit-scrollbar-thumb { + background: #1e1d30; + border-radius: 3px; + } + ::-webkit-scrollbar-thumb:hover { + background: #7c6ff730; + } +} + +/* === Glass Panels === */ +.glass-panel { + background: linear-gradient(135deg, rgba(15, 14, 28, 0.85), rgba(12, 11, 24, 0.9)); + backdrop-filter: blur(32px); + border: 1px solid var(--color-line); + border-radius: 1.25rem; +} +.glass-panel:hover { + border-color: #7c6ff718; +} + +.glass-sidebar { + background: linear-gradient(180deg, rgba(12, 11, 24, 0.95), rgba(7, 6, 18, 0.98)); + backdrop-filter: blur(48px); + border-right: 1px solid var(--color-line); +} + +/* === Chips === */ +.chip { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.7rem; + font-weight: 500; + font-family: var(--font-mono); + letter-spacing: 0.03em; +} +.chip-accent { + display: inline-flex; + align-items: center; + gap: 0.375rem; + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.7rem; + font-weight: 500; + font-family: var(--font-mono); + letter-spacing: 0.03em; + background: var(--color-accent-muted); + color: var(--color-accent); +} +.chip-blue { + display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.25rem 0.75rem; + border-radius: 9999px; font-size: 0.7rem; font-weight: 500; + font-family: var(--font-mono); letter-spacing: 0.03em; + background: var(--color-blue-muted); color: var(--color-blue); +} +.chip-green { + display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.25rem 0.75rem; + border-radius: 9999px; font-size: 0.7rem; font-weight: 500; + font-family: var(--font-mono); letter-spacing: 0.03em; + background: var(--color-green-muted); color: var(--color-green); +} +.chip-yellow { + display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.25rem 0.75rem; + border-radius: 9999px; font-size: 0.7rem; font-weight: 500; + font-family: var(--font-mono); letter-spacing: 0.03em; + background: var(--color-yellow-muted); color: var(--color-yellow); +} +.chip-red { + display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.25rem 0.75rem; + border-radius: 9999px; font-size: 0.7rem; font-weight: 500; + font-family: var(--font-mono); letter-spacing: 0.03em; + background: var(--color-red-muted); color: var(--color-red); +} + +/* === Typography === */ +.eyebrow { + font-size: 0.65rem; + font-family: var(--font-mono); + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--color-text-muted); +} +.metric-value { + font-size: 1.625rem; + font-weight: 700; + letter-spacing: -0.03em; + color: var(--color-text); +} + +/* === Interactive === */ +.card-hover { + transition: all 250ms cubic-bezier(0.4, 0, 0.2, 1); +} +.card-hover:hover { + background: linear-gradient(135deg, rgba(20, 19, 42, 0.9), rgba(15, 14, 28, 0.9)); + border-color: #7c6ff720; + box-shadow: 0 12px 40px rgba(124, 111, 247, 0.06), 0 2px 12px rgba(0, 0, 0, 0.3); + transform: translateY(-1px); +} + +.nav-item { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.625rem 1rem; + border-radius: 0.875rem; + font-size: 0.85rem; + font-weight: 500; + color: var(--color-text-secondary); + transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + border: 1px solid transparent; +} +.nav-item:hover { + background: var(--color-panel-hover); + color: var(--color-text); + border-color: var(--color-line); +} +.nav-item.active { + background: var(--color-accent-muted); + color: var(--color-accent); + border-color: #7c6ff720; + box-shadow: 0 0 20px rgba(124, 111, 247, 0.08); +} + +.status-dot { + display: inline-block; + width: 0.5rem; + height: 0.5rem; + border-radius: 9999px; +} + +.btn-primary { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1.5rem; + background: linear-gradient(135deg, #7c6ff7, #6366f1); + color: white; + font-size: 0.85rem; + font-weight: 600; + border-radius: 0.875rem; + transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; + box-shadow: 0 4px 16px rgba(124, 111, 247, 0.25); +} +.btn-primary:hover { + background: linear-gradient(135deg, #9b8fff, #818cf8); + box-shadow: 0 6px 24px rgba(124, 111, 247, 0.35); + transform: translateY(-1px); +} + +.btn-ghost { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + color: var(--color-text-secondary); + font-size: 0.85rem; + font-weight: 500; + border-radius: 0.875rem; + transition: all 200ms cubic-bezier(0.4, 0, 0.2, 1); + cursor: pointer; +} +.btn-ghost:hover { + color: var(--color-text); + background: var(--color-panel-hover); +} diff --git a/lazer/cardano/guards/source/apps/ui/app/layout.tsx b/lazer/cardano/guards/source/apps/ui/app/layout.tsx new file mode 100644 index 00000000..ab2fece9 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/app/layout.tsx @@ -0,0 +1,37 @@ +import type { Metadata } from "next"; +import "./globals.css"; + +export const metadata: Metadata = { + title: "GUARDS | Treasury Autopilot", + description: + "Oracle-aware treasury policy enforcement. Protect your DAO treasury with automated risk management.", + icons: { + icon: "/guards-icon.svg", + shortcut: "/guards-icon.svg", + apple: "/guards-icon.svg", + }, +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + + + + {children} + + ); +} diff --git a/lazer/cardano/guards/source/apps/ui/app/page.tsx b/lazer/cardano/guards/source/apps/ui/app/page.tsx new file mode 100644 index 00000000..25bba75b --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/app/page.tsx @@ -0,0 +1,21 @@ +import { Navbar } from "@/components/landing/navbar"; +import { Hero } from "@/components/landing/hero"; +import { HowItWorks } from "@/components/landing/how-it-works"; +import { MultichainSection } from "@/components/landing/multichain-section"; +import { PythBanner } from "@/components/landing/pyth-banner"; +import { CTASection } from "@/components/landing/cta-section"; +import { Footer } from "@/components/landing/footer"; + +export default function LandingPage() { + return ( +
+ + + + + + +
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/accounts-table.tsx b/lazer/cardano/guards/source/apps/ui/components/accounts-table.tsx new file mode 100644 index 00000000..5408b3ab --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/accounts-table.tsx @@ -0,0 +1,96 @@ +import { TreasuryPosition } from "@/lib/types"; + +interface AccountsTableProps { + positions: TreasuryPosition[]; +} + +export function AccountsTable({ positions }: AccountsTableProps) { + const total = positions.reduce((s, p) => s + p.fiatValue, 0); + + return ( +
+
+

Treasury Accounts

+
+ + + + + + + + + + + + {positions.map((pos) => ( + + + + + + + + ))} + + + + + + + + +
AssetRoleBalanceFiat ValueWeight
+
+
+ {pos.symbol.slice(0, 2)} +
+ + {pos.symbol} + +
+
+ + {pos.role === "risk" ? "Hot Risk" : "Stable Reserve"} + + + {pos.amount.toLocaleString()} + + ${pos.fiatValue.toLocaleString()} + +
+
+
+
+ + {(pos.weight * 100).toFixed(0)}% + +
+
+ Total + + ${total.toLocaleString()} + + 100% +
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/audit-log.tsx b/lazer/cardano/guards/source/apps/ui/components/audit-log.tsx new file mode 100644 index 00000000..a5f2f344 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/audit-log.tsx @@ -0,0 +1,60 @@ +import { ExecutionEvent } from "@/lib/types"; + +interface AuditLogProps { + events: ExecutionEvent[]; +} + +function timeFormat(timestamp: number) { + return new Date(timestamp).toLocaleString("en-US", { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + }); +} + +export function AuditLog({ events }: AuditLogProps) { + return ( +
+
+

Audit Log

+

+ Immutable execution record with oracle evidence +

+
+
+ {events.map((evt) => ( +
+
+ +
+
+

+ + {evt.kind === "derisk_swap" ? "De-Risk" : "Re-Entry"} + {" "} + — {evt.amount.toLocaleString()} {evt.sourceSymbol} → {evt.destinationSymbol} +

+

+ Stage: {evt.stage} · Route: {evt.route} · {evt.id} +

+
+ + {timeFormat(evt.timestamp)} + +
+ ))} +
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/execution-timeline.tsx b/lazer/cardano/guards/source/apps/ui/components/execution-timeline.tsx new file mode 100644 index 00000000..b34bde88 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/execution-timeline.tsx @@ -0,0 +1,114 @@ +import { ExecutionEvent } from "@/lib/types"; + +interface ExecutionTimelineProps { + events: ExecutionEvent[]; + referenceNowMs: number; +} + +function statusChip(status: ExecutionEvent["status"]) { + switch (status) { + case "executed": + return Executed; + case "pending": + return Pending; + case "failed": + return Failed; + } +} + +function kindLabel(kind: ExecutionEvent["kind"]) { + return kind === "derisk_swap" ? "De-Risk Swap" : "Re-Entry Swap"; +} + +function timeAgo(timestamp: number, referenceNowMs: number) { + const diff = referenceNowMs - timestamp; + const mins = Math.floor(diff / 60000); + if (mins < 1) return "Just now"; + if (mins < 60) return `${mins}m ago`; + const hours = Math.floor(mins / 60); + if (hours < 24) return `${hours}h ago`; + return `${Math.floor(hours / 24)}d ago`; +} + +export function ExecutionTimeline({ + events, + referenceNowMs, +}: ExecutionTimelineProps) { + return ( +
+
+
+

+ Execution Timeline +

+

+ Recent swap executions and pending intents +

+
+ {events.length} events +
+
+ {events.map((evt) => ( +
+ {/* Icon */} +
+ + {evt.kind === "derisk_swap" ? ( + <> + + + + ) : ( + <> + + + + )} + +
+ {/* Details */} +
+
+ + {kindLabel(evt.kind)} + + {statusChip(evt.status)} +
+

+ {evt.amount.toLocaleString()} {evt.sourceSymbol} →{" "} + {evt.destinationSymbol} via {evt.route} +

+
+ {/* Time */} +
+

+ {timeAgo(evt.timestamp, referenceNowMs)} +

+

+ {evt.id} +

+
+
+ ))} +
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/historical-strategy-lab.tsx b/lazer/cardano/guards/source/apps/ui/components/historical-strategy-lab.tsx new file mode 100644 index 00000000..7a858568 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/historical-strategy-lab.tsx @@ -0,0 +1,236 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { Play, TrendingUp } from "lucide-react"; +import { getStageAppearance } from "@/lib/stage"; +import { + mockDatasetOptions, + mockStrategyOptions, + runMockBacktest, + type MockDatasetId, + type MockBacktestPoint, + type MockStrategyId, +} from "@/lib/mock-backtest"; +import type { VaultBootstrapDraft } from "@/lib/vault-lab"; + +interface HistoricalStrategyLabProps { + draft: VaultBootstrapDraft; + dataset: MockDatasetId; +} + +function Chart({ + points, + activeIndex, + setActiveIndex, +}: { + points: MockBacktestPoint[]; + activeIndex: number; + setActiveIndex: (index: number) => void; +}) { + const width = 720; + const height = 180; + const maxPrice = Math.max(...points.map((point) => point.adaPrice)); + const minPrice = Math.min(...points.map((point) => point.adaPrice)); + const range = Math.max(maxPrice - minPrice, 0.01); + const stepX = width / Math.max(points.length - 1, 1); + const line = points + .map((point, index) => { + const normalizedY = height - ((point.adaPrice - minPrice) / range) * height; + return `${index * stepX},${normalizedY}`; + }) + .join(" "); + + return ( + + {[0, 0.25, 0.5, 0.75, 1].map((ratio) => ( + + ))} + + {points.map((point, index) => { + const y = height - ((point.adaPrice - minPrice) / range) * height; + const active = index === activeIndex; + const color = getStageAppearance(point.stage).chartColor; + return ( + setActiveIndex(index)} + /> + ); + })} + + ); +} + +export function HistoricalStrategyLab({ draft, dataset }: HistoricalStrategyLabProps) { + const [strategy, setStrategy] = useState("guards_ladder"); + const [activeIndex, setActiveIndex] = useState(0); + const [autoplay, setAutoplay] = useState(false); + + const result = useMemo( + () => + runMockBacktest(draft, { + strategy, + dataset, + days: 7, + intervalMinutes: 15, + referenceSymbol: draft.referenceSymbol, + }), + [dataset, draft, strategy], + ); + + const activePoint = result.points[activeIndex] ?? result.points[0]; + const appearance = getStageAppearance(activePoint?.stage ?? "normal"); + + useEffect(() => { + if (!autoplay || typeof window === "undefined") { + return undefined; + } + + const timer = window.setInterval(() => { + setActiveIndex((current) => (current + 1) % result.points.length); + }, 220); + + return () => window.clearInterval(timer); + }, [autoplay, result.points.length]); + + return ( +
+
+
+
+

Mock replay lab

+

+ Seven days of 15-minute price states. Execute the strategy engine repeatedly and inspect how stages and swaps evolve. +

+
+
+ 7d + 15m + + Dataset: {mockDatasetOptions.find((option) => option.id === dataset)?.label ?? dataset} + + +
+
+
+ +
+
+
+ {mockStrategyOptions.map((option) => ( + + ))} +
+ +
+ + setActiveIndex(Number(event.target.value))} + className="mt-3 w-full accent-[#7c6ff7]" + /> +
+
+ +
+
+
+
+ + Backtest summary +
+ {appearance.label} +
+
+
+

Executions

+

{result.summary.executionCount}

+
+
+

Min liquid value

+

${result.summary.minLiquidValueFiat.toLocaleString()}

+
+
+

Final stable ratio

+

{(result.summary.finalStableRatio * 100).toFixed(1)}%

+
+
+

Last point

+

{activePoint?.label}

+
+
+
+ +
+
+

Active point

+

ADA ${activePoint?.adaPrice.toFixed(4)}

+

EMA ${activePoint?.adaEmaPrice.toFixed(4)} · confidence ±${activePoint?.adaConfidence.toFixed(4)}

+
+
+

Strategy trigger

+

{activePoint?.trigger}

+
+
+

Execution

+

+ {activePoint?.executionLabel ?? "No execution at this step."} +

+
+
+

Reference asset ({draft.referenceSymbol})

+

${activePoint?.referencePrice.toFixed(2)}

+
+
+
+
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/landing/animations.tsx b/lazer/cardano/guards/source/apps/ui/components/landing/animations.tsx new file mode 100644 index 00000000..480c4953 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/landing/animations.tsx @@ -0,0 +1,90 @@ +"use client"; + +import { motion } from "framer-motion"; +import { ReactNode } from "react"; + +interface BlurInProps { + children: ReactNode; + delay?: number; + duration?: number; + className?: string; +} + +export function BlurIn({ + children, + delay = 0, + duration = 0.6, + className = "", +}: BlurInProps) { + return ( + + {children} + + ); +} + +interface SplitTextProps { + text: string; + className?: string; + wordDelay?: number; + duration?: number; +} + +export function SplitText({ + text, + className = "", + wordDelay = 0.08, + duration = 0.6, +}: SplitTextProps) { + const words = text.split(" "); + return ( + + {words.map((word, i) => ( + + {word} + + ))} + + ); +} + +interface FadeUpProps { + children: ReactNode; + delay?: number; + duration?: number; + className?: string; +} + +export function FadeUp({ + children, + delay = 0, + duration = 0.7, + className = "", +}: FadeUpProps) { + return ( + + {children} + + ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/landing/cta-section.tsx b/lazer/cardano/guards/source/apps/ui/components/landing/cta-section.tsx new file mode 100644 index 00000000..afd88d8e --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/landing/cta-section.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { ArrowRight } from "lucide-react"; +import { FadeUp } from "./animations"; + +export function CTASection() { + return ( +
+ {/* Gradient background */} +
+ +
+ +

+ Stop watching your treasury. +
+ + Start protecting it. + +

+
+ + +

+ Define policy bounds once, then let the execution flow react automatically + when the treasury breaches protected thresholds. +

+
+ + + + +
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/landing/footer.tsx b/lazer/cardano/guards/source/apps/ui/components/landing/footer.tsx new file mode 100644 index 00000000..d7d7e1fd --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/landing/footer.tsx @@ -0,0 +1,42 @@ +export function Footer() { + return ( +
+
+
+
+
+ Guards +
+

+ Oracle-aware treasury control plane for policy-driven, bounded execution. +

+
+ + +
+ +
+ Powered by Pyth price feeds. Cardano-first submission with multichain policy design. + guards.one +
+
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/landing/hero.tsx b/lazer/cardano/guards/source/apps/ui/components/landing/hero.tsx new file mode 100644 index 00000000..0e429872 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/landing/hero.tsx @@ -0,0 +1,94 @@ +"use client"; + +import { Sparkles, ArrowRight } from "lucide-react"; +import { BlurIn, SplitText } from "./animations"; +import { VideoBackground } from "./video-background"; + +export function Hero() { + return ( +
+ {/* Video */} + + + {/* Bottom gradient fade */} +
+ + {/* Content */} +
+
+
+ {/* Badge + Heading + Subtitle */} +
+ {/* Badge */} + +
+ + + For DAO and protocol treasuries + +
+
+ + {/* Heading */} +

+ +
+ +
+ + + {" "} + + + +

+ + {/* Subtitle */} + +

+ Guards automates treasury protection with Pyth-driven policy checks + and pre-approved execution bounds before a protected fiat or stable + floor is breached. +

+
+
+ + {/* CTA Buttons */} + + + +
+
+
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/landing/how-it-works.tsx b/lazer/cardano/guards/source/apps/ui/components/landing/how-it-works.tsx new file mode 100644 index 00000000..8430588c --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/landing/how-it-works.tsx @@ -0,0 +1,389 @@ +"use client"; + +import { useState } from "react"; +import { motion, AnimatePresence, useReducedMotion } from "framer-motion"; +import { FadeUp } from "./animations"; + +const steps = [ + { + number: "01", + title: "Observe", + description: + "Read Pyth price, EMA, confidence, and freshness for the protected asset and any reference asset.", + detail: + "The collector polls Pyth oracle feeds and caches snapshots. Every data point is timestamped and validated for freshness before reaching the engine.", + accent: "#7c6ff7", + icon: ( + + + + + + ), + }, + { + number: "02", + title: "Evaluate", + description: + "Evaluate drawdown, liquid value floors, and oracle quality against the treasury policy ladder.", + detail: + "The risk engine computes drawdown (spot vs EMA), checks absolute fiat floors, and validates oracle confidence. The result determines the next stage on the risk ladder.", + accent: "#3b82f6", + icon: ( + + + + + ), + }, + { + number: "03", + title: "Authorize", + description: + "Authorize a bounded execution intent with approved route, max size, and oracle evidence.", + detail: + "Every intent carries snapshot IDs, a reason hash, and expiry. It can only execute through governance-approved routes with capped volume. No open-ended permissions.", + accent: "#22c55e", + icon: ( + + + + + ), + }, + { + number: "04", + title: "Execute", + description: + "Execute automatically inside governance-approved route and volume limits, then record for audit.", + detail: + "The keeper swaps from the bounded execution bucket through DexHunter or the fallback venue. Every result is anchored with oracle evidence for full traceability.", + accent: "#f0bf5f", + icon: ( + + + + ), + }, +]; + +const ladderStages = [ + { + name: "Normal", + trigger: "Drawdown < watch threshold", + action: "Hold allocation, monitor oracle feeds continuously.", + color: "#22c55e", + }, + { + name: "Watch", + trigger: "Drawdown exceeds watch band", + action: "Increase monitoring frequency. Prepare execution routes and validate venue liquidity.", + color: "#f0bf5f", + }, + { + name: "Partial De-Risk", + trigger: "Drawdown exceeds partial threshold", + action: "Swap a portion of the risk asset into the approved stable to rebuild the protected floor.", + color: "#ef6f6c", + }, + { + name: "Full Stable Exit", + trigger: "Floor breached or drawdown critical", + action: "Emergency exit: move all remaining risk exposure into stable. Fiat floor is the priority.", + color: "#ef4444", + }, + { + name: "Frozen", + trigger: "Oracle stale or confidence too wide", + action: "All execution halted. The engine does not trade on unreliable data. Resumes when oracle quality recovers.", + color: "#9896aa", + }, + { + name: "Auto Re-Entry", + trigger: "Recovery confirmed + cooldown elapsed", + action: "Gradually restore risk allocation. Hysteresis prevents oscillation — a single tick is not enough.", + color: "#7c6ff7", + }, +]; + +export function HowItWorks() { + const [activeStep, setActiveStep] = useState(0); + const [activeLadder, setActiveLadder] = useState(null); + const reduceMotion = useReducedMotion(); + + return ( +
+
+ +

+ How It Works +

+

+ Oracle signal. +
+ Policy ladder. +
+ Bounded execution. +

+

+ Guards is not an arbitrary trading bot. Governance defines the policy, + the approved routes, and the volume limits. Automation only executes inside + that envelope. +

+
+ + {/* Interactive Steps */} +
+ {/* Left: Step selector */} +
+ {steps.map((step, i) => ( + setActiveStep(i)} + initial={{ opacity: 0, x: -20 }} + whileInView={{ opacity: 1, x: 0 }} + viewport={{ once: true }} + transition={ + reduceMotion ? { duration: 0 } : { delay: i * 0.1, duration: 0.4 } + } + aria-pressed={activeStep === i} + aria-label={`Select step ${step.number}: ${step.title}`} + className={`w-full text-left p-5 rounded-2xl border transition-all duration-300 cursor-pointer group ${ + activeStep === i + ? "border-white/10 bg-white/[0.04]" + : "border-transparent hover:border-white/5 hover:bg-white/[0.02]" + }`} + > +
+
+ {step.icon} +
+
+
+ + {step.number} + +

+ {step.title} +

+
+

+ {step.description} +

+
+
+
+ ))} +
+ + {/* Right: Active step detail */} +
+ + + {/* Glow */} +
+ +
+
+
+ {steps[activeStep].icon} +
+
+ + Step {steps[activeStep].number} + +

+ {steps[activeStep].title} +

+
+
+ +

+ {steps[activeStep].detail} +

+ + {/* Progress indicator */} +
+ {steps.map((step, i) => ( + + ))} +
+
+ + +
+
+ + {/* Risk Ladder - Expanded */} + +
+
+

+ Risk Ladder +

+

+ Six stages. Zero ambiguity. +

+

+ The risk ladder is a deterministic state machine — not a set of alerts. + Each stage has a precise trigger and a defined action. The engine escalates + and de-escalates automatically based on oracle signals, and freezes execution + when data quality degrades. +

+
+ +
+ {ladderStages.map((stage, i) => ( + + + + + {activeLadder === i && ( + +
+
+
+

+ Trigger +

+

+ {stage.trigger} +

+
+
+

+ Action +

+

+ {stage.action} +

+
+
+
+
+ )} +
+
+ ))} +
+
+
+
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/landing/multichain-section.tsx b/lazer/cardano/guards/source/apps/ui/components/landing/multichain-section.tsx new file mode 100644 index 00000000..dc52c4ac --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/landing/multichain-section.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { motion, useReducedMotion } from "framer-motion"; +import { FadeUp } from "./animations"; + +const chains = [ + { + name: "Cardano", + src: "/chain-cardano.png", + sizeClass: "h-12 md:h-14", + width: 200, + height: 56, + }, + { + name: "Solana", + src: "/chain-solana.png", + sizeClass: "h-12 md:h-14", + width: 200, + height: 56, + }, + { + name: "Ethereum", + src: "/chain-ethereum.svg", + sizeClass: "h-[5.625rem] md:h-[6.5rem]", + width: 220, + height: 72, + }, +]; + +// Duplicate for infinite scroll illusion +const marqueeChains = [...chains, ...chains, ...chains, ...chains]; + +export function MultichainSection() { + const prefersReducedMotion = useReducedMotion(); + const displayedChains = prefersReducedMotion ? chains : marqueeChains; + + return ( +
+
+ +
+

+ Multichain Native +

+

+ One policy engine. +
+ Every chain. +

+
+
+
+ + {/* Logo Carousel */} +
+
+
+ + + {displayedChains.map((chain, i) => ( +
+ {chain.name} +
+ ))} +
+
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/landing/navbar.tsx b/lazer/cardano/guards/source/apps/ui/components/landing/navbar.tsx new file mode 100644 index 00000000..6d59acbf --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/landing/navbar.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { ArrowRight, Menu, X } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; + +const links = [ + { href: "#how-it-works", label: "How It Works" }, + { href: "#multichain", label: "Multichain" }, + { href: "/dashboard", label: "Demo" }, +]; + +export function Navbar() { + const [scrolled, setScrolled] = useState(false); + const [mobileOpen, setMobileOpen] = useState(false); + + useEffect(() => { + const onScroll = () => setScrolled(window.scrollY > 40); + window.addEventListener("scroll", onScroll, { passive: true }); + onScroll(); + return () => window.removeEventListener("scroll", onScroll); + }, []); + + return ( + <> + +
+
+ {/* Logo */} + + Guards + + + {/* Desktop Links */} +
+ {links.map((link) => ( + + {link.label} + + + ))} +
+ + {/* Desktop CTA */} + + + {/* Mobile menu button */} + +
+
+
+ + {/* Mobile menu overlay */} + + {mobileOpen && ( + +
+ {links.map((link, i) => ( + setMobileOpen(false)} + initial={{ opacity: 0, y: 20 }} + animate={{ opacity: 1, y: 0 }} + transition={{ delay: i * 0.1 }} + className="text-white/70 text-2xl font-medium hover:text-white transition-colors" + > + {link.label} + + ))} + + Open Demo + + +
+
+ )} +
+ + ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/landing/pyth-banner.tsx b/lazer/cardano/guards/source/apps/ui/components/landing/pyth-banner.tsx new file mode 100644 index 00000000..0d8539d9 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/landing/pyth-banner.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { motion } from "framer-motion"; +import { FadeUp } from "./animations"; + +export function PythBanner() { + return ( +
+
+
+ +
+ +

+ Powered by +

+
+ + + + + + +

+ Real-time oracle data behind each risk signal, threshold check, and + bounded execution decision. +

+
+
+ +
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/landing/pyth-section.tsx b/lazer/cardano/guards/source/apps/ui/components/landing/pyth-section.tsx new file mode 100644 index 00000000..260e0773 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/landing/pyth-section.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { FadeUp } from "./animations"; + +const signals = [ + { + signal: "price", + usage: "Spot valuation for liquid value calculation", + icon: ( + + + + ), + }, + { + signal: "emaPrice", + usage: "Baseline for drawdown measurement — spot vs exponential moving average", + icon: ( + + + + ), + }, + { + signal: "confidence", + usage: "Widening interval triggers Frozen state — blocks all execution", + icon: ( + + + + + ), + }, + { + signal: "freshness", + usage: "Stale data halts the engine — no blind execution ever", + icon: ( + + + + + ), + }, +]; + +export function PythSection() { + return ( +
+
+ +
+
+ {/* Left: Oracle signals */} + +
+ {signals.map((s, i) => ( + +
+
+ {s.icon} +
+
+

+ {s.signal} +

+

+ {s.usage} +

+
+
+
+ ))} +
+
+ + {/* Right: Copy */} + +
+
+

+ Powered by +

+
+
+ + + PYTH + + +

+ Network +

+
+

+ Oracle data is not decorative. +
+ It drives execution. +

+

+ Every execution intent carries oracle snapshot IDs. Every + decision can be independently verified. Guards doesn't just + read prices — it reasons about data quality, confidence, and + staleness before authorizing a single swap. +

+
+
+
+
+
+ ); +} diff --git a/lazer/cardano/guards/source/apps/ui/components/landing/video-background.tsx b/lazer/cardano/guards/source/apps/ui/components/landing/video-background.tsx new file mode 100644 index 00000000..ee5c412b --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/components/landing/video-background.tsx @@ -0,0 +1,66 @@ +"use client"; + +import type Hls from "hls.js"; +import { useEffect, useRef } from "react"; + +export function VideoBackground() { + const videoRef = useRef(null); + const hlsRef = useRef(null); + + useEffect(() => { + const video = videoRef.current; + if (!video) { + return; + } + + const src = "https://stream.mux.com/s8pMcOvMQXc4GD6AX4e1o01xFogFxipmuKltNfSYza0200.m3u8"; + let mounted = true; + + if (video.canPlayType("application/vnd.apple.mpegurl")) { + video.src = src; + } else { + import("hls.js").then(({ default: Hls }) => { + if (!mounted || !videoRef.current || !Hls.isSupported()) { + return; + } + + const hls = new Hls({ enableWorker: false }); + hlsRef.current = hls; + hls.loadSource(src); + hls.attachMedia(videoRef.current); + }); + } + + return () => { + mounted = false; + + if (hlsRef.current) { + hlsRef.current.destroy(); + hlsRef.current = null; + } + + const currentVideo = videoRef.current; + if (currentVideo) { + currentVideo.pause(); + currentVideo.removeAttribute("src"); + currentVideo.load(); + } + }; + }, []); + + return ( +
diff --git a/lazer/cardano/guards/source/apps/ui/lib/demo-data.ts b/lazer/cardano/guards/source/apps/ui/lib/demo-data.ts index a3e34b5c..23b3ceb1 100644 --- a/lazer/cardano/guards/source/apps/ui/lib/demo-data.ts +++ b/lazer/cardano/guards/source/apps/ui/lib/demo-data.ts @@ -34,9 +34,9 @@ export const demoState: DemoState = { oracle: { feedId: "pyth-ada-usd", symbol: "ADA/USD", - price: 0.45, - emaPrice: 0.47, - confidence: 0.002, + price: 0.25200284, + emaPrice: 0.25131742, + confidence: 0.00005284, publisherCount: 14, updatedAtMs: DEMO_NOW_MS - 2_000, }, diff --git a/lazer/cardano/guards/source/apps/ui/lib/live-oracle.test.ts b/lazer/cardano/guards/source/apps/ui/lib/live-oracle.test.ts new file mode 100644 index 00000000..73cb53a7 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/lib/live-oracle.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it } from "vitest"; +import { demoState } from "./demo-data"; +import { applyLiveOracleToDemoState } from "./live-oracle"; +import type { DemoState, OracleSnapshot } from "./types"; + +const oracle: OracleSnapshot = { + feedId: "pyth-ada-usd", + symbol: "ADA/USD", + price: 0.25, + emaPrice: 0.3, + confidence: 0.001, + updatedAtMs: Date.parse("2026-03-22T19:00:00.000Z"), + publisherCount: 10, +}; + +describe("applyLiveOracleToDemoState", () => { + it("revalues the risk position and portfolio metrics using the live oracle", () => { + const state = applyLiveOracleToDemoState(demoState, oracle); + + const riskPosition = state.positions.find((position) => position.role === "risk"); + const stablePosition = state.positions.find((position) => position.role === "stable"); + + expect(riskPosition?.fiatValue).toBeCloseTo(31_250, 6); + expect(stablePosition?.fiatValue).toBe(37_500); + expect(state.metrics.liquidValue).toBeCloseTo(68_281.25, 6); + expect(state.metrics.stableRatio).toBeCloseTo(37_500 / 68_281.25, 6); + expect(state.metrics.drawdownBps).toBe(1667); + expect(riskPosition?.weight).toBeCloseTo(31_250 / 68_750, 6); + expect(stablePosition?.weight).toBeCloseTo(37_500 / 68_750, 6); + }); + + it("preserves fallback weights when total display value is zero", () => { + const zeroState: DemoState = { + ...demoState, + positions: demoState.positions.map((position) => + position.role === "risk" + ? { ...position, amount: 0, fiatValue: 0, weight: 0 } + : { ...position, fiatValue: 0, weight: 0 }, + ), + }; + + const state = applyLiveOracleToDemoState(zeroState, oracle); + + expect(state.metrics.liquidValue).toBe(0); + expect(state.metrics.stableRatio).toBe(0); + expect(state.positions.every((position) => position.weight === 0)).toBe(true); + }); + + it("still updates oracle metrics when the risk position is missing", () => { + const stateWithoutRisk: DemoState = { + ...demoState, + positions: demoState.positions.filter((position) => position.role !== "risk"), + }; + + const state = applyLiveOracleToDemoState(stateWithoutRisk, oracle); + + expect(state.oracle.price).toBe(oracle.price); + expect(state.metrics.drawdownBps).toBe(1667); + expect(state.metrics.oracleFreshness).toMatch(/ago$/); + }); +}); diff --git a/lazer/cardano/guards/source/apps/ui/lib/live-oracle.ts b/lazer/cardano/guards/source/apps/ui/lib/live-oracle.ts new file mode 100644 index 00000000..e6eed47f --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/lib/live-oracle.ts @@ -0,0 +1,80 @@ +import type { DemoState, OracleSnapshot } from "./types"; + +function formatOracleFreshness(nowMs: number, updatedAtMs: number): string { + const seconds = Math.max(0, Math.round((nowMs - updatedAtMs) / 1000)); + + if (seconds < 60) { + return `${seconds}s ago`; + } + + const minutes = Math.round(seconds / 60); + return `${minutes}m ago`; +} + +function computeDrawdownBps(price: number, emaPrice: number): number { + if (emaPrice <= 0 || price >= emaPrice) { + return 0; + } + + return Math.round(((emaPrice - price) / emaPrice) * 10_000); +} + +export function applyLiveOracleToDemoState( + state: DemoState, + oracle: OracleSnapshot, +): DemoState { + const nowMs = Date.now(); + const stablePosition = state.positions.find((position) => position.role === "stable"); + const riskPosition = state.positions.find((position) => position.role === "risk"); + + if (!riskPosition) { + return { + ...state, + nowMs, + oracle, + metrics: { + ...state.metrics, + oracleFreshness: formatOracleFreshness(nowMs, oracle.updatedAtMs), + drawdownBps: computeDrawdownBps(oracle.price, oracle.emaPrice), + }, + }; + } + + const riskFiatValue = riskPosition.amount * oracle.price; + const stableFiatValue = stablePosition?.fiatValue ?? 0; + const liquidRiskValue = riskFiatValue * (1 - state.policy.haircutBps / 10_000); + const liquidValue = liquidRiskValue + stableFiatValue; + const stableRatio = liquidValue <= 0 ? 0 : stableFiatValue / liquidValue; + const totalDisplayValue = riskFiatValue + stableFiatValue; + + const positions = state.positions.map((position) => { + if (position.role === "risk") { + const fiatValue = position.amount * oracle.price; + return { + ...position, + fiatValue, + weight: totalDisplayValue <= 0 ? 0 : fiatValue / totalDisplayValue, + }; + } + + const fiatValue = position.fiatValue; + return { + ...position, + fiatValue, + weight: totalDisplayValue <= 0 ? 0 : fiatValue / totalDisplayValue, + }; + }); + + return { + ...state, + nowMs, + oracle, + positions, + metrics: { + liquidValue, + stableRatio, + drawdownBps: computeDrawdownBps(oracle.price, oracle.emaPrice), + oracleFreshness: formatOracleFreshness(nowMs, oracle.updatedAtMs), + }, + }; +} diff --git a/lazer/cardano/guards/source/apps/ui/lib/live-prices.test.ts b/lazer/cardano/guards/source/apps/ui/lib/live-prices.test.ts new file mode 100644 index 00000000..67dc9446 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/lib/live-prices.test.ts @@ -0,0 +1,150 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { demoState } from "./demo-data"; +import { + applyLiveQuotesToDemoState, + liveReferencePriceForSymbol, + type LiveQuoteMap, +} from "./live-prices"; +import type { DemoState } from "./types"; + +const nowMs = Date.parse("2026-03-22T19:00:00.000Z"); + +function cloneState(): DemoState { + return { + ...demoState, + positions: demoState.positions.map((position) => ({ ...position })), + oracle: { ...demoState.oracle }, + policy: { ...demoState.policy }, + metrics: { ...demoState.metrics }, + }; +} + +describe("applyLiveQuotesToDemoState", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(nowMs); + }); + + afterEach(() => { + vi.clearAllTimers(); + vi.useRealTimers(); + }); + + it("is a no-op when there is no ADA quote", () => { + const state = cloneState(); + expect(applyLiveQuotesToDemoState(state, {})).toEqual(state); + }); + + it("rejects stale or low-confidence quotes using policy guardrails", () => { + const staleQuote: LiveQuoteMap = { + ada: { + ...stateOracle(), + updatedAtMs: nowMs - demoState.policy.maxStaleUs / 1000 - 1, + }, + }; + const wideConfidenceQuote: LiveQuoteMap = { + ada: { + ...stateOracle(), + confidence: 0.01, + }, + }; + + expect(applyLiveQuotesToDemoState(cloneState(), staleQuote)).toEqual(cloneState()); + expect(applyLiveQuotesToDemoState(cloneState(), wideConfidenceQuote)).toEqual(cloneState()); + }); + + it("recomputes liquid value, stable ratio, and weights when ADA price changes", () => { + const state = applyLiveQuotesToDemoState(cloneState(), { + ada: { + ...stateOracle(), + price: 0.25, + emaPrice: 0.3, + }, + }); + + const riskPosition = state.positions.find((position) => position.role === "risk"); + const stablePosition = state.positions.find((position) => position.role === "stable"); + + expect(riskPosition?.fiatValue).toBeCloseTo(31_250, 6); + expect(riskPosition?.weight).toBeCloseTo(31_250 / 68_750, 6); + expect(stablePosition?.weight).toBeCloseTo(37_500 / 68_750, 6); + expect(state.metrics.liquidValue).toBeCloseTo(68_281.25, 6); + expect(state.metrics.stableRatio).toBeCloseTo(37_500 / 68_281.25, 6); + expect(state.metrics.drawdownBps).toBe(1667); + }); + + it("returns guarded live reference prices by symbol", () => { + const quotes: LiveQuoteMap = { + xau: { + feedId: "pyth-xau-usd", + symbol: "XAU/USD", + price: 4405.12, + emaPrice: 4398, + confidence: 0.35, + updatedAtMs: nowMs - 1_000, + publisherCount: 12, + }, + btc: { + feedId: "pyth-btc-usd", + symbol: "BTC/USD", + price: 84500, + emaPrice: 84400, + confidence: 4, + updatedAtMs: nowMs - 1_000, + publisherCount: 12, + }, + }; + + expect( + liveReferencePriceForSymbol(quotes, "XAU/USD", { + maxStaleUs: demoState.policy.maxStaleUs, + maxConfidenceBps: demoState.policy.maxConfidenceBps, + }), + ).toBeCloseTo(4405.12, 6); + expect( + liveReferencePriceForSymbol(quotes, "BTC/USD", { + maxStaleUs: demoState.policy.maxStaleUs, + maxConfidenceBps: demoState.policy.maxConfidenceBps, + }), + ).toBeCloseTo(84500, 6); + expect( + liveReferencePriceForSymbol(quotes, "EUR/USD", { + maxStaleUs: demoState.policy.maxStaleUs, + maxConfidenceBps: demoState.policy.maxConfidenceBps, + }), + ).toBeUndefined(); + }); + + it("rejects stale reference quotes", () => { + const quotes: LiveQuoteMap = { + xau: { + feedId: "pyth-xau-usd", + symbol: "XAU/USD", + price: 4405.12, + emaPrice: 4398, + confidence: 0.35, + updatedAtMs: nowMs - demoState.policy.maxStaleUs / 1000 - 1, + publisherCount: 12, + }, + }; + + expect( + liveReferencePriceForSymbol(quotes, "XAU/USD", { + maxStaleUs: demoState.policy.maxStaleUs, + maxConfidenceBps: demoState.policy.maxConfidenceBps, + }), + ).toBeUndefined(); + }); +}); + +function stateOracle() { + return { + feedId: "pyth-ada-usd", + symbol: "ADA/USD", + price: 0.252, + emaPrice: 0.251, + confidence: 0.00004, + updatedAtMs: nowMs - 1_000, + publisherCount: 12, + }; +} diff --git a/lazer/cardano/guards/source/apps/ui/lib/live-prices.ts b/lazer/cardano/guards/source/apps/ui/lib/live-prices.ts new file mode 100644 index 00000000..d9349762 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/lib/live-prices.ts @@ -0,0 +1,160 @@ +import type { DemoState, OracleSnapshot } from "./types"; + +export interface LiveQuote extends OracleSnapshot {} + +export interface LiveQuoteMap { + ada?: LiveQuote; + xau?: LiveQuote; + btc?: LiveQuote; + sol?: LiveQuote; + eur?: LiveQuote; +} + +interface OracleGuardrails { + maxStaleUs: number; + maxConfidenceBps: number; +} + +function formatOracleFreshness(nowMs: number, updatedAtMs: number): string { + const seconds = Math.max(0, Math.round((nowMs - updatedAtMs) / 1000)); + if (seconds < 60) { + return `${seconds}s ago`; + } + + const minutes = Math.round(seconds / 60); + return `${minutes}m ago`; +} + +function computeDrawdownBps(price: number, emaPrice: number): number { + if (emaPrice <= 0 || price >= emaPrice) { + return 0; + } + + return Math.round(((emaPrice - price) / emaPrice) * 10_000); +} + +function computeConfidenceBps(price: number, confidence: number): number { + if (!Number.isFinite(price) || price <= 0 || !Number.isFinite(confidence) || confidence < 0) { + return Number.POSITIVE_INFINITY; + } + + return (confidence / price) * 10_000; +} + +function quotePassesGuardrails( + quote: LiveQuote, + guardrails: OracleGuardrails, + nowMs = Date.now(), +): boolean { + const updatedAgoMs = Math.max(0, nowMs - quote.updatedAtMs); + const confidenceBps = computeConfidenceBps(quote.price, quote.confidence); + + if (guardrails.maxStaleUs > 0 && updatedAgoMs > guardrails.maxStaleUs / 1000) { + return false; + } + + if ( + guardrails.maxConfidenceBps > 0 && + confidenceBps > guardrails.maxConfidenceBps + ) { + return false; + } + + return true; +} + +export function applyLiveQuotesToDemoState( + state: DemoState, + quotes: LiveQuoteMap, +): DemoState { + const ada = quotes.ada; + if (!ada) { + return state; + } + + const nowMs = Date.now(); + if (!quotePassesGuardrails(ada, state.policy, nowMs)) { + return state; + } + + const stablePosition = state.positions.find((position) => position.role === "stable"); + const riskPosition = state.positions.find((position) => position.role === "risk"); + + if (!riskPosition) { + return { + ...state, + nowMs, + oracle: ada, + metrics: { + ...state.metrics, + drawdownBps: computeDrawdownBps(ada.price, ada.emaPrice), + oracleFreshness: formatOracleFreshness(nowMs, ada.updatedAtMs), + }, + }; + } + + const stableFiatValue = stablePosition?.fiatValue ?? 0; + const riskFiatValue = riskPosition.amount * ada.price; + const liquidRiskValue = riskFiatValue * (1 - state.policy.haircutBps / 10_000); + const liquidValue = liquidRiskValue + stableFiatValue; + const stableRatio = liquidValue <= 0 ? 0 : stableFiatValue / liquidValue; + const totalDisplayValue = riskFiatValue + stableFiatValue; + + return { + ...state, + nowMs, + oracle: ada, + positions: state.positions.map((position) => { + if (position.role === "risk") { + const fiatValue = position.amount * ada.price; + return { + ...position, + fiatValue, + weight: totalDisplayValue <= 0 ? 0 : fiatValue / totalDisplayValue, + }; + } + + return { + ...position, + weight: totalDisplayValue <= 0 ? 0 : position.fiatValue / totalDisplayValue, + }; + }), + metrics: { + liquidValue, + stableRatio, + drawdownBps: computeDrawdownBps(ada.price, ada.emaPrice), + oracleFreshness: formatOracleFreshness(nowMs, ada.updatedAtMs), + }, + }; +} + +export function liveReferencePriceForSymbol( + quotes: LiveQuoteMap, + symbol: string, + guardrails?: OracleGuardrails, +): number | undefined { + const quote = (() => { + switch (symbol) { + case "XAU/USD": + return quotes.xau; + case "BTC/USD": + return quotes.btc; + case "SOL/USD": + return quotes.sol; + case "EUR/USD": + return quotes.eur; + default: + return undefined; + } + })(); + + if (!quote) { + return undefined; + } + + if (guardrails && !quotePassesGuardrails(quote, guardrails)) { + return undefined; + } + + return quote.price; +} diff --git a/lazer/cardano/guards/source/apps/ui/lib/mock-backtest.ts b/lazer/cardano/guards/source/apps/ui/lib/mock-backtest.ts index 842ae76e..80ae079d 100644 --- a/lazer/cardano/guards/source/apps/ui/lib/mock-backtest.ts +++ b/lazer/cardano/guards/source/apps/ui/lib/mock-backtest.ts @@ -177,14 +177,14 @@ function toLabel(timestampMs: number): string { function referenceBasePrice(referenceSymbol: string): number { switch (referenceSymbol) { case "BTC/USD": - return 87_250; + return 67_846.32774; case "SOL/USD": - return 186; + return 86.2; case "EUR/USD": - return 1.09; + return 1.15578; case "XAU/USD": default: - return 3_025; + return 4_421.412; } } @@ -436,6 +436,7 @@ export function runMockBacktest( adaAmount: holdings.adaAmount, stableAmount: holdings.stableAmount, secondsSinceUpdate: 15, + secondsSinceLastTransition: 900, }; const step = applyExecution(holdings, scenario, draft, resolved.strategy); diff --git a/lazer/cardano/guards/source/apps/ui/lib/vault-lab.ts b/lazer/cardano/guards/source/apps/ui/lib/vault-lab.ts index 18c2e6dd..20cacbe7 100644 --- a/lazer/cardano/guards/source/apps/ui/lib/vault-lab.ts +++ b/lazer/cardano/guards/source/apps/ui/lib/vault-lab.ts @@ -101,25 +101,25 @@ export const referenceAssetOptions: ReferenceAssetOption[] = [ { symbol: "XAU/USD", label: "Gold (XAU/USD)", - defaultPrice: 3025, + defaultPrice: 4421.412, category: "rwa", }, { symbol: "BTC/USD", label: "Bitcoin (BTC/USD)", - defaultPrice: 87250, + defaultPrice: 67846.32774, category: "crypto", }, { symbol: "SOL/USD", label: "Solana (SOL/USD)", - defaultPrice: 186, + defaultPrice: 86.2, category: "crypto", }, { symbol: "EUR/USD", label: "Euro (EUR/USD)", - defaultPrice: 1.09, + defaultPrice: 1.15578, category: "fx", }, ]; @@ -262,7 +262,7 @@ export function buildBootstrapDraft(policy: UiPolicyConfig): VaultBootstrapDraft useReferenceTarget: false, targetOunces: 25, referenceSymbol: "XAU/USD", - referencePrice: 3025, + referencePrice: 4421.412, }; } @@ -336,7 +336,7 @@ export const scenarioPresets: ScenarioPreset[] = [ adaEmaPrice: 0.47, adaConfidence: 0.002, stablePrice: 1, - xauUsd: 3025, + xauUsd: 4421.412, adaAmount: 125000, stableAmount: 37500, secondsSinceUpdate: 2, @@ -354,7 +354,7 @@ export const scenarioPresets: ScenarioPreset[] = [ adaEmaPrice: 0.47, adaConfidence: 0.0024, stablePrice: 1, - xauUsd: 3025, + xauUsd: 4421.412, adaAmount: 125000, stableAmount: 37500, secondsSinceUpdate: 2, @@ -372,7 +372,7 @@ export const scenarioPresets: ScenarioPreset[] = [ adaEmaPrice: 0.47, adaConfidence: 0.0028, stablePrice: 1, - xauUsd: 3025, + xauUsd: 4421.412, adaAmount: 125000, stableAmount: 37500, secondsSinceUpdate: 2, @@ -390,7 +390,7 @@ export const scenarioPresets: ScenarioPreset[] = [ adaEmaPrice: 0.47, adaConfidence: 0.0035, stablePrice: 1, - xauUsd: 3025, + xauUsd: 4421.412, adaAmount: 125000, stableAmount: 37500, secondsSinceUpdate: 2, @@ -408,7 +408,7 @@ export const scenarioPresets: ScenarioPreset[] = [ adaEmaPrice: 0.47, adaConfidence: 0.002, stablePrice: 1, - xauUsd: 3025, + xauUsd: 4421.412, adaAmount: 125000, stableAmount: 37500, secondsSinceUpdate: 45, @@ -426,7 +426,7 @@ export const scenarioPresets: ScenarioPreset[] = [ adaEmaPrice: 0.47, adaConfidence: 0.0018, stablePrice: 1, - xauUsd: 3025, + xauUsd: 4421.412, adaAmount: 55000, stableAmount: 90000, secondsSinceUpdate: 2, diff --git a/lazer/cardano/guards/source/apps/ui/lib/wallet-session.ts b/lazer/cardano/guards/source/apps/ui/lib/wallet-session.ts index 0028313c..2e8eca42 100644 --- a/lazer/cardano/guards/source/apps/ui/lib/wallet-session.ts +++ b/lazer/cardano/guards/source/apps/ui/lib/wallet-session.ts @@ -10,6 +10,8 @@ export interface WalletSession { connectedAtMs: number; } +export const WALLET_SESSION_STORAGE_KEY = "guards-wallet-session"; + const MOCK_WALLET_ADDRESSES: Record = { cardano: "addr_test1qpz7...guards_mock", svm: "6w4F...guardsMockSvm", @@ -78,6 +80,37 @@ export function shortWalletAddress(address: string): string { return `${address.slice(0, 8)}...${address.slice(-6)}`; } +export function hydrateStoredWalletSession(): WalletSession | null { + if (typeof window === "undefined") { + return null; + } + + const raw = window.localStorage.getItem(WALLET_SESSION_STORAGE_KEY); + if (!raw) { + return null; + } + + try { + return JSON.parse(raw) as WalletSession; + } catch { + window.localStorage.removeItem(WALLET_SESSION_STORAGE_KEY); + return null; + } +} + +export function persistWalletSession(session: WalletSession | null): void { + if (typeof window === "undefined") { + return; + } + + if (!session) { + window.localStorage.removeItem(WALLET_SESSION_STORAGE_KEY); + return; + } + + window.localStorage.setItem(WALLET_SESSION_STORAGE_KEY, JSON.stringify(session)); +} + function getCardanoProviders(): Array<[string, Cip30Provider]> { if (typeof window === "undefined") { return []; diff --git a/lazer/cardano/guards/source/apps/ui/next.config.ts b/lazer/cardano/guards/source/apps/ui/next.config.ts index 68a6c64d..73af4aea 100644 --- a/lazer/cardano/guards/source/apps/ui/next.config.ts +++ b/lazer/cardano/guards/source/apps/ui/next.config.ts @@ -2,6 +2,9 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { output: "standalone", + experimental: { + externalDir: true, + }, }; export default nextConfig; diff --git a/lazer/cardano/guards/source/apps/ui/package.json b/lazer/cardano/guards/source/apps/ui/package.json index 1ccea141..71fc9491 100644 --- a/lazer/cardano/guards/source/apps/ui/package.json +++ b/lazer/cardano/guards/source/apps/ui/package.json @@ -4,7 +4,7 @@ "private": true, "type": "module", "engines": { - "node": ">=20.9.0" + "node": ">=24.0.0" }, "scripts": { "dev": "next dev", @@ -12,6 +12,7 @@ "start": "next start" }, "dependencies": { + "@pythnetwork/pyth-lazer-sdk": "^6.2.1", "framer-motion": "^12.38.0", "hls.js": "^1.6.15", "lucide-react": "^0.577.0", diff --git a/lazer/cardano/guards/source/apps/ui/vercel.json b/lazer/cardano/guards/source/apps/ui/vercel.json new file mode 100644 index 00000000..7597fc62 --- /dev/null +++ b/lazer/cardano/guards/source/apps/ui/vercel.json @@ -0,0 +1,7 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "nextjs", + "installCommand": "cd ../.. && pnpm install --frozen-lockfile", + "buildCommand": "pnpm build", + "devCommand": "pnpm dev" +} diff --git a/lazer/cardano/guards/source/docs/vercel-deploy.md b/lazer/cardano/guards/source/docs/vercel-deploy.md new file mode 100644 index 00000000..0cd0747d --- /dev/null +++ b/lazer/cardano/guards/source/docs/vercel-deploy.md @@ -0,0 +1,56 @@ +# Vercel Deploy + +This repo can be deployed to `Vercel` for the operator UI in `apps/ui`. + +## Target + +- App: `apps/ui` +- Framework: `Next.js` +- Runtime baseline: `Node >= 24.0.0` + +## Why the Vercel config lives in `apps/ui` + +The deploy target is the Next app only. The rest of the monorepo contains backend simulation, Cardano off-chain tooling, and contract scaffolding that do not belong in the browser deployment. + +`apps/ui/vercel.json` assumes the Vercel project uses: + +- Root Directory: `apps/ui` + +That lets Vercel treat the UI as the deployable app while still installing the full monorepo from the repository root. + +## Required Vercel project settings + +1. Import the GitHub repository into Vercel. +2. Set `Root Directory` to `apps/ui`. +3. Keep the commands from `apps/ui/vercel.json`: + - Install: `cd ../.. && pnpm install --frozen-lockfile` + - Build: `pnpm build` + - Dev: `pnpm dev` +4. Set Node.js to `24.x`. + +## Why `externalDir` is enabled + +The UI imports shared domain types from `packages/core` via the app `tsconfig` path mapping. `externalDir` allows the Next build to consume those shared files while the app is deployed from the `apps/ui` subdirectory. + +## Current production scope + +This Vercel deployment is for the frontend only: + +- dashboard shell +- demo/replay UI +- local static data embedded in the app + +It does **not** deploy: + +- Cardano keepers +- Pyth live collector jobs +- DexHunter execution services +- SQLite or any deployable backend storage + +Those still need a separate runtime such as `Railway`, `Fly.io`, `Render`, or equivalent. + +## Environment variables + +The current UI does not require wallet or oracle secrets to render. You can deploy the shell without adding the Cardano/Pyth backend secrets to Vercel. + +If future UI releases call live backend APIs, add only the public frontend variables there and keep signing keys/provider secrets in the backend runtime. diff --git a/lazer/cardano/guards/source/pnpm-lock.yaml b/lazer/cardano/guards/source/pnpm-lock.yaml index e2043c7f..06ccb27f 100644 --- a/lazer/cardano/guards/source/pnpm-lock.yaml +++ b/lazer/cardano/guards/source/pnpm-lock.yaml @@ -59,6 +59,9 @@ importers: apps/ui: dependencies: + '@pythnetwork/pyth-lazer-sdk': + specifier: ^6.2.1 + version: 6.2.1 framer-motion: specifier: ^12.38.0 version: 12.38.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)