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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# Pyth Examples

This repository contains examples of applications integrating Pyth products and services.
This repository contains examples of applications integrating Pyth products and services.
32 changes: 32 additions & 0 deletions lazer/cardano/inventory-edge-protocol/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Optional: judge API (default 8787)
# JUDGE_API_PORT=8787

# Pyth Lazer API key
ACCESS_TOKEN=

# 24-word seed for PreProd (Lucid + Evolution txs)
CARDANO_MNEMONIC=

# Lucid mint + Evolution vault txs (adjust / close / liquidar / …) — una de:
# Sin esto, Evolution usa Koios y evaluateTx suele fallar ("Koios evaluateTx failed").
BLOCKFROST_PROJECT_ID=
# MAESTRO_API_KEY=
# Opcional: si solo usás Koios con plan que expone ogmios
# KOIOS_TOKEN=

# Depósitos on-chain al script liquidity_pool (dejar margen para fees)
# POOL_DEPOSIT_RESERVE_LOVELACE=4000000

# After mock-assets, pick one line printed to the console:
# SHADOW_POLICY_ID=
# SHADOW_NAME_HEX=
# Optional: XAU_USD | WTI_USD | BTC_USD
# SHADOW_ASSET=XAU_USD

# Vault demo (openVault)
# VAULT_DEBT_LOVELACE=1000000000
# VAULT_COLLATERAL_QTY=1000000

# applyHedge demo
# HEDGE_STRIKE_RAW=2500000
# HEDGE_PAYOUT_LOVELACE=5000000
6 changes: 6 additions & 0 deletions lazer/cardano/inventory-edge-protocol/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules/
.env
data/
onchain/build/
onchain/.aiken/
web/dist/
51 changes: 51 additions & 0 deletions lazer/cardano/inventory-edge-protocol/PITCH.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Inventory-Edge Protocol — RealFi MVP (Hackathon)

## The missing primitive on Cardano

Institutional **RealFi** needs a standard pattern to (1) represent an inventory or RWA position on-chain, (2) attach **real-time valuation**, and (3) enforce **risk rules** (loan health, insurance strikes) in the same transaction graph as the asset. Cardano’s eUTxO model is excellent for auditable state transitions, but teams still lack a **small, composable vault** that is explicitly designed around **oracle-verified** prices and **Lazer-style** asset metadata—not just a generic “lock tokens” script.

**Inventory-Edge** is that primitive: a single spending validator (`vault`) that:

- Locks a **shadow RWA NFT** (demo mint via Lucid; metadata carries Pyth Lazer feed hints).
- Tracks **synthetic debt** and a **collateral quantity** (off-chain scaling documented next to the datum).
- Supports **Adjust** (owner), **ApplyHedge** (parametric insurance strike / payout fields), **Close** (debt-free unwind), **Liquidate** (Pyth pull model + underwater check), and **ClaimInsurance** (Pyth pull model + strike breach).

Judges can read `onchain/validators/vault.ak` in one sitting: it is intentionally smaller than a production protocol, but it is **not** a toy—it calls `pyth.get_updates` exactly like the audited Pyth Lazer Cardano integration.

## High-frequency risk with Pyth Lazer

We use **Pyth Lazer** off-chain to fetch **Solana-binary** price updates (the format shared with Cardano), then attach a **reference input** to the on-chain **Pyth state** and perform a **zero-lovelace withdrawal** against Pyth’s script so the **payload is verified on-chain** before our vault logic runs.

That is the same pull-model pattern proven in the reference `lazer-rwa` project: the oracle is not “trusted JSON in metadata”; it is **cryptographically checked** in the transaction.

For **liquidation**, we compare integer-rounded collateral value against **110% of debt** using on-chain integer arithmetic (`price * qty * 100 < debt * 110` in the MVP). For **insurance**, `ClaimInsurance` requires the owner’s signature and checks `spot < strike` using the same verified feed.

## Synergy with Lazer-RWA

**Lazer-RWA** (reference: `pyth-examples/lazer/cardano/lazer-rwa`) shows how to align **off-chain streaming**, **datum design**, and **Pyth state resolution** on **PreProd**. We treat that repo as the **design north star**, not a dependency to fork blindly:

- **On-chain**: same `pyth-network/pyth-lazer-cardano` dependency and PreProd **policy id** as the reference integration.
- **Off-chain**: we keep **Lucid** for the **asset demo** (CIP-25 metadata + native mint) where the UX wins, and **Evolution SDK** for **Pyth-withdraw + Plutus v3 spends**, because Lucid 0.10 does not expose Plutus v3 attach paths—an honest trade-off documented here rather than hidden in the code.

The three **shadow** assets (gold / WTI / BTC) map to **Pyth Lazer feed ids** in `lib/feeds.ts` (XAU is aligned with the reference; BTC/WTI are **placeholders** until you confirm ids from the Lazer symbols API—this keeps the demo honest).

## Limitations (MVP scope)

- **Composition**: `Liquidate` and `ClaimInsurance` are implemented as **single transactions** that combine Pyth verification with vault redemption in the Evolution builder. If your cluster parameters or coin-selection edge cases differ, fall back to the two-step flow described in the original sprint plan (verify Pyth on-chain, then spend vault) and keep iterating.
- **Insurance payout**: the on-chain check authorizes the transition when `price < strike`; **exact payout wiring** to outputs is left as a product layer (the datum already stores `payout_lovelace` for demos and future tightening).
- **Lucid vs Evolution**: Lucid is **deprecated** on npm; we still use it for **Preprod Blockfrost mints** because it is the fastest way to get CIP-25 NFTs for judges. Production should migrate mints to Evolution or another maintained stack.

## How to demo (operators)

**Judges:** the scoring surface is the **Aiken vault + blueprint** — see the “For judges” section in [`README.md`](./README.md). The steps below are for live PreProd demos only.

1. `npm run build:onchain`
2. Set `BLOCKFROST_PROJECT_ID` (or `MAESTRO_API_KEY`), `CARDANO_MNEMONIC`, and `ACCESS_TOKEN` in `.env` (see `.env.example`). Skipping Blockfrost/Maestro usually breaks Evolution’s chain simulation (Koios-only is brittle).
3. `npm run mock-assets` → copy `SHADOW_POLICY_ID` / `SHADOW_NAME_HEX` for one NFT.
4. `npm run tx:open-vault` → locks NFT + vault datum at the script.
5. Optional: `npm run tx:hedge` → sets insurance fields.
6. `npm run tx:liquidate` → Pyth payload + `Liquidate` when the vault is underwater.

**Beyond the CLI:** `package.json` does not ship scripts for **Close**, **Adjust**, **ClaimInsurance**, or **pool** actions — those are available from the **judge API + Vite UI** via `npm run demo` (same `.env`), which calls into `lib/transactions.ts` / `lib/pool_onchain.ts`.

This is **PreProd-only** MVP code: do not reuse keys or mnemonics from demos on mainnet.
216 changes: 216 additions & 0 deletions lazer/cardano/inventory-edge-protocol/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
# Inventory-Edge Protocol — Judge Briefing

**Screenshots & UI pages (for judges):** [Google Doc — captures](https://docs.google.com/document/d/10R7zigvUrFPtYojwtqCrn2axnEarXEnd423Ktbk2eNo/edit?usp=sharing) (the same link appears in the [repository root README](../../../README.md) so it shows on the GitHub repo / PR home view.)

RealFi / RWA hackathon MVP: a **Cardano vault** that locks a demo NFT, tracks synthetic debt, supports **parametric insurance** (strike in datum), and uses **Pyth Lazer** (pull model) for **liquidation** and **insurance** checks.

**Narrative deck (why it matters):** see [PITCH.md](./PITCH.md).

**Location in `pyth-examples`:** `lazer/cardano/inventory-edge-protocol/` (from repo root, `cd` into this folder before `npm install`).

### For judges (static review first)

Contract logic is reviewable **without** running the browser demo or holding keys:

| Priority | Artifact | Why it matters |
|----------|----------|----------------|
| **1** | [`onchain/validators/vault.ak`](onchain/validators/vault.ak) | Datum/redeemers, `pyth.get_updates`, liquidation and insurance checks. |
| **2** | [`onchain/plutus.json`](onchain/plutus.json) | Committed blueprint; rebuild with `npm run build:onchain` if you want to verify it matches sources. |
| **3** | [`onchain/validators/liquidity_pool.ak`](onchain/validators/liquidity_pool.ak) | tADA pool validator used with the pool APIs / UI. |
| **4** | [`PITCH.md`](./PITCH.md) | Product story, honest limits, alignment with Pyth Lazer examples. |

**Optional, no secrets:** `npm install` → `npm run build:onchain` (needs [Aiken](https://aiken-lang.org) **1.1+**; first build may need network to fetch the git dependency in `onchain/aiken.toml`) → `npx tsc` (typechecks `lib/`, `scripts/`, `server/` only; the Vite app under `web/` is separate).

The **judge UI** (`npm run demo`) is for operator demos only; it is **not** required to score the on-chain design.

---

## 1. What on-chain contracts did we ship?

| Artifact | Role |
|----------|------|
| **`vault` (Aiken)** | Single Plutus **v3** spending validator: [`onchain/validators/vault.ak`](onchain/validators/vault.ak). |
| **`vault.vault.spend`** | Entry judges should read: all business logic (datum/redeemer, Pyth, NFT continuity). |
| **`vault.vault.else`** | Aiken-generated companion artifact in [`onchain/plutus.json`](onchain/plutus.json) (same validator hash; not a separate product contract). |

**Compiled blueprint:** `onchain/plutus.json` (generated by `aiken build`).

**Validator script hash (blake2b-224, hex)** — used for the vault **enterprise** address on **PreProd** (network id 0):

```text
39e330f0020708cecb4cf7dd09c3912000c9f30b31c80cb7c2ad21a1
```

We do **not** deploy a separate Pyth contract. We integrate the **existing Pyth Lazer deployment on Cardano PreProd**:

| Constant | Value |
|----------|--------|
| **Pyth Lazer governance `PolicyId` (PreProd, hex)** | `d799d287105dea9377cdf9ea8502a83d2b9eb2d2050a8aea800a21e6` |

That is the same **PreProd governance policy** used by the official Pyth Lazer Cardano integration in [`pyth-network/pyth-lazer-cardano`](https://github.com/pyth-network/pyth-lazer-cardano) (this example depends on that library via `onchain/aiken.toml`). For product docs, see [Pyth Lazer](https://docs.pyth.network/lazer).

**Shadow NFTs** are **native assets** minted off-chain (Lucid + signing policy), **not** minted by this Aiken project. Metadata is **CIP-25** label `721` with fields such as `pyth_lazer_feed_id` and `inventory_edge_class` (see `scripts/mock_assets.ts`).

---

## 2. On-chain state machine (auditor view)

### Datum — `VaultDatum`

| Field | Meaning |
|-------|---------|
| `owner` | `VerificationKeyHash` — must sign owner-only actions. |
| `pyth_policy` | Pyth Lazer policy id (PreProd constant above). |
| `nft_policy` / `nft_name` | Locked “shadow” NFT. |
| `debt_lovelace` | Synthetic debt (demo integer, lovelace units). |
| `collateral_qty` | Integer chosen off-chain so `price_raw * qty` is comparable to debt. |
| `feed_id` | Pyth Lazer numeric feed id (e.g. **346** = XAU/USD in our demo). |
| `hedge` | `Option HedgeParams { strike_raw, payout_lovelace }` for parametric insurance. |

### Redeemer — `VaultRedeemer`

| Variant | Who | Pyth required? | Summary |
|---------|-----|----------------|---------|
| `Adjust { new_debt }` | Owner | No | Updates debt; NFT must stay in continuing vault output. |
| `ApplyHedge { strike_raw, payout_lovelace }` | Owner | No | Sets `hedge` to `Some`; NFT preserved. |
| `Close` | Owner | No | `debt_lovelace == 0`, no continuing vault output. |
| `Liquidate` | Anyone | **Yes** | `pyth.get_updates`; underwater if `price * collateral_qty * 100 < debt_lovelace * 110`. |
| `ClaimInsurance` | Owner | **Yes** | Requires `hedge` = `Some` and `price < strike_raw`. |

**Pull model:** `Liquidate` / `ClaimInsurance` call `pyth.get_updates(pyth_policy, tx)` which expects the transaction to include the **Pyth state reference input** and the **zero-withdrawal** witness with the **Solana-format** update payload (same pattern as the official Cardano + Lazer examples).

---

## 3. Off-chain stack (why two libraries)

| Layer | Tech | Used for |
|-------|------|----------|
| Mint + metadata | **Lucid** + **Blockfrost** (or **Maestro**) | `npm run mock-assets` or judge UI mint — PreProd NFT demo. |
| Vault txs + Pyth witness | **Evolution SDK** + **Blockfrost** or **Maestro** (same priority as Lucid; **Koios** only if neither is available) | `openVault`, `applyHedge`, `adjustDebt`, `closeVault`, `liquidate`, `claimInsurance` in [`lib/transactions.ts`](lib/transactions.ts). |
| Liquidity pool (tADA) | **Aiken** [`onchain/validators/liquidity_pool.ak`](onchain/validators/liquidity_pool.ak) + **Lucid** (deposit) + **Evolution** (Plutus v3 withdraw) [`lib/pool_onchain.ts`](lib/pool_onchain.ts) | Datum locks to your payment key hash (only you can spend). API: `POST /api/pool/onchain/deposit-percent`, `GET /api/pool/onchain/positions`, `POST /api/pool/onchain/withdraw-all`. |
| Judge UI | **Vite + React** + **Express** (local API) | `npm run demo` — mint, vault, pool balances read from chain (script + vault datums), hedge, Pyth risk, liquidate / claim. Optional local audit: `data/judge_audit.json`. |
| Oracle payload | **@pythnetwork/pyth-lazer-sdk** | Fetch latest update in Solana binary encoding. |
| Pyth state resolution | **@pythnetwork/pyth-lazer-cardano-js** | `getPythState` / `getPythScriptHash`. |

**Lucid 0.10** does not support attaching **Plutus V3** spend scripts in a maintainable way; Evolution handles **Plutus V3** vault spends and **Pyth withdrawals** together. This split is intentional and documented in [PITCH.md](./PITCH.md).

---

## 4. Repository layout

```text
inventory-edge-protocol/
├── README.md ← This file (judges)
├── PITCH.md ← Product / thesis (judges)
├── .env.example
├── package.json
├── lib/
│ ├── feeds.ts ← Pyth Pro ids + canal (`Metal.XAU/USD` 346, `Crypto.BTC/USD` 1, WTI futuro `WTIK6` 2694)
│ ├── pyth.ts
│ ├── transactions.ts ← openVault, applyHedge, liquidate
│ └── ...
├── server/
│ └── index.ts ← Local judge API (`.env` signing)
├── web/ ← Vite React judge UI
│ ├── index.html
│ └── src/
├── scripts/
│ ├── mock_assets.ts ← Mint 3 shadow NFTs
│ ├── run_open_vault.ts
│ ├── run_apply_hedge.ts
│ └── run_liquidate.ts
└── onchain/
├── aiken.toml
├── plutus.json ← Blueprint (commit after build)
└── validators/
├── vault.ak ← Core vault contract
└── liquidity_pool.ak ← tADA pool validator
```

---

## 5. Build & run (operators)

**Prerequisites:** Node 20+, [Aiken](https://aiken-lang.org) 1.1+, PreProd **tADA**, **Pyth Lazer** `ACCESS_TOKEN`, and **Blockfrost Preprod** `BLOCKFROST_PROJECT_ID` or **Maestro** `MAESTRO_API_KEY`.

```bash
cd lazer/cardano/inventory-edge-protocol # if you cloned pyth-examples from its root
npm install
npm run build:onchain # produces onchain/plutus.json
npx tsc # optional typecheck
```

**`package.json` scripts:**

| Script | Purpose |
|--------|---------|
| `build:onchain` | `aiken build` in `onchain/` |
| `mock-assets` | Mint demo shadow NFTs (`scripts/mock_assets.ts`) |
| `tx:open-vault` / `tx:hedge` / `tx:liquidate` | CLI flows in `scripts/` |
| `dev:api` | Express judge API only (`server/index.ts`) |
| `dev:web` | Vite dev server for `web/` |
| `demo` | API + web together (`concurrently`) |
| `build:web` / `preview:web` | Production build / preview for the UI |

**Environment:** copy `.env.example` → `.env` and fill secrets (never commit `.env`).

| Variable | Purpose |
|----------|---------|
| `CARDANO_MNEMONIC` | 24-word PreProd wallet |
| `ACCESS_TOKEN` | Pyth Lazer API key |
| `BLOCKFROST_PROJECT_ID` or `MAESTRO_API_KEY` | Lucid chain access for mint |
| `SHADOW_POLICY_ID` / `SHADOW_NAME_HEX` | After mint, to open vault |
| `SHADOW_ASSET` | `XAU_USD` \| `WTI_USD` \| `BTC_USD` (feeds in `lib/feeds.ts`) |

**Loader:** `server/load_env.ts` reads `.env` from the **package root** (parent of `server/`), not from whatever the current working directory is — useful when debugging “API sees no env”.

**Suggested demo sequence:**

```bash
npm run mock-assets
# Export SHADOW_* from script output, then:
npm run tx:open-vault
npm run tx:hedge # optional
npm run tx:liquidate # needs underwater economics + ACCESS_TOKEN
```

**Judge UI (browser):** with the same `.env`, start API + Vite together:

```bash
npm run demo
```

Open [http://127.0.0.1:5173](http://127.0.0.1:5173). The UI proxies `/api` to `http://127.0.0.1:8787` (override with `JUDGE_API_PORT`). Signing stays server-side via `CARDANO_MNEMONIC` — suitable for hackathon booths, not for production custody.

**CLI vs UI:** `package.json` only wires **open-vault**, **hedge**, and **liquidate** as `tx:*` scripts. **Close**, **adjust debt**, **claim insurance**, and **liquidity pool** flows live in [`lib/transactions.ts`](lib/transactions.ts) / [`lib/pool_onchain.ts`](lib/pool_onchain.ts) and are driven from the **judge API** when you run `npm run demo` (or via your own `tsx` one-liner).

---

## 6. External references (for comparison)

- **Pyth Lazer Cardano + Aiken:** [`pyth-lazer-cardano`](https://github.com/pyth-network/pyth-lazer-cardano) (dependency in `onchain/aiken.toml`; `pyth.get_updates` and PreProd policy id come from there).
- **Pyth Lazer docs:** [docs.pyth.network — Lazer](https://docs.pyth.network/lazer) (access token, feeds, integration concepts).

---

## 7. Honest MVP limits & pitfalls (read before scoring)

**Design / product**

- **BTC / WTI feed ids** in `lib/feeds.ts` may be **placeholders**; **XAU (346)** is aligned with typical Lazer metal feeds — confirm every id via the Lazer symbols API before production use.
- **Insurance payout:** on-chain we **authorize** `ClaimInsurance` when price &lt; strike; routing exact **payout ADA** to outputs is left as a product-layer refinement (datum already stores `payout_lovelace` for demos).
- **`pyth-lazer-cardano` in `aiken.toml` tracks `main` on GitHub** — reproducible builds depend on fetch time; the **committed** `onchain/plutus.json` (+ lockfile) is what reviewers should treat as the shipped artifact unless they intentionally rebuild.
- **PreProd only** — do not reuse demo mnemonics or API keys on mainnet.

**If someone runs the live demo (operators, not required for judges)**

- **No `BLOCKFROST_PROJECT_ID` or `MAESTRO_API_KEY`:** Lucid mint and Evolution simulation paths often break; Koios-only setups frequently hit `evaluateTx` failures (see comments in [`.env.example`](./.env.example)).
- **No `ACCESS_TOKEN` (Pyth Lazer):** `Liquidate` and `ClaimInsurance` cannot be fully exercised — both require the pull-model witness.
- **`SHADOW_POLICY_ID` / `SHADOW_NAME_HEX` unset:** `tx:open-vault` and related flows fail until set after `mock-assets`.
- **`ClaimInsurance`:** implemented on-chain and in [`lib/transactions.ts`](lib/transactions.ts); there is **no** `tx:claim-insurance` script — use the judge **API/UI** under `npm run demo` or call the library from a small script.

---

## 8. License

On-chain project license: **Apache-2.0** (see `onchain/aiken.toml` preamble).
37 changes: 37 additions & 0 deletions lazer/cardano/inventory-edge-protocol/lib/blueprint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { readFileSync } from "node:fs";
import { PLUTUS_JSON } from "./paths.js";

export type BlueprintValidator = {
title: string;
compiledCode: string;
hash: string;
};

export type Blueprint = {
validators: BlueprintValidator[];
};

export function loadBlueprint(): Blueprint {
const raw = readFileSync(PLUTUS_JSON, "utf8");
return JSON.parse(raw) as Blueprint;
}

export function vaultSpendValidator(blueprint: Blueprint): BlueprintValidator {
const v = blueprint.validators.find((x) => x.title === "vault.vault.spend");
if (!v) throw new Error("vault.vault.spend not found in plutus.json — run: npm run build:onchain");
return v;
}

export function liquidityPoolSpendValidator(
blueprint: Blueprint,
): BlueprintValidator {
const v = blueprint.validators.find(
(x) => x.title === "liquidity_pool.liquidity_pool.spend",
);
if (!v) {
throw new Error(
"liquidity_pool.liquidity_pool.spend not found in plutus.json — run: npm run build:onchain",
);
}
return v;
}
Loading