diff --git a/.claude/SKILL.MD b/.claude/SKILL.MD new file mode 100644 index 0000000..45991a9 --- /dev/null +++ b/.claude/SKILL.MD @@ -0,0 +1,41 @@ +# Long-Short Backend Skills + +## Database Operations + +```bash +cd apps/long-short-backend + +# Run migrations +DATABASE_URL=postgres://postgres:JBNGlQ9wNFLlYWc2mG@localhost:5432/margin pnpm run:migrate + +# Create new migration +DATABASE_URL=postgres://postgres:JBNGlQ9wNFLlYWc2mG@localhost:5432/margin pnpm kysely migrate:make + +# Run seeds +DATABASE_URL=postgres://postgres:JBNGlQ9wNFLlYWc2mG@localhost:5432/margin pnpm run:seed + +# Regenerate database types +DATABASE_URL=postgres://postgres:JBNGlQ9wNFLlYWc2mG@localhost:5432/margin pnpm codegen +``` + +## Development + +```bash +cd apps/long-short-backend + +# Start dev server +DATABASE_URL=postgres://postgres:JBNGlQ9wNFLlYWc2mG@localhost:5432/margin pnpm dev + +# Build +pnpm build +``` + +## Docker + +```bash +docker compose build long-short-backend # Build image +docker compose up -d # Start services +docker compose logs -f long-short-backend # View logs +docker compose exec long-short-backend pnpm --filter=long-short-backend run:migrate # Migrate in Docker +docker compose exec long-short-backend pnpm --filter=long-short-backend run:seed # Seed in Docker +``` diff --git a/.claude/specs/felis-build-tx.md b/.claude/specs/felis-build-tx.md new file mode 100644 index 0000000..a3817ad --- /dev/null +++ b/.claude/specs/felis-build-tx.md @@ -0,0 +1,11 @@ +# @minswap/felis-build-tx + +DEX transaction builder. Depends on `felis-ledger-core`, `felis-ledger-utils`, `felis-tx-builder`, `felis-dex-v2`. + +**Location:** `packages/minswap-build-tx` + +## What's here +- `DEXOrderTransaction.createBulkOrdersTx()` — main entry point, batches multiple DEX orders into one tx +- Order option types for each V2 step (SwapExactIn, SwapExactOut, Deposit, Withdraw, etc.) +- `Djed` — stablecoin protocol (mint/rate calculations) +- `MetadataMessage` — transaction label constants (DEX_MARKET_ORDER, DEX_LIMIT_ORDER, etc.) diff --git a/.claude/specs/felis-cip.md b/.claude/specs/felis-cip.md new file mode 100644 index 0000000..17579bf --- /dev/null +++ b/.claude/specs/felis-cip.md @@ -0,0 +1,11 @@ +# @minswap/felis-cip + +Cardano Improvement Proposals. Depends on `felis-ledger-core`, `felis-ledger-utils`. + +**Location:** `packages/cip` + +## What's here +- `Bip32` — HD wallet key derivation, address generation, UTXO filtering +- `Bip39` — mnemonic seed to `BaseAddressWallet` / `EnterpriseAddressWallet` +- `CIP-25` — NFT metadata standard types +- `CIP-68` — reference NFT token standard (isRefNFT, isFT, isNFT, mint helpers) diff --git a/.claude/specs/felis-dex-v1.md b/.claude/specs/felis-dex-v1.md new file mode 100644 index 0000000..cba57f3 --- /dev/null +++ b/.claude/specs/felis-dex-v1.md @@ -0,0 +1,10 @@ +# @minswap/felis-dex-v1 + +Legacy DEX V1 protocol types. Depends on `felis-ledger-core`, `felis-ledger-utils`. + +**Location:** `packages/minswap-dex-v1` + +## What's here +- `Order` — V1 order parsing from UTXOs (SWAP_EXACT_IN, DEPOSIT, WITHDRAW, etc.) +- Compiled Plutus V1 scripts for mainnet/testnet +- Primarily consumed by the syncer for historical order parsing diff --git a/.claude/specs/felis-dex-v2.md b/.claude/specs/felis-dex-v2.md new file mode 100644 index 0000000..16089d0 --- /dev/null +++ b/.claude/specs/felis-dex-v2.md @@ -0,0 +1,13 @@ +# @minswap/felis-dex-v2 + +DEX V2 protocol types and calculations. Depends on `felis-ledger-core`, `felis-ledger-utils`. + +**Location:** `packages/minswap-dex-v2` + +## What's here +- `OrderV2` — 11 step types (SWAP_EXACT_IN, STOP_LOSS, OCO, DEPOSIT, WITHDRAW, PARTIAL_SWAP, MULTI_ROUTING, etc.) +- `PoolV2` — liquidity pool state, reserves, fees (denominator=10000) +- `DexV2Calculation` — swap/deposit/withdraw math, price impact, multi-routing +- `OrderV2Datum` — Plutus serialization (fromPlutusJson/toPlutusJson, fromDataHex/toDataHex) +- Config helpers: `getDexV2Configs()`, `getDexV2PoolAddresses()`, `buildDexV2OrderAddress()` +- `BATCHER_FEE_DEX_V2` — fee schedule per step type diff --git a/.claude/specs/felis-ledger-core.md b/.claude/specs/felis-ledger-core.md new file mode 100644 index 0000000..fc5dc15 --- /dev/null +++ b/.claude/specs/felis-ledger-core.md @@ -0,0 +1,18 @@ +# @minswap/felis-ledger-core + +Cardano blockchain primitives. Depends on `felis-ledger-utils`. + +**Location:** `packages/ledger-core` + +## What's here +- `Address` — bech32/hex conversion, stake address extraction, PlutusJson serialization +- `Asset` — policy ID + token name, `ADA` constant for lovelace +- `Value` — multi-asset container (get/set/add/subtract/canCover) +- `TxIn` / `TxOut` / `Utxo` — transaction inputs/outputs +- `Transaction` / `TxBody` — full transaction types +- `PrivateKey` / `PublicKey` / `PublicKeyHash` — crypto keys +- `Bytes` — hex/string/base64 wrapper +- `PlutusData` — Plutus serialization (Constr, List, Map, Int, Bytes) +- `NetworkEnvironment` — MAINNET (764824073), TESTNET_PREVIEW (2), TESTNET_PREPROD (1) +- `XJSON` — type-preserving JSON (bigint, Date, Bytes, Asset, Address, Value) +- `getTimeFromSlotMagic` / `getSlotFromTimeMagic` — slot/time conversion diff --git a/.claude/specs/felis-ledger-utils.md b/.claude/specs/felis-ledger-utils.md new file mode 100644 index 0000000..de3431a --- /dev/null +++ b/.claude/specs/felis-ledger-utils.md @@ -0,0 +1,15 @@ +# @minswap/felis-ledger-utils + +Foundation utility library. All other packages depend on this. + +**Location:** `packages/ledger-utils` + +## What's here +- `Result` / `Maybe` — error handling and optionals +- `Duration` — time arithmetic +- `blake2b256`, `blake2b224`, `sha3` — crypto hashes +- `encodeBech32` / `decodeBech32` +- `RustModule` — WASM loader (must call `await RustModule.load()` before any WASM ops) +- `CborHex` — branded type for CBOR hex strings +- `getErrorMessage(error)` — safe stringify that handles BigInt +- `safeFreeRustObjects()` — prevents double-free on WASM objects diff --git a/.claude/specs/felis-lending-market.md b/.claude/specs/felis-lending-market.md new file mode 100644 index 0000000..986b708 --- /dev/null +++ b/.claude/specs/felis-lending-market.md @@ -0,0 +1,20 @@ +# @minswap/felis-lending-market + +Liqwid Finance lending protocol integration. Depends on `felis-ledger-core`, `felis-ledger-utils`. + +**Location:** `packages/minswap-lending-market` + +## What's here +- `LiqwidProviderV2` — namespace wrapping Liqwid V2 GraphQL API + +### Namespaces +- `Transactions` — supply, withdraw, borrow, modifyBorrow, repayLoan, submit (returns CBOR hex) +- `Calculations` — loan health factor, supply/withdraw caps, net APY +- `Data` — query markets, loans, user loans, yield earned +- `signTx()` / `getTxHash()` — sign and hash Liqwid transactions + +## Gotchas +- Liqwid V2 API uses `number` for amounts (not `bigint` like the rest of the codebase) +- `MarketId` is a string like "Ada", "MIN", "DJED" (not the market_id from our DB) +- `loanUtxoId` format is `"{txHash}-{outputIndex}"` (dash separator, not hash) +- API endpoints differ per network: `v2.api.liqwid.finance` (mainnet), `v2.api.preprod.liqwid.dev` (preprod), `v2.api.preview.liqwid.dev` (preview) diff --git a/.claude/specs/felis-tx-builder.md b/.claude/specs/felis-tx-builder.md new file mode 100644 index 0000000..810d46e --- /dev/null +++ b/.claude/specs/felis-tx-builder.md @@ -0,0 +1,12 @@ +# @minswap/felis-tx-builder + +High-level Cardano transaction composition. Depends on `felis-ledger-core`, `felis-ledger-utils`, `felis-cip`. + +**Location:** `packages/tx-builder` + +## What's here +- `TxBuilder` — fluent API: collectFrom, payTo, mintAssets, validFrom/To, attachValidator, complete() +- `TxComplete` — signing (signWithPrivateKey, partialSign, assemble) +- `CoinSelectionAlgorithm` — MINSWAP (smart + change splitting), SPEND_ALL, SPEND_ALL_V2 +- `UtxoSelection` — UTXO selection and collateral selection +- `EmulatorProvider` — off-chain provider for testing diff --git a/.claude/specs/long-short-backend.md b/.claude/specs/long-short-backend.md new file mode 100644 index 0000000..e9738ed --- /dev/null +++ b/.claude/specs/long-short-backend.md @@ -0,0 +1,30 @@ +# long-short-backend + +Leveraged long/short trading API. Integrates Minswap DEX with Liqwid lending. + +**Location:** `apps/long-short-backend` + +## Key files +``` +src/api/state-machine.ts -- Build/waiting functions per order type +src/services/position-service.ts -- Core business logic (create, buildTx, close) +src/api/routes/position.ts -- API route handlers +src/api/schemas.ts -- TypeBox request/response schemas +src/api/helper.ts -- CIP-8 authentication +src/repository/ -- Database access layer +src/provider/cardanoscan.ts -- On-chain transaction search +src/config/market.ts -- Market config cache +src/cmd/run-api.ts -- Entry point +``` + +## Order state machine (non-obvious flow) +**Open LONG:** LONG_BUY → LONG_SUPPLY → LONG_BORROW → LONG_BUY_MORE +**Close LONG:** LONG_SELL → LONG_REPAY → LONG_WITHDRAW → LONG_SELL_ALL + +Each order goes through: build tx → user signs → confirm on chain → wait for output spend → next order. + +## Gotchas +- LONG_SUPPLY uses LiqwidProvider V1, all other Liqwid ops use V2 +- `amount_borrow = amount_in * (leverage - 1) + 4_000_000n` (fee buffer) +- Check waiting orders BEFORE finding unhandled orders in buildTx flow +- `built_valid_to` determines when to rebuild an expired transaction diff --git a/CLAUDE.md b/CLAUDE.md index d9fe158..f8176ca 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,117 +1,47 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. +The role of this file is to describe common mistake and confusion points that agents might encounter as they work in this project. If you ever encounter something in the project that surprises you, please alert the developer working with you and indicate that this is the case in the CLAUDE.md file to help prevent future agents from having the same issue. + +> **Principle:** If the info is in the codebase, it probably doesn't need to be in this file. Keep this lean. ## Project Overview -Minswap Felis is a monorepo implementing Cardano DEX infrastructure with leveraged long/short trading positions integrated with Liqwid lending protocol. Built with TypeScript, Turborepo, and pnpm workspaces. +Minswap Felis - Cardano DEX monorepo with leveraged long/short trading via Liqwid lending. TypeScript, Turborepo, pnpm workspaces. ## Commands ```bash -# Build & Development -pnpm build # Build all packages (respects turbo dependency graph) -pnpm dev # Watch mode across all packages -pnpm check-types # Run tsc --noEmit on all packages - -# Testing -pnpm test # Run all tests via vitest -pnpm test:watch # Watch mode for tests -pnpm --filter=@repo/ledger-core test # Run tests for single package - -# Linting & Formatting -pnpm format-and-lint # Check with biome (no changes) +pnpm build # Build all packages +pnpm check-types # Type-check all packages +pnpm test # Run all tests (vitest) +pnpm --filter=@repo/ledger-core test # Single package tests pnpm format-and-lint:fix # Auto-fix with biome -pnpm exec turbo run check-circular-imports # Detect circular imports - -# Web App pnpm --filter=web dev # Next.js dev server (port 3001) -``` - -## Architecture - -### Package Dependency Graph -``` -ledger-utils (foundation: Result type, crypto, hex, bech32) - ↓ -ledger-core (Address, Tx, UTXO, Scripts, Protocol) - ↓ - ├→ cip (BIP32/39, CIP-25, CIP-68) - │ ↓ - └───┴→ tx-builder (high-level transaction composition) - ↓ - minswap-build-tx (DEX transaction builder) - ↓ - minswap-lending-market (Liqwid SDK, Nitro Wallet) - ↓ - web (Next.js app) -minswap-dex-v2 (DEX V2 protocol types) → depends on ledger-core, ledger-utils +# Long-short backend +pnpm --filter=long-short-backend run migrate:latest # Apply DB migrations +pnpm --filter=long-short-backend run migrate:down # Rollback last migration ``` -### Key Packages -- **ledger-utils**: `Result` error handling, crypto utilities, `CborHex` branded types -- **ledger-core**: Cardano primitives (Address, Tx, UTXO), `NetworkEnvironment` discrimination -- **tx-builder**: Transaction composition with UTXO selection -- **minswap-dex-v2**: DEX V2 protocol types and order handling -- **minswap-lending-market**: Liqwid provider SDK, Nitro Wallet (password-less signing) +## Package Dependency Graph -## Code Patterns - -### Error Handling -Use `Result` from ledger-utils: -```typescript -Result.ok(value), Result.err(error), Result.isOk() ``` -Use `getErrorMessage(error)` for safe error stringification (handles BigInt). - -### Class Design -- Protected constructors with static factory methods -- Immutable `readonly` properties -- Bidirectional serialization: `toHex()`, `fromHex()`, `toPlutusJson()`, `fromPlutusJson()` - -### Type Conventions -- `bigint` for all amounts, slots, values (never numbers) -- `CborHex` branded types for CBOR serialization -- `NetworkEnvironment` required for address/protocol operations (mainnet vs testnet) -- Type-only imports: `import type { Type } from "..."` -- Workspace imports: Always use `@repo/*` scope - -### WASM Initialization -```typescript -import { RustModule } from "@repo/ledger-utils"; -beforeAll(async () => { - await RustModule.load(); -}); +ledger-utils → ledger-core → cip → tx-builder → minswap-build-tx → minswap-lending-market → web +minswap-dex-v2 → ledger-core, ledger-utils ``` -## Key Dependencies - -- **@emurgo/cardano-serialization-lib (CSL)**: Cardano primitives via Rust/WASM -- **@stricahq/cbors**: CBOR encoding (Cardano serialization format) -- **json-bigint**: Preserve BigInt precision in JSON (don't use native JSON for amounts) -- **remeda**: Functional utility library (like lodash/fp) -- **@minswap/tiny-invariant**: Development-only assertions -- **dpdm**: Circular import detection (run via `check-circular-imports` script) - -## Testing - -- Framework: Vitest with `vitest.config.mts` per package -- Test location: `{package}/test/**/*.{test,spec}.ts` -- Property-based testing: `fast-check` available -- Environment: Node (Vitest aliases browser WASM to Node variants) - -## Code Style (Biome) +## Conventions -- Line width: 120 -- Quotes: Double -- Semicolons: Always -- Trailing commas: All -- Arrow parens: Always `(x) => x` +- `bigint` for all Cardano amounts, slots, IDs (never `number`) +- `Result` from ledger-utils for error handling +- `import type` for type-only imports +- `@repo/*` scope for workspace imports +- `await RustModule.load()` required before crypto/WASM operations in tests +- `json-bigint` for JSON with BigInt values (never native `JSON.stringify`) -## Tech Stack +## Gotchas -- TypeScript 5.8, Node.js >= 22 -- Turborepo + pnpm 9.0.0 -- Vitest, Biome -- Next.js, React 19, Jotai, Ant Design +- Cardanoscan API: use `address.toHex()` (not bech32), header is `"apiKey"` +- DB types: update `src/database/db.d.ts` after migrations, use `Generated` for defaults +- DB IDs: always `BigInt(row.id)` when mapping rows +- API fields: snake_case externally, camelCase internally diff --git a/Dockerfile.backend b/Dockerfile.backend new file mode 100644 index 0000000..769fbc6 --- /dev/null +++ b/Dockerfile.backend @@ -0,0 +1,50 @@ +# Stage 1: Base +FROM node:22-slim AS base +WORKDIR /usr/src/app + +# Install pnpm and curl (for healthcheck) +RUN apt-get update && apt-get install -y curl && rm -rf /var/lib/apt/lists/* +RUN corepack enable && corepack prepare pnpm@9.0.0 --activate + +# Set pnpm global bin directory +ENV PNPM_HOME="/root/.local/share/pnpm" +ENV PATH="${PNPM_HOME}:${PATH}" + +# Stage 2: Prune / Extract package.json files +# This stage filters the repo to only the files needed for installation +FROM base AS cleaner +RUN pnpm add -g turbo +COPY . . +# Generate a pruned version of the repo +ARG APP_NAME=long-short-backend +RUN turbo prune ${APP_NAME} --docker + +# Stage 3: Install dependencies +FROM base AS installer +# Copy only the lockfile and the pruned package.json files from the cleaner stage +COPY --from=cleaner /usr/src/app/out/json/ . +COPY --from=cleaner /usr/src/app/out/pnpm-lock.yaml ./pnpm-lock.yaml + +# Install dependencies (heavily cached) +RUN pnpm install --frozen-lockfile + +# Stage 4: Build / Source +FROM base AS builder +COPY --from=installer /usr/src/app ./ +# Copy the actual source code (this is what changes often) +COPY --from=cleaner /usr/src/app/out/full/ . +COPY turbo.json turbo.json + +# Build the app +ARG APP_NAME=long-short-backend +RUN pnpm turbo build --filter=${APP_NAME} + +# Stage 5: Production release +FROM base AS release +COPY --from=builder /usr/src/app ./ + +# Expose API port +EXPOSE 9999 + +# Command is set in docker-compose.yml +CMD ["node", "--version"] diff --git a/Dockerfile b/Dockerfile.interface similarity index 98% rename from Dockerfile rename to Dockerfile.interface index fb2d662..286ec11 100644 --- a/Dockerfile +++ b/Dockerfile.interface @@ -40,7 +40,7 @@ COPY --from=builder /app/apps/web ./apps/web RUN pnpm install --prod --frozen-lockfile # Expose port -EXPOSE 3000 +EXPOSE 3002 # Set environment to production ENV NODE_ENV=production diff --git a/apps/example/src/cardanoscan.ts b/apps/example/src/cardanoscan.ts index b5bd710..1713959 100644 --- a/apps/example/src/cardanoscan.ts +++ b/apps/example/src/cardanoscan.ts @@ -2,8 +2,7 @@ import fs from "node:fs"; import { RustModule } from "@minswap/felis-ledger-utils"; import { MinswapStableswapSyncer, MinswapV1Syncer, MinswapV2Syncer, SplashSyncer, SundaeSwapV1Syncer, SundaeSwapV3Syncer, Transaction, WingridersV1Syncer, WingridersV2Syncer } from "@minswap/felis-syncer"; import socketIO from "socket.io-client"; -import { NetworkEnvironment } from "../../../packages/ledger-core/dist/network-id"; -import { Bytes } from "@minswap/felis-ledger-core"; +import { Bytes, NetworkEnvironment } from "@minswap/felis-ledger-core"; const main = async () => { await RustModule.load(); diff --git a/apps/long-short-backend/.config/kysely.config.ts b/apps/long-short-backend/.config/kysely.config.ts new file mode 100644 index 0000000..8bcd2cd --- /dev/null +++ b/apps/long-short-backend/.config/kysely.config.ts @@ -0,0 +1,29 @@ +import { PostgresAdapter, PostgresDriver, PostgresIntrospector, PostgresQueryCompiler } from "kysely"; +import { defineConfig } from "kysely-ctl"; +import { Pool } from "pg"; + +export default defineConfig({ + dialect: { + createAdapter() { + return new PostgresAdapter(); + }, + createDriver() { + const pool = new Pool({ + connectionString: process.env.DATABASE_URL, + }); + return new PostgresDriver({ pool }); + }, + createIntrospector(db) { + return new PostgresIntrospector(db); + }, + createQueryCompiler() { + return new PostgresQueryCompiler(); + }, + }, + migrations: { + migrationFolder: "migrations", + }, + seeds: { + seedFolder: "seeds", + }, +}); diff --git a/apps/long-short-backend/.config/migrations/1770095322614_position.ts b/apps/long-short-backend/.config/migrations/1770095322614_position.ts new file mode 100644 index 0000000..51979fc --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770095322614_position.ts @@ -0,0 +1,125 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up(db: Kysely): Promise { + // Create position table + await db.schema + .createTable("position") + .addColumn("id", "bigserial", (col) => col.primaryKey()) + .addColumn("user_address", "varchar(128)", (col) => col.notNull()) + .addColumn("market", "varchar(128)", (col) => col.notNull()) + .addColumn("side", "varchar(8)", (col) => col.notNull()) // LONG, SHORT + .addColumn("status", "varchar(16)", (col) => col.notNull().defaultTo("OPEN")) // OPEN, CLOSED, LIQUIDATED + .addColumn("leverage", "numeric", (col) => col.notNull()) + .addColumn("collateral_asset", "varchar(128)", (col) => col.notNull()) + .addColumn("collateral_amount", "numeric", (col) => col.notNull()) + .addColumn("entry_price", "numeric", (col) => col.notNull()) + .addColumn("position_size", "numeric", (col) => col.notNull()) + .addColumn("borrowed_amount", "numeric", (col) => col.notNull()) + .addColumn("liquidation_price", "numeric", (col) => col.notNull()) + .addColumn("take_profit_price", "numeric") + .addColumn("stop_loss_price", "numeric") + .addColumn("realized_pnl", "numeric", (col) => col.notNull().defaultTo(0)) + .addColumn("unrealized_pnl", "numeric", (col) => col.notNull().defaultTo(0)) + .addColumn("funding_paid", "numeric", (col) => col.notNull().defaultTo(0)) + .addColumn("liqwid_supply_id", "varchar(128)") + .addColumn("liqwid_borrow_id", "varchar(128)") + .addColumn("created_at", "timestamp", (col) => col.notNull().defaultTo(sql`now()`)) + .addColumn("updated_at", "timestamp", (col) => col.notNull().defaultTo(sql`now()`)) + .addColumn("closed_at", "timestamp") + .execute(); + + // Create indexes for position table + await db.schema + .createIndex("idx_position_user_address") + .on("position") + .column("user_address") + .execute(); + + await db.schema + .createIndex("idx_position_market") + .on("position") + .column("market") + .execute(); + + await db.schema + .createIndex("idx_position_status") + .on("position") + .column("status") + .execute(); + + await db.schema + .createIndex("idx_position_user_status") + .on("position") + .columns(["user_address", "status"]) + .execute(); + + // Create order table + await db.schema + .createTable("order") + .addColumn("id", "bigserial", (col) => col.primaryKey()) + .addColumn("position_id", "bigint", (col) => col.references("position.id").onDelete("set null")) + .addColumn("user_address", "varchar(128)", (col) => col.notNull()) + .addColumn("market", "varchar(128)", (col) => col.notNull()) + .addColumn("order_type", "varchar(16)", (col) => col.notNull()) // MARKET, LIMIT, STOP_MARKET, STOP_LIMIT + .addColumn("side", "varchar(8)", (col) => col.notNull()) // LONG, SHORT + .addColumn("action", "varchar(16)", (col) => col.notNull()) // OPEN, CLOSE, INCREASE, DECREASE + .addColumn("status", "varchar(16)", (col) => col.notNull().defaultTo("PENDING")) // PENDING, FILLED, CANCELLED, EXPIRED + .addColumn("leverage", "numeric") + .addColumn("collateral_amount", "numeric") + .addColumn("size", "numeric", (col) => col.notNull()) + .addColumn("price", "numeric") // limit price + .addColumn("trigger_price", "numeric") // for stop orders + .addColumn("slippage_tolerance", "numeric") + .addColumn("tx_hash", "varchar(64)") + .addColumn("filled_price", "numeric") + .addColumn("filled_at", "timestamp") + .addColumn("expires_at", "timestamp") + .addColumn("created_at", "timestamp", (col) => col.notNull().defaultTo(sql`now()`)) + .execute(); + + // Create indexes for order table + await db.schema + .createIndex("idx_order_user_address") + .on("order") + .column("user_address") + .execute(); + + await db.schema + .createIndex("idx_order_position_id") + .on("order") + .column("position_id") + .execute(); + + await db.schema + .createIndex("idx_order_market") + .on("order") + .column("market") + .execute(); + + await db.schema + .createIndex("idx_order_status") + .on("order") + .column("status") + .execute(); + + await db.schema + .createIndex("idx_order_user_status") + .on("order") + .columns(["user_address", "status"]) + .execute(); + + await db.schema + .createIndex("idx_order_tx_hash") + .on("order") + .column("tx_hash") + .execute(); +} + +export async function down(db: Kysely): Promise { + // Drop order table first (has FK to position) + await db.schema.dropTable("order").ifExists().execute(); + + // Drop position table + await db.schema.dropTable("position").ifExists().execute(); +} diff --git a/apps/long-short-backend/.config/migrations/1770098176165_market_config.ts b/apps/long-short-backend/.config/migrations/1770098176165_market_config.ts new file mode 100644 index 0000000..68a1b50 --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770098176165_market_config.ts @@ -0,0 +1,30 @@ +import type { Kysely } from "kysely"; + +export async function up(db: Kysely): Promise { + await db.schema + .createTable("market_config") + .addColumn("market_id", "varchar(64)", (col) => col.primaryKey()) + .addColumn("asset_a", "varchar(128)", (col) => col.notNull()) + .addColumn("asset_b", "varchar(128)", (col) => col.notNull()) + .addColumn("amm_lp_asset", "varchar(128)", (col) => col.notNull()) + .addColumn("asset_a_q_token_ticker", "varchar(32)", (col) => col.notNull()) + .addColumn("asset_a_q_token_raw", "varchar(128)", (col) => col.notNull()) + .addColumn("asset_b_q_token_ticker", "varchar(32)", (col) => col.notNull()) + .addColumn("asset_b_q_token_raw", "varchar(128)", (col) => col.notNull()) + .addColumn("collateral_market_id", "varchar(64)", (col) => col.notNull()) + .addColumn("leverage", "integer", (col) => col.notNull()) + .addColumn("min_collateral", "numeric", (col) => col.notNull()) + .addColumn("enable", "boolean", (col) => col.notNull().defaultTo(true)) + .execute(); + + // Create index on enable for filtering active markets + await db.schema + .createIndex("idx_market_config_enable") + .on("market_config") + .column("enable") + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable("market_config").ifExists().execute(); +} diff --git a/apps/long-short-backend/.config/migrations/1770117151271_update_position_table.ts b/apps/long-short-backend/.config/migrations/1770117151271_update_position_table.ts new file mode 100644 index 0000000..e862aa3 --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770117151271_update_position_table.ts @@ -0,0 +1,60 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up(db: Kysely): Promise { + // Step 1: Drop the FK constraint on order.position_id (keep as indexed column only) + await sql`ALTER TABLE "order" DROP CONSTRAINT IF EXISTS "order_position_id_fkey"`.execute(db); + + // Step 2: Drop existing position table + await db.schema.dropTable("position").ifExists().execute(); + + // Step 3: Create new position table + await db.schema + .createTable("position") + .addColumn("id", "bigserial", (col) => col.primaryKey()) + .addColumn("market_id", "varchar(64)", (col) => col.notNull().references("market_config.market_id")) + .addColumn("user_address", "varchar(128)", (col) => col.notNull()) + .addColumn("side", "varchar(8)", (col) => col.notNull()) // LONG | SHORT + .addColumn("status", "varchar(16)", (col) => col.notNull().defaultTo("PENDING")) // PENDING | OPEN | CLOSING | CLOSE + .addColumn("amount_in", "numeric", (col) => col.notNull()) + .addColumn("amount_borrow", "numeric", (col) => col.notNull()) + .addColumn("created_at", "timestamp", (col) => col.notNull().defaultTo(sql`now()`)) + .addColumn("closed_at", "timestamp") + .execute(); + + // Create index on market_id + await db.schema + .createIndex("idx_position_market_id") + .on("position") + .column("market_id") + .execute(); + + // Create index on user_address + await db.schema + .createIndex("idx_position_user_address") + .on("position") + .column("user_address") + .execute(); + + // Create index on closed_at + await db.schema + .createIndex("idx_position_closed_at") + .on("position") + .column("closed_at") + .execute(); + + // Create partial unique index for open positions (closed_at IS NULL) + // This ensures only one open position per user per market + await sql`CREATE UNIQUE INDEX idx_position_user_market_unique_open + ON position (user_address, market_id) + WHERE closed_at IS NULL`.execute(db); + + // Create unique constraint for closed positions + await sql`CREATE UNIQUE INDEX idx_position_user_market_closed_unique + ON position (user_address, market_id, closed_at) + WHERE closed_at IS NOT NULL`.execute(db); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable("position").ifExists().execute(); +} diff --git a/apps/long-short-backend/.config/migrations/1770117151272_update_order_table.ts b/apps/long-short-backend/.config/migrations/1770117151272_update_order_table.ts new file mode 100644 index 0000000..5bab067 --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770117151272_update_order_table.ts @@ -0,0 +1,38 @@ +import type { Kysely } from "kysely"; + +export async function up(db: Kysely): Promise { + // Step 1: Drop existing order table + await db.schema.dropTable("order").ifExists().execute(); + + // Step 2: Create new order table + await db.schema + .createTable("order") + .addColumn("id", "bigserial", (col) => col.primaryKey()) + .addColumn("position_id", "bigint", (col) => col.notNull()) // ref to position.id (not FK) + .addColumn("order_type", "varchar(32)", (col) => col.notNull()) // OPEN | CLOSE | INCREASE | DECREASE + .addColumn("created_tx_id", "varchar(64)", (col) => col.notNull()) + .addColumn("created_tx_index", "integer", (col) => col.notNull()) + .addColumn("asset_in", "varchar(128)") + .addColumn("amount_in", "numeric") + .addColumn("asset_out", "varchar(128)") + .addColumn("amount_out", "numeric") + .execute(); + + // Create index on position_id + await db.schema + .createIndex("idx_order_position_id") + .on("order") + .column("position_id") + .execute(); + + // Create index on created_tx_id + await db.schema + .createIndex("idx_order_created_tx_id") + .on("order") + .column("created_tx_id") + .execute(); +} + +export async function down(db: Kysely): Promise { + await db.schema.dropTable("order").ifExists().execute(); +} diff --git a/apps/long-short-backend/.config/migrations/1770117151273_update_market_config_leverage.ts b/apps/long-short-backend/.config/migrations/1770117151273_update_market_config_leverage.ts new file mode 100644 index 0000000..31f2659 --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770117151273_update_market_config_leverage.ts @@ -0,0 +1,10 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "market_config" ALTER COLUMN "leverage" TYPE numeric USING leverage::numeric`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "market_config" ALTER COLUMN "leverage" TYPE integer USING leverage::integer`.execute(db); +} diff --git a/apps/long-short-backend/.config/migrations/1770117151274_order_nullable_tx_fields.ts b/apps/long-short-backend/.config/migrations/1770117151274_order_nullable_tx_fields.ts new file mode 100644 index 0000000..b01dffa --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770117151274_order_nullable_tx_fields.ts @@ -0,0 +1,12 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "order" ALTER COLUMN "created_tx_id" DROP NOT NULL`.execute(db); + await sql`ALTER TABLE "order" ALTER COLUMN "created_tx_index" DROP NOT NULL`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "order" ALTER COLUMN "created_tx_id" SET NOT NULL`.execute(db); + await sql`ALTER TABLE "order" ALTER COLUMN "created_tx_index" SET NOT NULL`.execute(db); +} diff --git a/apps/long-short-backend/.config/migrations/1770117151275_order_built_tx_fields.ts b/apps/long-short-backend/.config/migrations/1770117151275_order_built_tx_fields.ts new file mode 100644 index 0000000..476bae6 --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770117151275_order_built_tx_fields.ts @@ -0,0 +1,14 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "order" ADD COLUMN "built_tx_id" TEXT`.execute(db); + await sql`ALTER TABLE "order" ADD COLUMN "built_outputs_hash" TEXT`.execute(db); + await sql`ALTER TABLE "order" ADD COLUMN "built_valid_to" TIMESTAMPTZ`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "order" DROP COLUMN "built_valid_to"`.execute(db); + await sql`ALTER TABLE "order" DROP COLUMN "built_outputs_hash"`.execute(db); + await sql`ALTER TABLE "order" DROP COLUMN "built_tx_id"`.execute(db); +} diff --git a/apps/long-short-backend/.config/migrations/1770117151276_order_waiting_column.ts b/apps/long-short-backend/.config/migrations/1770117151276_order_waiting_column.ts new file mode 100644 index 0000000..bc50593 --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770117151276_order_waiting_column.ts @@ -0,0 +1,10 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "order" ADD COLUMN "waiting" BOOLEAN NOT NULL DEFAULT true`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "order" DROP COLUMN "waiting"`.execute(db); +} diff --git a/apps/long-short-backend/.config/migrations/1770117151277_market_config_borrow_market_ids.ts b/apps/long-short-backend/.config/migrations/1770117151277_market_config_borrow_market_ids.ts new file mode 100644 index 0000000..53eb3cf --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770117151277_market_config_borrow_market_ids.ts @@ -0,0 +1,12 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "market_config" ADD COLUMN "borrow_market_id_long" TEXT NOT NULL DEFAULT ''`.execute(db); + await sql`ALTER TABLE "market_config" ADD COLUMN "borrow_market_id_short" TEXT NOT NULL DEFAULT ''`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "market_config" DROP COLUMN "borrow_market_id_long"`.execute(db); + await sql`ALTER TABLE "market_config" DROP COLUMN "borrow_market_id_short"`.execute(db); +} diff --git a/apps/long-short-backend/.config/migrations/1770117151278_market_config_short_leverage.ts b/apps/long-short-backend/.config/migrations/1770117151278_market_config_short_leverage.ts new file mode 100644 index 0000000..387948d --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770117151278_market_config_short_leverage.ts @@ -0,0 +1,12 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "market_config" RENAME COLUMN "leverage" TO "long_leverage"`.execute(db); + await sql`ALTER TABLE "market_config" ADD COLUMN "short_leverage" NUMERIC NOT NULL DEFAULT 0`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "market_config" DROP COLUMN "short_leverage"`.execute(db); + await sql`ALTER TABLE "market_config" RENAME COLUMN "long_leverage" TO "leverage"`.execute(db); +} diff --git a/apps/long-short-backend/.config/migrations/1770117151279_market_config_collateral_market_ids.ts b/apps/long-short-backend/.config/migrations/1770117151279_market_config_collateral_market_ids.ts new file mode 100644 index 0000000..a3fce18 --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770117151279_market_config_collateral_market_ids.ts @@ -0,0 +1,14 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "market_config" RENAME COLUMN "collateral_market_id" TO "long_collateral_market_id"`.execute(db); + await sql`ALTER TABLE "market_config" ADD COLUMN "short_collateral_market_id" VARCHAR(64) NOT NULL DEFAULT ''`.execute( + db, + ); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "market_config" DROP COLUMN "short_collateral_market_id"`.execute(db); + await sql`ALTER TABLE "market_config" RENAME COLUMN "long_collateral_market_id" TO "collateral_market_id"`.execute(db); +} diff --git a/apps/long-short-backend/.config/migrations/1770117151280_drop_built_outputs_hash.ts b/apps/long-short-backend/.config/migrations/1770117151280_drop_built_outputs_hash.ts new file mode 100644 index 0000000..60e83c4 --- /dev/null +++ b/apps/long-short-backend/.config/migrations/1770117151280_drop_built_outputs_hash.ts @@ -0,0 +1,10 @@ +import type { Kysely } from "kysely"; +import { sql } from "kysely"; + +export async function up(db: Kysely): Promise { + await sql`ALTER TABLE "order" DROP COLUMN "built_outputs_hash"`.execute(db); +} + +export async function down(db: Kysely): Promise { + await sql`ALTER TABLE "order" ADD COLUMN "built_outputs_hash" TEXT`.execute(db); +} diff --git a/apps/long-short-backend/.config/seeds/market_config.ts b/apps/long-short-backend/.config/seeds/market_config.ts new file mode 100644 index 0000000..4d7040c --- /dev/null +++ b/apps/long-short-backend/.config/seeds/market_config.ts @@ -0,0 +1,39 @@ +import type { Kysely } from "kysely"; + +/** + * Seed data for market_config table + * + * Note: Update asset values with actual mainnet/testnet values before deployment + */ +export async function seed(db: Kysely): Promise { + // Clear existing data + await db.deleteFrom("market_config").execute(); + + // Insert seed data + await db + .insertInto("market_config") + .values([ + { + market_id: "ADA-NIGHT", + asset_a: "lovelace", + asset_b: "0691b2fecca1ac4f53cb6dfb00b7013e561d1f34403b957cbb5af1fa.4e49474854", // NIGHT + amm_lp_asset: + "f5808c2c990d86da54bfc97d89cee6efa20cd8461616359478d96b4c.e74c52975908a612d5ce68327040d449aae99f8b463bb6de046a1b23c5713169", + asset_a_q_token_ticker: "qAda", + asset_a_q_token_raw: "a04ce7a52545e5e33c2867e148898d9e667a69602285f6a1298f9d68", + asset_b_q_token_ticker: "qNIGHT", + asset_b_q_token_raw: "c45fa8aefc662c003a32be67f6a4652d8ce56bd9e54d7696efd40c86", + long_collateral_market_id: "NIGHT", + short_collateral_market_id: "Ada", + borrow_market_id_long: "Ada", + borrow_market_id_short: "NIGHT", + long_leverage: 1.5, + short_leverage: 0.5, + min_collateral: "200000000", // 200 ADA in lovelace + enable: true, + }, + ]) + .execute(); + + console.log("Seeded market_config table"); +} diff --git a/apps/long-short-backend/README.md b/apps/long-short-backend/README.md new file mode 100644 index 0000000..0302de5 --- /dev/null +++ b/apps/long-short-backend/README.md @@ -0,0 +1,372 @@ +# Long-Short Backend + +Cardano leveraged long/short trading backend. Users supply collateral, borrow via [Liqwid](https://liqwid.finance/) lending, and swap via [Minswap](https://minswap.org/) DEX to create leveraged positions. + +## Tech Stack + +- **Fastify** - HTTP server +- **Kysely** - PostgreSQL query builder (type-safe) +- **TypeBox** - Request/response schema validation +- **CIP-8** - Cardano message signing for authentication +- **Workspace packages**: `@repo/felis-ledger-core`, `@repo/felis-ledger-utils`, `@repo/felis-tx-builder`, `@repo/minswap-build-tx`, `@repo/minswap-lending-market` + +## Quick Start + +### Prerequisites + +- PostgreSQL (Docker or local) +- Node.js >= 22 +- pnpm 9+ + +### Setup + +```bash +# From monorepo root +pnpm install +pnpm build + +# Set environment variables (see below) + +# Run migrations +pnpm --filter=long-short-backend run run:migrate + +# Seed market config +pnpm --filter=long-short-backend run run:seed + +# Start dev server +pnpm --filter=long-short-backend dev +``` + +### Docker (PostgreSQL + Redis) + +```bash +# From monorepo root +docker compose up -d +``` + +## Environment Variables + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `DATABASE_URL` | Yes | - | PostgreSQL connection string | +| `CARDANOSCAN_API_KEY` | Yes | - | Cardanoscan API key | +| `NETWORK` | No | `mainnet` | `mainnet` or `testnet_preview` | +| `API_PORT` | No | `9999` | HTTP server port | +| `API_HOST` | No | `0.0.0.0` | HTTP server host | + +## Database Schema + +Three tables: `position`, `order`, `market_config`. + +### position + +| Column | Type | Description | +|--------|------|-------------| +| `id` | bigserial | Primary key | +| `market_id` | varchar | Market identifier (e.g. `ADA-NIGHT`) | +| `user_address` | varchar | Cardano bech32 address | +| `side` | varchar | `LONG` or `SHORT` | +| `status` | varchar | `PENDING` / `OPEN` / `CLOSING` / `CLOSED` | +| `amount_in` | numeric | Collateral amount (lovelace) | +| `amount_borrow` | numeric | Amount to borrow | +| `created_at` | timestamp | Position creation time | +| `closed_at` | timestamp | When position was closed (null if open) | + +Constraint: only one open position per user per market. + +### order + +Each position has a sequence of orders that execute in order. + +| Column | Type | Description | +|--------|------|-------------| +| `id` | bigserial | Primary key (also determines execution order) | +| `position_id` | bigint | FK to position | +| `order_type` | varchar | e.g. `LONG_BUY`, `SHORT_SUPPLY` | +| `asset_in` | varchar | Input asset (nullable, filled by prior order's waiting fn) | +| `amount_in` | numeric | Input amount (nullable) | +| `asset_out` | varchar | Output asset (nullable) | +| `amount_out` | numeric | Output amount (set after confirmation) | +| `built_tx_id` | varchar | Transaction hash after local build | +| `built_valid_to` | timestamp | Transaction expiry time | +| `created_tx_id` | varchar | Transaction hash when confirmed on-chain | +| `created_tx_index` | int | Output index in confirmed tx | +| `waiting` | boolean | `true` while waiting for output to be spent | + +### market_config + +| Column | Type | Description | +|--------|------|-------------| +| `market_id` | varchar | PK, e.g. `ADA-NIGHT` | +| `asset_a` | varchar | Base asset (`lovelace` for ADA) | +| `asset_b` | varchar | Quote asset (policyId.tokenName) | +| `amm_lp_asset` | varchar | Minswap LP token | +| `asset_a_q_token_ticker` | varchar | Liqwid qToken ticker for A (e.g. `qAda`) | +| `asset_a_q_token_raw` | varchar | Liqwid qToken policyId for A | +| `asset_b_q_token_ticker` | varchar | Liqwid qToken ticker for B (e.g. `qNIGHT`) | +| `asset_b_q_token_raw` | varchar | Liqwid qToken policyId for B | +| `long_collateral_market_id` | varchar | Liqwid market ID for LONG collateral | +| `short_collateral_market_id` | varchar | Liqwid market ID for SHORT collateral | +| `borrow_market_id_long` | varchar | Liqwid market ID for LONG borrow | +| `borrow_market_id_short` | varchar | Liqwid market ID for SHORT borrow | +| `long_leverage` | numeric | LONG leverage multiplier (e.g. 1.5) | +| `short_leverage` | numeric | SHORT leverage multiplier (e.g. 0.5) | +| `min_collateral` | numeric | Minimum collateral in lovelace | +| `enable` | boolean | Enable/disable market | + +## API Endpoints + +### Authentication + +All POST endpoints require CIP-8 signature authentication: + +```json +{ + "data": { "market_id": "ADA-NIGHT", "..." : "..." }, + "user_address": "addr1q...", + "witness": { + "key": "a40101...", + "signature": "844da2..." + } +} +``` + +The client signs `JSON.stringify(data)` using their wallet's payment key (CIP-8 format). The server verifies the signature matches the `user_address`. + +### Endpoints + +#### `GET /health` +Health check. Returns `{ status: "ok" }`. + +#### `GET /metadata` +Returns all enabled market configurations. No auth required. + +#### `POST /position/create` +Create a new leveraged position. + +```json +// Request data +{ "market_id": "ADA-NIGHT", "side": "SHORT", "amount_in": "600000000" } + +// Response +{ "success": true, "data": { "id": "1", "market_id": "ADA-NIGHT", "side": "SHORT", "status": "PENDING", ... } } +``` + +#### `GET /position/get?user_address=addr1q...` +Get user's open position. No auth required. + +#### `POST /position/build-tx` +Build the next transaction in the order sequence. Call repeatedly until position reaches target status. + +```json +// Request data +{ "market_id": "ADA-NIGHT", "utxos": ["828258..."] } + +// Response (transaction ready) +{ "success": true, "data": { "tx_raw": "84a4...", "tx_id": "abc123...", "order_type": "SHORT_SUPPLY" } } + +// Response (waiting for confirmation) +{ "success": true, "data": { "waiting": true, "order_type": "SHORT_SUPPLY", "message": "..." } } +``` + +#### `POST /position/close` +Initiate closing of an OPEN position. + +```json +// Request data +{ "market_id": "ADA-NIGHT" } +``` + +#### `POST /liqwid/submit` +Submit a signed Liqwid transaction. + +```json +// Request data +{ "raw_tx": "84a4...", "witness_set": "a100..." } +``` + +## Architecture + +``` +src/ +├── cmd/run-api.ts # Entry point: init WASM, DB, providers, start server +├── api/ +│ ├── server.ts # Fastify setup, route registration +│ ├── routes/ # HTTP endpoint handlers +│ ├── schemas.ts # TypeBox request/response schemas +│ ├── helper.ts # CIP-8 authentication +│ └── state-machine.ts # Order build & waiting logic (core) +├── config/market.ts # Market config loading & cache +├── database/ +│ ├── db.d.ts # Generated Kysely types +│ └── postgres.ts # DB connection +├── provider/ +│ ├── cardanoscan.ts # On-chain tx queries +│ ├── kupo.ts # UTXO indexer +│ └── minswap-aggregator.ts # Swap price estimation +├── repository/ # Data access layer +│ ├── position-repository.ts +│ └── order-repository.ts +├── services/ +│ └── position-service.ts # Business logic orchestration +└── utils/ # Logger, signature, hashing +``` + +### Layer Flow + +``` +HTTP Request → Route Handler → Authentication (CIP-8) + → PositionService (business logic) + → OrderRepository / PositionRepository (data access) + → StateMachine (tx building / waiting) + → Providers (Cardanoscan, Liqwid, Minswap Aggregator) +``` + +## State Machine + +The state machine is the core concept. Each position is a chain of orders that execute sequentially. The client calls `build-tx` repeatedly to advance through the chain. + +### Position Lifecycle + +``` +PENDING → OPEN → CLOSING → CLOSED +``` + +- **PENDING**: Orders created, waiting for all opening orders to complete +- **OPEN**: All opening orders complete, position is active +- **CLOSING**: Close requested, closing orders in progress +- **CLOSED**: All closing orders complete + +### LONG Order Flow + +**Opening** (4 orders): +``` +LONG_BUY → DEX swap: ADA → Asset B +LONG_SUPPLY → Supply Asset B to Liqwid → receive qB +LONG_BORROW → Borrow ADA using qB collateral +LONG_BUY_MORE → DEX swap: borrowed ADA → Asset B → position OPEN +``` + +**Closing** (4 orders): +``` +LONG_SELL → DEX swap: Asset B → ADA +LONG_REPAY → Repay ADA loan, redeem qB collateral +LONG_WITHDRAW → Withdraw Asset B from Liqwid +LONG_SELL_ALL → DEX swap: remaining Asset B → ADA → position CLOSED +``` + +### SHORT Order Flow + +**Opening** (3 orders): +``` +SHORT_SUPPLY → Supply ADA to Liqwid → receive qADA +SHORT_BORROW → Borrow Asset B using qADA collateral +SHORT_SELL → DEX swap: Asset B → ADA → position OPEN +``` + +**Closing** (3 orders): +``` +SHORT_BUY → DEX swap: ADA → Asset B (buy back) +SHORT_REPAY → Repay Asset B loan, redeem qADA collateral +SHORT_WITHDRAW → Withdraw ADA from Liqwid → position CLOSED +``` + +### Transaction Lifecycle + +Each order goes through this lifecycle: + +``` +1. build-tx called → StateMachine builds tx → built_tx_id set +2. Client signs & submits tx to chain +3. Next build-tx call → finds tx on-chain → created_tx_id set, waiting = true +4. Next build-tx call → waiting function checks if output is spent + → If spent: extract amounts, transition to next order (or complete position) + → If not spent: return "waiting" message +``` + +### How `buildTx()` Works + +``` +1. Check for waiting order (created_tx_id set, waiting = true) + → Call waiting function + → If confirmed: transition to next order or complete position + → If not confirmed: return waiting message + +2. Find next unhandled order (has assetIn/amountIn/assetOut, no created_tx_id) + +3. If order has built_tx_id: + → Search for it on-chain + → If found: set created_tx_id, return waiting + → If not found & expired: rebuild + → If not found & not expired: return waiting with remaining time + +4. Build new transaction using StateMachine handler + → Set built_tx_id and built_valid_to + → Return tx_raw for client to sign +``` + +## Market Configuration + +### Liqwid Integration Fields + +- **qToken ticker** (`asset_a_q_token_ticker`): Used as `marketId` when calling Liqwid supply API (e.g. `"qAda"`, `"qNIGHT"`) +- **qToken raw** (`asset_a_q_token_raw`): The on-chain policyId of the qToken, used for matching UTXOs and as `assetOut` in orders +- **Collateral market ID** (`long_collateral_market_id`): Liqwid market ID for withdraw (e.g. `"NIGHT"`, `"Ada"`) +- **Borrow market ID** (`borrow_market_id_long`): Liqwid market ID for borrow (e.g. `"Ada"`, `"NIGHT"`) + +### Leverage + +- **LONG**: `amount_borrow = amount_in * (leverage - 1) + 4 ADA fee` + - Example: 600 ADA at 1.5x → borrow 304 ADA +- **SHORT**: `amount_borrow` = aggregator estimate of `amount_in * leverage` ADA worth of Asset B + - Example: 600 ADA at 0.5x → estimate 300 ADA worth of NIGHT → borrow that amount + +## Development Guide + +### Scripts + +```bash +pnpm --filter=long-short-backend dev # Start with watch mode +pnpm --filter=long-short-backend build # TypeScript compile +pnpm --filter=long-short-backend test # Run tests +pnpm --filter=long-short-backend run run:migrate # Apply migrations +pnpm --filter=long-short-backend run run:seed # Seed market_config +pnpm --filter=long-short-backend run codegen # Regenerate db.d.ts from schema +``` + +### Adding a New Market + +1. Insert a row into `market_config` table (or add to seed file) +2. All Liqwid market IDs and qToken values must match the Liqwid protocol exactly (case-sensitive) +3. Restart server to reload market config cache + +### Running Migrations + +```bash +# Create new migration +# File: .config/migrations/{timestamp}_{description}.ts + +# Apply +pnpm --filter=long-short-backend run run:migrate + +# After migration, regenerate types +pnpm --filter=long-short-backend run codegen +``` + +### Key Files (in order of importance) + +1. **`src/api/state-machine.ts`** - Core order build & waiting logic +2. **`src/services/position-service.ts`** - Business logic orchestration +3. **`src/repository/order-repository.ts`** - Order data access and transitions +4. **`src/api/routes/position.ts`** - HTTP endpoint handlers +5. **`src/config/market.ts`** - Market config loading +6. **`src/provider/cardanoscan.ts`** - On-chain transaction queries +7. **`src/cmd/run-api.ts`** - App initialization sequence + +### Common Patterns + +- All Cardano amounts use `bigint` (lovelace), but Liqwid API uses `number` +- Repository functions accept `Kysely | Transaction` for transaction support +- Database columns are `snake_case`, TypeScript types are `camelCase` +- Addresses use `.toHex()` for Cardanoscan API, bech32 for Liqwid API +- Asset format: `policyId.tokenName` internally, `policyId + tokenName` (no dot) for Minswap aggregator, `"lovelace"` for ADA diff --git a/apps/long-short-backend/example/sign-data.ts b/apps/long-short-backend/example/sign-data.ts new file mode 100644 index 0000000..f138d23 --- /dev/null +++ b/apps/long-short-backend/example/sign-data.ts @@ -0,0 +1,52 @@ +import { baseAddressWalletFromSeed } from "@minswap/felis-cip"; +import { NetworkEnvironment } from "@minswap/felis-ledger-core"; +import { signData, verifySignData } from "../src/utils/signature"; +import { RustModule } from "@minswap/felis-ledger-utils"; +import invariant from "@minswap/tiny-invariant"; +import { HashUtils } from "../src/utils"; + +const main = async () => { + await RustModule.load(); + const seed = + "melt enemy surface feed kiss helmet suffer demise toilet insane human refuse park insect lawsuit custom inch spirit throw radio alarm creek chat symptom"; + const wallet = baseAddressWalletFromSeed( + seed, + NetworkEnvironment.TESTNET_PREVIEW, + ); + + const data = { + market: "ADA-MIN", + side: "LONG", + amount: "500000000", + }; + const message = Buffer.from(JSON.stringify(data)).toString("hex"); + const hashMessage = HashUtils.sha256(message); + console.log("json data", JSON.stringify(data)); + console.log(message); + // Sign the message + const { signature, key } = signData( + wallet.paymentKey, + wallet.address.bech32, + hashMessage, + ); + + const result = { + data, + user_address: wallet.address.bech32, + witness: { + key, + signature, + } + }; + console.log(JSON.stringify(result, null, 4)); + + const authenticated = verifySignData({ + message: hashMessage, + address: wallet.address.bech32, + key, + signature, + }); + invariant(authenticated, "Signature verification failed"); +}; + +main(); diff --git a/apps/long-short-backend/package.json b/apps/long-short-backend/package.json new file mode 100644 index 0000000..c410301 --- /dev/null +++ b/apps/long-short-backend/package.json @@ -0,0 +1,60 @@ +{ + "name": "long-short-backend", + "version": "0.1.0", + "private": true, + "exports": { + ".": "./dist/index.js" + }, + "scripts": { + "build": "tsc", + "start": "node --import tsx src/cmd/run-api.ts", + "dev": "node --import tsx --watch src/cmd/run-api.ts", + "test": "vitest", + "run:migrate": "pnpm kysely migrate:latest", + "run:seed": "pnpm kysely seed:run", + "codegen": "kysely-codegen --exclude-pattern=\"*timescale*.*\" --out-file=src/database/db.d.ts" + }, + "devDependencies": { + "@cardano-ogmios/schema": "^6.11.0", + "@repo/eslint-config": "workspace:*", + "@repo/typescript-config": "workspace:*", + "@types/crypto-js": "^4.2.2", + "@types/json-bigint": "^1.0.4", + "@types/node": "^22.15.3", + "@types/pg": "^8.16.0", + "dpdm": "^3.14.0", + "eslint": "^9.31.0", + "kysely-codegen": "^0.19.0", + "kysely-ctl": "^0.19.0", + "tsx": "^4.19.4", + "typescript": "5.8.2", + "vitest": "^3.2.4" + }, + "dependencies": { + "@cardano-ogmios/client": "^6.14.0", + "@emurgo/cardano-message-signing-nodejs": "^1.0.1", + "@fastify/cors": "^10.0.2", + "@minswap/felis-build-tx": "workspace:*", + "@minswap/felis-cip": "workspace:*", + "@minswap/felis-dex-v1": "workspace:*", + "@minswap/felis-dex-v2": "workspace:*", + "@minswap/felis-ledger-core": "workspace:*", + "@minswap/felis-ledger-utils": "workspace:*", + "@minswap/felis-tx-builder": "workspace:*", + "@minswap/felis-lending-market": "workspace:*", + "@minswap/tiny-invariant": "^1.2.0", + "@sinclair/typebox": "^0.34.33", + "@types/bun": "^1.3.5", + "bignumber.js": "^9.1.2", + "bip39": "^3.1.0", + "crypto-js": "^4.2.0", + "exponential-backoff": "^3.1.3", + "fastify": "^5.2.2", + "ioredis": "^5.9.0", + "kysely": "^0.28.11", + "p-timeout": "^7.0.1", + "pg": "^8.16.3", + "remeda": "^2.33.1", + "socket.io-client": "^4.8.3" + } +} diff --git a/apps/long-short-backend/src/api/helper.ts b/apps/long-short-backend/src/api/helper.ts new file mode 100644 index 0000000..c46d6a4 --- /dev/null +++ b/apps/long-short-backend/src/api/helper.ts @@ -0,0 +1,33 @@ +import { HashUtils } from "../utils"; +import { verifySignData } from "../utils/signature"; +import type { SignedDataType } from "./schemas"; + +export namespace ApiHelper { + export type AuthenticateResult = { success: true } | { success: false; error: string }; + + /** + * Authenticate a request by verifying the witness signature + * The witness must be the signed data of JSON.stringify(data) + * + * @param data - The data object that was signed + * @param userAddress - The user's Cardano address (bech32) + * @param witness - The signed data (COSEKey and COSESign1) + */ + export function authenticate(data: object, userAddress: string, witness: SignedDataType): AuthenticateResult { + const message = Buffer.from(JSON.stringify(data)).toString("hex"); + const hashMessage = HashUtils.sha256(message); + + const isValid = verifySignData({ + message: hashMessage, + address: userAddress, + key: witness.key, + signature: witness.signature, + }); + + if (!isValid) { + return { success: false, error: "Invalid authentication signature" }; + } + + return { success: true }; + } +} diff --git a/apps/long-short-backend/src/api/index.ts b/apps/long-short-backend/src/api/index.ts new file mode 100644 index 0000000..e584dca --- /dev/null +++ b/apps/long-short-backend/src/api/index.ts @@ -0,0 +1,2 @@ +export * from "./schemas"; +export { type ApiServerOptions, createApiServer } from "./server"; diff --git a/apps/long-short-backend/src/api/routes/liqwid.ts b/apps/long-short-backend/src/api/routes/liqwid.ts new file mode 100644 index 0000000..62ae379 --- /dev/null +++ b/apps/long-short-backend/src/api/routes/liqwid.ts @@ -0,0 +1,94 @@ +import type { NetworkEnvironment } from "@minswap/felis-ledger-core"; +import { LiqwidProvider } from "@minswap/felis-lending-market"; +import type { FastifyInstance } from "fastify"; +import { API_ENDPOINTS } from "../../constants"; +import { logger } from "../../utils"; +import { ApiHelper } from "../helper"; +import { + type AuthenLiqwidSubmitBodyType, + AuthenLiqwidSubmitBodyTypeSchema, + ErrorResponseSchema, + LiqwidSubmitResponseSchema, + type LiqwidSubmitResponseType, +} from "../schemas"; + +export function registerLiqwidRoutes(fastify: FastifyInstance, networkEnv: NetworkEnvironment): void { + // POST /liqwid/submit + fastify.post<{ + Body: AuthenLiqwidSubmitBodyType; + Reply: LiqwidSubmitResponseType; + }>( + API_ENDPOINTS.LIQWID_SUBMIT, + { + schema: { + body: AuthenLiqwidSubmitBodyTypeSchema, + response: { + 200: LiqwidSubmitResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + }, + }, + }, + async (request, reply) => { + const { data, user_address, witness } = request.body; + const { raw_tx, witness_set } = data; + + // Authenticate request + const authResult = ApiHelper.authenticate(data, user_address, witness); + if (!authResult.success) { + return reply.status(401).send({ + success: false, + error: authResult.error, + }); + } + + logger.info("Submitting Liqwid transaction", { + userAddress: user_address, + rawTxLength: raw_tx.length, + witnessSetLength: witness_set.length, + }); + + try { + // Submit transaction to Liqwid + const submitResult = await LiqwidProvider.submitTransaction({ + transaction: raw_tx, + signature: witness_set, + networkEnv, + }); + + if (submitResult.type === "err") { + logger.error("Failed to submit Liqwid transaction", { + error: submitResult.error.message, + userAddress: user_address, + }); + return reply.status(400).send({ + success: false, + error: submitResult.error.message, + }); + } + + const txHash = submitResult.value; + logger.info("Liqwid transaction submitted successfully", { + txHash, + userAddress: user_address, + }); + + return reply.status(200).send({ + success: true, + data: { + tx_hash: txHash, + }, + }); + } catch (error) { + logger.error("Exception submitting Liqwid transaction", { + error, + userAddress: user_address, + }); + return reply.status(400).send({ + success: false, + error: error instanceof Error ? error.message : "Failed to submit transaction", + }); + } + }, + ); +} diff --git a/apps/long-short-backend/src/api/routes/metadata.ts b/apps/long-short-backend/src/api/routes/metadata.ts new file mode 100644 index 0000000..d96f2e7 --- /dev/null +++ b/apps/long-short-backend/src/api/routes/metadata.ts @@ -0,0 +1,75 @@ +import type { NetworkEnvironment } from "@minswap/felis-ledger-core"; +import { LiqwidProviderV2 } from "@minswap/felis-lending-market"; +import type { FastifyInstance } from "fastify"; +import { getEnabledMarketConfigs, type MarketConfig } from "../../config/market"; +import { API_ENDPOINTS } from "../../constants"; +import { logger } from "../../utils"; +import { type MarketConfigResponseType, MetadataResponseSchema, type MetadataResponseType } from "../schemas"; + +type LiqwidMarketApyMap = Map; + +function marketConfigToResponse(config: MarketConfig, liqwidApys: LiqwidMarketApyMap): MarketConfigResponseType { + const assetAApys = liqwidApys.get(config.borrowMarketIdLong); + const assetBApys = liqwidApys.get(config.borrowMarketIdShort); + + return { + market_id: config.marketId, + asset_a: config.assetA.toString(), + asset_b: config.assetB.toString(), + amm_lp_asset: config.ammLpAsset, + asset_a_q_token_ticker: config.assetAQTokenTicker, + asset_a_q_token_raw: config.assetAQTokenRaw, + asset_b_q_token_ticker: config.assetBQTokenTicker, + asset_b_q_token_raw: config.assetBQTokenRaw, + long_collateral_market_id: config.longCollateralMarketId, + short_collateral_market_id: config.shortCollateralMarketId, + long_leverage: config.longLeverage, + short_leverage: config.shortLeverage, + min_collateral: config.minCollateral.toString(), + asset_a_borrow_apy: assetAApys?.borrowAPY ?? null, + asset_a_supply_apy: assetAApys?.supplyAPY ?? null, + asset_b_borrow_apy: assetBApys?.borrowAPY ?? null, + asset_b_supply_apy: assetBApys?.supplyAPY ?? null, + }; +} + +async function fetchLiqwidMarketApys(networkEnv: NetworkEnvironment): Promise { + const apyMap: LiqwidMarketApyMap = new Map(); + const config = LiqwidProviderV2.createConfig(networkEnv); + const result = await LiqwidProviderV2.Data.markets(config); + if (result.type === "ok") { + for (const market of result.value.results) { + apyMap.set(market.id, { borrowAPY: market.borrowAPY, supplyAPY: market.supplyAPY }); + } + } else { + logger.error("Failed to fetch Liqwid market data", { error: result.error }); + } + return apyMap; +} + +export function registerMetadataRoutes(fastify: FastifyInstance, networkEnv: NetworkEnvironment): void { + // GET /metadata + fastify.get<{ + Reply: MetadataResponseType; + }>( + API_ENDPOINTS.METADATA, + { + schema: { + response: { + 200: MetadataResponseSchema, + }, + }, + }, + async (_request, reply) => { + const marketConfigs = getEnabledMarketConfigs(); + const liqwidApys = await fetchLiqwidMarketApys(networkEnv); + + return reply.status(200).send({ + success: true, + data: { + markets: marketConfigs.map((c) => marketConfigToResponse(c, liqwidApys)), + }, + }); + }, + ); +} diff --git a/apps/long-short-backend/src/api/routes/position.ts b/apps/long-short-backend/src/api/routes/position.ts new file mode 100644 index 0000000..a933c85 --- /dev/null +++ b/apps/long-short-backend/src/api/routes/position.ts @@ -0,0 +1,237 @@ +import invariant from "@minswap/tiny-invariant"; +import type { FastifyInstance } from "fastify"; +import { API_ENDPOINTS } from "../../constants"; +import type { Position } from "../../repository/position-repository"; +import type { PositionService } from "../../services/position-service"; +import { ApiHelper } from "../helper"; +import { + type AuthenBuildTxBodyType, + AuthenBuildTxBodyTypeSchema, + type AuthenClosePositionBodyType, + AuthenClosePositionBodyTypeSchema, + type AuthenCreatePositionBodyType, + AuthenCreatePositionBodyTypeSchema, + BuildTxResponseSchema, + type BuildTxResponseType, + ClosePositionResponseSchema, + type ClosePositionResponseType, + CreatePositionResponseSchema, + type CreatePositionResponseType, + ErrorResponseSchema, + GetPositionQuerySchema, + type GetPositionQueryType, + GetPositionResponseSchema, + type GetPositionResponseType, + type PositionResponseType, +} from "../schemas"; + +function positionToResponse(position: Position): PositionResponseType { + return { + id: position.id.toString(), + market_id: position.marketId, + user_address: position.userAddress, + side: position.side, + status: position.status, + amount_in: position.amountIn, + amount_borrow: position.amountBorrow, + created_at: position.createdAt.toISOString(), + closed_at: position.closedAt?.toISOString() ?? null, + }; +} + +export function registerPositionRoutes(fastify: FastifyInstance, positionService: PositionService): void { + // GET /position/get?user_address=... + fastify.get<{ + Querystring: GetPositionQueryType; + Reply: GetPositionResponseType; + }>( + API_ENDPOINTS.POSITION_GET, + { + schema: { + querystring: GetPositionQuerySchema, + response: { + 200: GetPositionResponseSchema, + 400: ErrorResponseSchema, + }, + }, + }, + async (request, reply) => { + const { user_address } = request.query; + + const position = await positionService.getOpenPositionByUser(user_address); + + return reply.status(200).send({ + success: true, + data: position ? positionToResponse(position) : null, + }); + }, + ); + + // POST /position/create + fastify.post<{ + Body: AuthenCreatePositionBodyType; + Reply: CreatePositionResponseType; + }>( + API_ENDPOINTS.POSITION_CREATE, + { + schema: { + body: AuthenCreatePositionBodyTypeSchema, + response: { + 200: CreatePositionResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + }, + }, + }, + async (request, reply) => { + const { data, user_address, witness } = request.body; + const { market_id, side, amount_in } = data; + + // Authenticate request + const authResult = ApiHelper.authenticate(data, user_address, witness); + if (!authResult.success) { + return reply.status(401).send({ + success: false, + error: authResult.error, + }); + } + + // Create position via service + const result = await positionService.createPosition({ + userAddress: user_address, + marketId: market_id, + side, + amountIn: BigInt(amount_in), + }); + + if (!result.success) { + return reply.status(400).send({ + success: false, + error: result.error, + }); + } + + return reply.status(200).send({ + success: true, + data: positionToResponse(result.position), + }); + }, + ); + + // POST /position/build-tx + fastify.post<{ + Body: AuthenBuildTxBodyType; + Reply: BuildTxResponseType; + }>( + API_ENDPOINTS.POSITION_BUILD_TX, + { + schema: { + body: AuthenBuildTxBodyTypeSchema, + response: { + 200: BuildTxResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + }, + }, + }, + async (request, reply) => { + const { data, user_address, witness } = request.body; + const { market_id, utxos } = data; + + // Authenticate request + const authResult = ApiHelper.authenticate(data, user_address, witness); + if (!authResult.success) { + return reply.status(401).send({ + success: false, + error: authResult.error, + }); + } + + // Build transaction via service + const result = await positionService.buildTx({ + userAddress: user_address, + marketId: market_id, + utxos, + }); + + if (!result.success) { + return reply.status(400).send({ + success: false, + error: result.error, + }); + } + + // Handle waiting state (transaction already built, waiting for confirmation) + if ("waiting" in result && result.waiting) { + return reply.status(200).send({ + success: true, + data: { + order_type: result.orderType, + waiting: true, + message: result.message, + }, + }); + } + + // Return newly built transaction + invariant("txRaw" in result && result.txRaw && "txId" in result && result.txId, "type-safe"); + return reply.status(200).send({ + success: true, + data: { + tx_raw: result.txRaw, + tx_id: result.txId, + order_type: result.orderType, + }, + }); + }, + ); + + // POST /position/close + fastify.post<{ + Body: AuthenClosePositionBodyType; + Reply: ClosePositionResponseType; + }>( + API_ENDPOINTS.POSITION_CLOSE, + { + schema: { + body: AuthenClosePositionBodyTypeSchema, + response: { + 200: ClosePositionResponseSchema, + 400: ErrorResponseSchema, + 401: ErrorResponseSchema, + }, + }, + }, + async (request, reply) => { + const { data, user_address, witness } = request.body; + const { market_id } = data; + + // Authenticate request + const authResult = ApiHelper.authenticate(data, user_address, witness); + if (!authResult.success) { + return reply.status(401).send({ + success: false, + error: authResult.error, + }); + } + + // Close position via service + const result = await positionService.closePosition({ + userAddress: user_address, + marketId: market_id, + }); + + if (!result.success) { + return reply.status(400).send({ + success: false, + error: result.error, + }); + } + + return reply.status(200).send({ + success: true, + data: positionToResponse(result.position), + }); + }, + ); +} diff --git a/apps/long-short-backend/src/api/schemas.ts b/apps/long-short-backend/src/api/schemas.ts new file mode 100644 index 0000000..1f4e295 --- /dev/null +++ b/apps/long-short-backend/src/api/schemas.ts @@ -0,0 +1,194 @@ +import { type Static, Type } from "@sinclair/typebox"; +import { StateMachine } from "./state-machine"; + +export const SignedDataSchema = Type.Object({ + key: Type.String({ minLength: 1, description: "COSEKey hex" }), + signature: Type.String({ minLength: 1, description: "COSESign1 hex" }), +}); + +export type SignedDataType = Static; + +// Common authenticated request schema +export const AuthenCommonSchema = >(dataSchema: T) => + Type.Object({ + data: dataSchema, + user_address: Type.String({ minLength: 1, description: "User's Cardano address (bech32)" }), + witness: SignedDataSchema, + }); + +export type AuthenCommonType = { + data: T; + user_address: string; + witness: SignedDataType; +}; + +// Position schemas (derived from StateMachine enums) +export const PositionSideSchema = Type.Union(Object.values(StateMachine.PositionSide).map((v) => Type.Literal(v))); +export const PositionStatusSchema = Type.Union(Object.values(StateMachine.PositionStatus).map((v) => Type.Literal(v))); + +export const CreatePositionDataSchema = Type.Object({ + market_id: Type.String({ minLength: 1, description: "Market ID (e.g., ADA-MIN)" }), + side: Type.Union([Type.Literal("LONG"), Type.Literal("SHORT")], { description: "Position side" }), + amount_in: Type.String({ pattern: "^[0-9]+$", description: "Collateral amount in lovelace" }), +}); + +export type CreatePositionDataType = Static; + +export const AuthenCreatePositionBodyTypeSchema = AuthenCommonSchema(CreatePositionDataSchema); + +export type AuthenCreatePositionBodyType = AuthenCommonType; + +export const PositionResponseSchema = Type.Object({ + id: Type.String({ description: "Position ID" }), + market_id: Type.String(), + user_address: Type.String(), + side: PositionSideSchema, + status: PositionStatusSchema, + amount_in: Type.String(), + amount_borrow: Type.String(), + created_at: Type.String({ format: "date-time" }), + closed_at: Type.Union([Type.String({ format: "date-time" }), Type.Null()]), +}); + +export type PositionResponseType = Static; + +export const CreatePositionResponseSchema = Type.Object({ + success: Type.Boolean(), + data: Type.Optional(PositionResponseSchema), + error: Type.Optional(Type.String()), +}); + +export type CreatePositionResponseType = Static; + +// Get position schemas +export const GetPositionQuerySchema = Type.Object({ + user_address: Type.String({ minLength: 1, description: "User's Cardano address (bech32)" }), +}); + +export type GetPositionQueryType = Static; + +export const GetPositionResponseSchema = Type.Object({ + success: Type.Boolean(), + data: Type.Union([PositionResponseSchema, Type.Null()]), +}); + +export type GetPositionResponseType = Static; + +// Build TX schemas +export const BuildTxDataSchema = Type.Object({ + market_id: Type.String({ description: "Market identifier (e.g., ADA-MIN)" }), + utxos: Type.Array(Type.String({ minLength: 1 }), { description: "User UTXOs (CBOR hex)" }), +}); + +export type BuildTxDataType = Static; + +export const AuthenBuildTxBodyTypeSchema = AuthenCommonSchema(BuildTxDataSchema); + +export type AuthenBuildTxBodyType = AuthenCommonType; + +export const BuildTxResponseSchema = Type.Object({ + success: Type.Boolean(), + data: Type.Optional( + Type.Object({ + tx_raw: Type.Optional(Type.String({ description: "Unsigned transaction CBOR hex" })), + tx_id: Type.Optional(Type.String({ description: "Transaction ID (hash)" })), + order_type: Type.String({ description: "Order type being processed" }), + waiting: Type.Optional(Type.Boolean({ description: "True if transaction is waiting for confirmation" })), + message: Type.Optional(Type.String({ description: "Status message when waiting" })), + }), + ), + error: Type.Optional(Type.String()), +}); + +export type BuildTxResponseType = Static; + +// Close position schemas +export const ClosePositionDataSchema = Type.Object({ + market_id: Type.String({ minLength: 1, description: "Market ID (e.g., ADA-MIN)" }), +}); + +export type ClosePositionDataType = Static; + +export const AuthenClosePositionBodyTypeSchema = AuthenCommonSchema(ClosePositionDataSchema); + +export type AuthenClosePositionBodyType = AuthenCommonType; + +export const ClosePositionResponseSchema = Type.Object({ + success: Type.Boolean(), + data: Type.Optional(PositionResponseSchema), + error: Type.Optional(Type.String()), +}); + +export type ClosePositionResponseType = Static; + +// Error response +export const ErrorResponseSchema = Type.Object({ + success: Type.Literal(false), + error: Type.String(), +}); + +export type ErrorResponseType = Static; + +// Liqwid submit schemas +export const LiqwidSubmitDataSchema = Type.Object({ + raw_tx: Type.String({ minLength: 1, description: "Raw transaction CBOR hex" }), + witness_set: Type.String({ minLength: 1, description: "Witness set CBOR hex" }), +}); + +export type LiqwidSubmitDataType = Static; + +export const AuthenLiqwidSubmitBodyTypeSchema = AuthenCommonSchema(LiqwidSubmitDataSchema); + +export type AuthenLiqwidSubmitBodyType = AuthenCommonType; + +export const LiqwidSubmitResponseSchema = Type.Object({ + success: Type.Boolean(), + data: Type.Optional( + Type.Object({ + tx_hash: Type.String({ description: "Submitted transaction hash" }), + }), + ), + error: Type.Optional(Type.String()), +}); + +export type LiqwidSubmitResponseType = Static; + +// Market config schemas +export const MarketConfigResponseSchema = Type.Object({ + market_id: Type.String({ description: "Market identifier (e.g., ADA-MIN)" }), + asset_a: Type.String({ description: "First asset" }), + asset_b: Type.String({ description: "Second asset" }), + amm_lp_asset: Type.String({ description: "Minswap LP token" }), + asset_a_q_token_ticker: Type.String({ description: "Liqwid qToken ticker for asset A" }), + asset_a_q_token_raw: Type.String({ description: "Liqwid qToken raw asset for asset A" }), + asset_b_q_token_ticker: Type.String({ description: "Liqwid qToken ticker for asset B" }), + asset_b_q_token_raw: Type.String({ description: "Liqwid qToken raw asset for asset B" }), + long_collateral_market_id: Type.String({ description: "Liqwid market ID for long collateral" }), + short_collateral_market_id: Type.String({ description: "Liqwid market ID for short collateral" }), + long_leverage: Type.Number({ description: "Long leverage multiplier" }), + short_leverage: Type.Number({ description: "Short leverage multiplier" }), + min_collateral: Type.String({ description: "Minimum collateral in lovelace" }), + asset_a_borrow_apy: Type.Union([Type.Number(), Type.Null()], { + description: "Liqwid borrow APY for asset A", + }), + asset_a_supply_apy: Type.Union([Type.Number(), Type.Null()], { + description: "Liqwid supply APY for asset A", + }), + asset_b_borrow_apy: Type.Union([Type.Number(), Type.Null()], { + description: "Liqwid borrow APY for asset B", + }), + asset_b_supply_apy: Type.Union([Type.Number(), Type.Null()], { + description: "Liqwid supply APY for asset B", + }), +}); + +export type MarketConfigResponseType = Static; + +export const MetadataResponseSchema = Type.Object({ + success: Type.Boolean(), + data: Type.Object({ + markets: Type.Array(MarketConfigResponseSchema), + }), +}); + +export type MetadataResponseType = Static; diff --git a/apps/long-short-backend/src/api/server.ts b/apps/long-short-backend/src/api/server.ts new file mode 100644 index 0000000..7170851 --- /dev/null +++ b/apps/long-short-backend/src/api/server.ts @@ -0,0 +1,61 @@ +import cors from "@fastify/cors"; +import type { NetworkEnvironment } from "@minswap/felis-ledger-core"; +import Fastify, { type FastifyInstance } from "fastify"; +import type { Kysely } from "kysely"; +import { API_ENDPOINTS } from "../constants"; +import type { DB } from "../database"; +import { type CardanoscanProvider, MinswapAggregatorProvider } from "../provider"; +import { PositionService } from "../services/position-service"; +import { logger } from "../utils"; +import { registerLiqwidRoutes } from "./routes/liqwid"; +import { registerMetadataRoutes } from "./routes/metadata"; +import { registerPositionRoutes } from "./routes/position"; + +export type ApiServerOptions = { + port: number; + host: string; + db: Kysely; + cardanoscanProvider: CardanoscanProvider; + networkEnv: NetworkEnvironment; +}; + +export async function createApiServer(options: ApiServerOptions): Promise { + const { port, host, db, networkEnv, cardanoscanProvider } = options; + + const fastify = Fastify({ + logger: { + level: "info", + }, + }); + + // Register CORS + await fastify.register(cors, { + origin: true, + methods: ["GET", "POST", "PUT", "DELETE"], + }); + + // Health check endpoint (disable logging to reduce noise) + fastify.get(API_ENDPOINTS.HEALTH, { logLevel: "silent" }, async () => { + return { status: "ok" }; + }); + + // Create services + const aggregatorProvider = new MinswapAggregatorProvider(networkEnv); + const positionService = new PositionService(db, networkEnv, cardanoscanProvider, aggregatorProvider); + + // Register routes + registerLiqwidRoutes(fastify, networkEnv); + registerMetadataRoutes(fastify, networkEnv); + registerPositionRoutes(fastify, positionService); + + // Start server + try { + await fastify.listen({ port, host }); + logger.info(`API server listening on ${host}:${port}`); + } catch (err) { + logger.error("Failed to start API server", { error: err }); + throw err; + } + + return fastify; +} diff --git a/apps/long-short-backend/src/api/state-machine.ts b/apps/long-short-backend/src/api/state-machine.ts new file mode 100644 index 0000000..d50cddd --- /dev/null +++ b/apps/long-short-backend/src/api/state-machine.ts @@ -0,0 +1,1240 @@ +import { DEXOrderTransaction } from "@minswap/felis-build-tx"; +import { DexVersion, OrderV2Direction, OrderV2StepType } from "@minswap/felis-dex-v2"; +import { Address, Asset, getTimeFromSlotMagic, type NetworkEnvironment, Utxo } from "@minswap/felis-ledger-core"; +import { Duration, Maybe, RustModule, safeFreeRustObjects } from "@minswap/felis-ledger-utils"; +import { LiqwidProvider, LiqwidProviderV2 } from "@minswap/felis-lending-market"; +import { CoinSelectionAlgorithm, EmulatorProvider } from "@minswap/felis-tx-builder"; +import invariant from "@minswap/tiny-invariant"; +import type { MarketConfig } from "../config"; +import type { CardanoscanProvider } from "../provider"; +export namespace StateMachine { + export enum PositionSide { + LONG = "LONG", + SHORT = "SHORT", + } + + export enum PositionStatus { + PENDING = "PENDING", + OPEN = "OPEN", + CLOSING = "CLOSING", + CLOSED = "CLOSED", + } + + export enum LongOrderType { + LONG_BUY = "LONG_BUY", + LONG_SUPPLY = "LONG_SUPPLY", + LONG_BORROW = "LONG_BORROW", + LONG_BUY_MORE = "LONG_BUY_MORE", + LONG_SELL = "LONG_SELL", + LONG_REPAY = "LONG_REPAY", + LONG_WITHDRAW = "LONG_WITHDRAW", + LONG_SELL_ALL = "LONG_SELL_ALL", + } + + export enum ShortOrderType { + SHORT_SUPPLY = "SHORT_SUPPLY", + SHORT_BORROW = "SHORT_BORROW", + SHORT_SELL = "SHORT_SELL", + SHORT_BUY = "SHORT_BUY", + SHORT_REPAY = "SHORT_REPAY", + SHORT_WITHDRAW = "SHORT_WITHDRAW", + } + + export type BuiltResult = { + txRaw: string; + txId: string; + validTo: number; + }; + + // Common order data type for all Handle functions + export type OrderData = { + orderType: string; + assetIn: string | null; + amountIn: string | null; + assetOut: string | null; + }; + + // Common options for all Handle functions + export type HandleBuildTxOptions = { + order: OrderData; + marketConfig: MarketConfig; + userAddress: string; + networkEnv: NetworkEnvironment; + utxos: string[]; + /** Amount to borrow (used for LONG_BORROW / SHORT_BORROW) */ + amountBorrow?: string; + /** Loan transaction ID (used for LONG_REPAY / SHORT_REPAY to identify the loan) */ + loanTxId?: string; + /** Loan output index (used for LONG_REPAY / SHORT_REPAY) */ + loanOutputIndex?: number; + /** Collateral qToken amount (used for LONG_REPAY / SHORT_REPAY to redeem collateral) */ + collateralAmount?: string; + /** Supply amountOut from SUPPLY order (used for LONG_WITHDRAW / SHORT_WITHDRAW) */ + supplyAmountOut?: string; + }; + + export const handleLongBuy = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos } = options; + invariant( + order.orderType === LongOrderType.LONG_BUY || order.orderType === LongOrderType.LONG_BUY_MORE, + "Invalid order type for handleLongBuy", + ); + invariant(order.assetIn, "assetIn is required for LONG_BUY order"); + invariant(order.amountIn, "amountIn is required for LONG_BUY order"); + invariant(order.assetOut, "assetOut is required for LONG_BUY order"); + const walletUtxos: Utxo[] = utxos.map((u) => Utxo.fromHex(u)); + const sender = Address.fromBech32(userAddress); + + const txb = DEXOrderTransaction.createBulkOrdersTx({ + networkEnv, + sender, + orderOptions: [ + { + lpAsset: Asset.fromString(marketConfig.ammLpAsset), + version: DexVersion.DEX_V2, + type: OrderV2StepType.SWAP_EXACT_IN, + assetIn: marketConfig.assetA, + amountIn: BigInt(order.amountIn), + minimumAmountOut: 1n, + direction: OrderV2Direction.A_TO_B, + killOnFailed: false, + isLimitOrder: false, + }, + ], + }); + const validTo = Date.now() + Duration.newMinutes(3).milliseconds; + txb.validToUnixTime(validTo); + + const { txComplete, txId } = await txb.completeUnsafeForTxChaining({ + coinSelectionAlgorithm: CoinSelectionAlgorithm.SPEND_ALL, + walletUtxos, + changeAddress: sender, + provider: new EmulatorProvider(networkEnv), + }); + const txRaw = txComplete.complete(); + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + safeFreeRustObjects(eTx); + + return { + txRaw, + txId: txId, + validTo, + }; + }; + + export const handleLongSupply = async (options: HandleBuildTxOptions): Promise => { + const { order, userAddress, networkEnv, utxos } = options; + invariant(order.orderType === LongOrderType.LONG_SUPPLY, "Invalid order type for handleLongSupply"); + invariant(order.assetIn, "assetIn is required for LONG_SUPPLY order"); + invariant(order.amountIn, "amountIn is required for LONG_SUPPLY order"); + invariant(order.assetOut, "assetOut is required for LONG_SUPPLY order"); + + // assetOut contains the lending market ID (collateral token qMIN or qADA) + // We need to extract the market ID from the assetOut + // For example: "186cd98a29585651c89f05807a876cf26cdf47a7f86f70be3b9e4cc0" -> "MIN" + const marketId = order.assetOut as LiqwidProvider.MarketId; + + const buildTxResult = await LiqwidProvider.getSupplyTransaction({ + marketId, + amount: Number(order.amountIn), + address: userAddress, + utxos, + networkEnv, + }); + + if (buildTxResult.type === "err") { + throw new Error(`Failed to build supply transaction: ${buildTxResult.error.message}`); + } + + const txRaw = buildTxResult.value; + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + const txBody = eTx.body(); + const ttl = txBody.ttl(); + invariant(Maybe.isJust(ttl), "TTL must be set in the transaction body"); + safeFreeRustObjects(eTx, txBody); + + const validTo = getTimeFromSlotMagic(networkEnv, ttl); + const txId = LiqwidProvider.getLiqwidTxHash(txRaw); + + return { + txRaw, + txId, + validTo: validTo.getTime(), + }; + }; + + export const handleLongBorrow = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos, amountBorrow } = options; + invariant(order.orderType === LongOrderType.LONG_BORROW, "Invalid order type for handleLongBorrow"); + invariant(order.assetIn, "assetIn is required for LONG_BORROW order"); + invariant(order.amountIn, "amountIn is required for LONG_BORROW order"); + invariant(amountBorrow, "amountBorrow is required for LONG_BORROW order"); + + const apiConfig = LiqwidProviderV2.createConfig(networkEnv); + + const buildTxResult = await LiqwidProviderV2.Transactions.borrow(apiConfig, { + address: userAddress, + utxos, + marketId: marketConfig.borrowMarketIdLong as LiqwidProviderV2.MarketId, + amount: Number(amountBorrow), + collaterals: [ + { + id: marketConfig.assetBQTokenTicker, + amount: Number(order.amountIn), + }, + ], + }); + + if (buildTxResult.type === "err") { + throw new Error(`Failed to build borrow transaction: ${buildTxResult.error.message}`); + } + + const txRaw = buildTxResult.value; + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + const txBody = eTx.body(); + const ttl = txBody.ttl(); + invariant(Maybe.isJust(ttl), "TTL must be set in the transaction body"); + safeFreeRustObjects(eTx, txBody); + + const validTo = getTimeFromSlotMagic(networkEnv, ttl); + const txId = LiqwidProviderV2.getTxHash(txRaw); + + return { + txRaw, + txId, + validTo: validTo.getTime(), + }; + }; + + /** + * Build LONG_SELL or LONG_SELL_ALL transaction: Sell asset B for asset A (B_TO_A swap via DEX) + */ + export const handleLongSell = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos } = options; + invariant( + order.orderType === LongOrderType.LONG_SELL || order.orderType === LongOrderType.LONG_SELL_ALL, + "Invalid order type for handleLongSell", + ); + invariant(order.assetIn, "assetIn is required for LONG_SELL order"); + invariant(order.amountIn, "amountIn is required for LONG_SELL order"); + invariant(order.assetOut, "assetOut is required for LONG_SELL order"); + + const walletUtxos: Utxo[] = utxos.map((u) => Utxo.fromHex(u)); + const sender = Address.fromBech32(userAddress); + + const txb = DEXOrderTransaction.createBulkOrdersTx({ + networkEnv, + sender, + orderOptions: [ + { + lpAsset: Asset.fromString(marketConfig.ammLpAsset), + version: DexVersion.DEX_V2, + type: OrderV2StepType.SWAP_EXACT_IN, + assetIn: marketConfig.assetB, + amountIn: BigInt(order.amountIn), + minimumAmountOut: 1n, + direction: OrderV2Direction.B_TO_A, + killOnFailed: false, + isLimitOrder: false, + }, + ], + }); + const validTo = Date.now() + Duration.newMinutes(3).milliseconds; + txb.validToUnixTime(validTo); + + const { txComplete, txId } = await txb.completeUnsafeForTxChaining({ + coinSelectionAlgorithm: CoinSelectionAlgorithm.SPEND_ALL, + walletUtxos, + changeAddress: sender, + provider: new EmulatorProvider(networkEnv), + }); + const txRaw = txComplete.complete(); + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + safeFreeRustObjects(eTx); + + return { + txRaw, + txId: txId, + validTo, + }; + }; + + /** + * Build LONG_REPAY transaction: Repay loan to Liqwid and redeem collateral + * Uses repayLoan API with loanUtxoId format: "{txHash}-{outputIndex}" + */ + export const handleLongRepay = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos, loanTxId, loanOutputIndex, collateralAmount } = + options; + invariant(order.orderType === LongOrderType.LONG_REPAY, "Invalid order type for handleLongRepay"); + invariant(order.assetIn, "assetIn is required for LONG_REPAY order"); + invariant(order.amountIn, "amountIn is required for LONG_REPAY order"); + invariant(loanTxId, "loanTxId is required for LONG_REPAY order"); + invariant(loanOutputIndex !== undefined, "loanOutputIndex is required for LONG_REPAY order"); + invariant(collateralAmount, "collateralAmount is required for LONG_REPAY order"); + + const apiConfig = LiqwidProviderV2.createConfig(networkEnv); + + // Format loanUtxoId as "{txHash}-{outputIndex}" + const loanUtxoId = `${loanTxId}-${loanOutputIndex}`; + + // Format collateral ID as "{MarketId}.{policyId}" + // assetBQTokenTicker is the market ID (e.g., "MIN") + // We need the policy ID from assetBQTokenRaw (format: "policyId.assetName" or "policyId") + const qTokenParts = marketConfig.assetBQTokenRaw.split("."); + const qTokenPolicyId = qTokenParts[0]; + const collateralId = `${marketConfig.borrowMarketIdLong}.${qTokenPolicyId}`; + + console.log("collateralId", collateralId); + console.log("amount colla", collateralAmount); + + const buildTxResult = await LiqwidProviderV2.Transactions.repayLoan(apiConfig, { + address: userAddress, + utxos, + loanUtxoId, + collaterals: [ + { + id: collateralId, + amount: Number(collateralAmount), + }, + ], + }); + + if (buildTxResult.type === "err") { + throw new Error(`Failed to build repay transaction: ${buildTxResult.error.message}`); + } + + const txRaw = buildTxResult.value; + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + const txBody = eTx.body(); + const ttl = txBody.ttl(); + invariant(Maybe.isJust(ttl), "TTL must be set in the transaction body"); + safeFreeRustObjects(eTx, txBody); + + const validTo = getTimeFromSlotMagic(networkEnv, ttl); + const txId = LiqwidProviderV2.getTxHash(txRaw); + + return { + txRaw, + txId, + validTo: validTo.getTime(), + }; + }; + + /** + * Build LONG_WITHDRAW transaction: Withdraw underlying asset from Liqwid + * Uses the amountOut from LONG_SUPPLY order as the withdraw amount + */ + export const handleLongWithdraw = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos, supplyAmountOut } = options; + invariant(order.orderType === LongOrderType.LONG_WITHDRAW, "Invalid order type for handleLongWithdraw"); + invariant(order.assetIn, "assetIn is required for LONG_WITHDRAW order"); + invariant(order.amountIn, "amountIn is required for LONG_WITHDRAW order"); + invariant(supplyAmountOut, "supplyAmountOut is required for LONG_WITHDRAW order"); + + const apiConfig = LiqwidProviderV2.createConfig(networkEnv); + + const buildTxResult = await LiqwidProviderV2.Transactions.withdraw(apiConfig, { + address: userAddress, + utxos, + marketId: marketConfig.longCollateralMarketId as LiqwidProviderV2.MarketId, + amount: Number(supplyAmountOut), + }); + + if (buildTxResult.type === "err") { + throw new Error(`Failed to build withdraw transaction: ${buildTxResult.error.message}`); + } + + const txRaw = buildTxResult.value; + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + const txBody = eTx.body(); + const ttl = txBody.ttl(); + invariant(Maybe.isJust(ttl), "TTL must be set in the transaction body"); + safeFreeRustObjects(eTx, txBody); + + const validTo = getTimeFromSlotMagic(networkEnv, ttl); + const txId = LiqwidProviderV2.getTxHash(txRaw); + + return { + txRaw, + txId, + validTo: validTo.getTime(), + }; + }; + + // ============================================================================ + // SHORT Build Functions + // ============================================================================ + + /** + * Build SHORT_SUPPLY transaction: Supply asset A (ADA) to Liqwid, receive qADA + */ + export const handleShortSupply = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos } = options; + invariant(order.orderType === ShortOrderType.SHORT_SUPPLY, "Invalid order type for handleShortSupply"); + invariant(order.assetIn, "assetIn is required for SHORT_SUPPLY order"); + invariant(order.amountIn, "amountIn is required for SHORT_SUPPLY order"); + + const marketId = marketConfig.shortCollateralMarketId as LiqwidProvider.MarketId; + const buildTxResult = await LiqwidProvider.getSupplyTransaction({ + marketId, + amount: Number(order.amountIn), + address: userAddress, + utxos, + networkEnv, + }); + + if (buildTxResult.type === "err") { + throw new Error(`Failed to build supply transaction: ${buildTxResult.error.message}`); + } + + const txRaw = buildTxResult.value; + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + const txBody = eTx.body(); + const ttl = txBody.ttl(); + invariant(Maybe.isJust(ttl), "TTL must be set in the transaction body"); + safeFreeRustObjects(eTx, txBody); + + const validTo = getTimeFromSlotMagic(networkEnv, ttl); + const txId = LiqwidProvider.getLiqwidTxHash(txRaw); + + return { + txRaw, + txId, + validTo: validTo.getTime(), + }; + }; + + /** + * Build SHORT_BORROW transaction: Borrow asset B using qADA as collateral + */ + export const handleShortBorrow = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos, amountBorrow } = options; + invariant(order.orderType === ShortOrderType.SHORT_BORROW, "Invalid order type for handleShortBorrow"); + invariant(order.assetIn, "assetIn is required for SHORT_BORROW order"); + invariant(order.amountIn, "amountIn is required for SHORT_BORROW order"); + invariant(amountBorrow, "amountBorrow is required for SHORT_BORROW order"); + + const apiConfig = LiqwidProviderV2.createConfig(networkEnv); + + const buildTxResult = await LiqwidProviderV2.Transactions.borrow(apiConfig, { + address: userAddress, + utxos, + marketId: marketConfig.borrowMarketIdShort as LiqwidProviderV2.MarketId, + amount: Number(amountBorrow), + collaterals: [ + { + id: marketConfig.assetAQTokenTicker, + amount: Number(order.amountIn), + }, + ], + }); + + if (buildTxResult.type === "err") { + throw new Error(`Failed to build borrow transaction: ${buildTxResult.error.message}`); + } + + const txRaw = buildTxResult.value; + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + const txBody = eTx.body(); + const ttl = txBody.ttl(); + invariant(Maybe.isJust(ttl), "TTL must be set in the transaction body"); + safeFreeRustObjects(eTx, txBody); + + const validTo = getTimeFromSlotMagic(networkEnv, ttl); + const txId = LiqwidProviderV2.getTxHash(txRaw); + + return { + txRaw, + txId, + validTo: validTo.getTime(), + }; + }; + + /** + * Build SHORT_SELL transaction: Sell asset B for asset A via DEX (B_TO_A swap) + * Reuses the same DEX swap pattern as handleLongSell + */ + export const handleShortSell = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos } = options; + invariant(order.orderType === ShortOrderType.SHORT_SELL, "Invalid order type for handleShortSell"); + invariant(order.assetIn, "assetIn is required for SHORT_SELL order"); + invariant(order.amountIn, "amountIn is required for SHORT_SELL order"); + invariant(order.assetOut, "assetOut is required for SHORT_SELL order"); + + const walletUtxos: Utxo[] = utxos.map((u) => Utxo.fromHex(u)); + const sender = Address.fromBech32(userAddress); + + const txb = DEXOrderTransaction.createBulkOrdersTx({ + networkEnv, + sender, + orderOptions: [ + { + lpAsset: Asset.fromString(marketConfig.ammLpAsset), + version: DexVersion.DEX_V2, + type: OrderV2StepType.SWAP_EXACT_IN, + assetIn: marketConfig.assetB, + amountIn: BigInt(order.amountIn), + minimumAmountOut: 1n, + direction: OrderV2Direction.B_TO_A, + killOnFailed: false, + isLimitOrder: false, + }, + ], + }); + const validTo = Date.now() + Duration.newMinutes(3).milliseconds; + txb.validToUnixTime(validTo); + + const { txComplete, txId } = await txb.completeUnsafeForTxChaining({ + coinSelectionAlgorithm: CoinSelectionAlgorithm.SPEND_ALL, + walletUtxos, + changeAddress: sender, + provider: new EmulatorProvider(networkEnv), + }); + const txRaw = txComplete.complete(); + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + safeFreeRustObjects(eTx); + + return { + txRaw, + txId: txId, + validTo, + }; + }; + + /** + * Build SHORT_BUY transaction: Buy asset B with asset A via DEX (A_TO_B swap) + * Reuses the same DEX swap pattern as handleLongBuy + */ + export const handleShortBuy = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos } = options; + invariant(order.orderType === ShortOrderType.SHORT_BUY, "Invalid order type for handleShortBuy"); + invariant(order.assetIn, "assetIn is required for SHORT_BUY order"); + invariant(order.amountIn, "amountIn is required for SHORT_BUY order"); + invariant(order.assetOut, "assetOut is required for SHORT_BUY order"); + + const walletUtxos: Utxo[] = utxos.map((u) => Utxo.fromHex(u)); + const sender = Address.fromBech32(userAddress); + + const txb = DEXOrderTransaction.createBulkOrdersTx({ + networkEnv, + sender, + orderOptions: [ + { + lpAsset: Asset.fromString(marketConfig.ammLpAsset), + version: DexVersion.DEX_V2, + type: OrderV2StepType.SWAP_EXACT_IN, + assetIn: marketConfig.assetA, + amountIn: BigInt(order.amountIn), + minimumAmountOut: 1n, + direction: OrderV2Direction.A_TO_B, + killOnFailed: false, + isLimitOrder: false, + }, + ], + }); + const validTo = Date.now() + Duration.newMinutes(3).milliseconds; + txb.validToUnixTime(validTo); + + const { txComplete, txId } = await txb.completeUnsafeForTxChaining({ + coinSelectionAlgorithm: CoinSelectionAlgorithm.SPEND_ALL, + walletUtxos, + changeAddress: sender, + provider: new EmulatorProvider(networkEnv), + }); + const txRaw = txComplete.complete(); + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + safeFreeRustObjects(eTx); + + return { + txRaw, + txId: txId, + validTo, + }; + }; + + /** + * Build SHORT_REPAY transaction: Repay asset B loan to Liqwid and redeem qADA collateral + */ + export const handleShortRepay = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos, loanTxId, loanOutputIndex, collateralAmount } = + options; + invariant(order.orderType === ShortOrderType.SHORT_REPAY, "Invalid order type for handleShortRepay"); + invariant(order.assetIn, "assetIn is required for SHORT_REPAY order"); + invariant(order.amountIn, "amountIn is required for SHORT_REPAY order"); + invariant(loanTxId, "loanTxId is required for SHORT_REPAY order"); + invariant(loanOutputIndex !== undefined, "loanOutputIndex is required for SHORT_REPAY order"); + invariant(collateralAmount, "collateralAmount is required for SHORT_REPAY order"); + + const apiConfig = LiqwidProviderV2.createConfig(networkEnv); + + // Format loanUtxoId as "{txHash}-{outputIndex}" + const loanUtxoId = `${loanTxId}-${loanOutputIndex}`; + + // Format collateral ID: use assetAQTokenRaw (qADA) for SHORT + const qTokenParts = marketConfig.assetAQTokenRaw.split("."); + const qTokenPolicyId = qTokenParts[0]; + const collateralId = `${marketConfig.borrowMarketIdShort}.${qTokenPolicyId}`; + + const buildTxResult = await LiqwidProviderV2.Transactions.repayLoan(apiConfig, { + address: userAddress, + utxos, + loanUtxoId, + collaterals: [ + { + id: collateralId, + amount: Number(collateralAmount), + }, + ], + }); + + if (buildTxResult.type === "err") { + throw new Error(`Failed to build repay transaction: ${buildTxResult.error.message}`); + } + + const txRaw = buildTxResult.value; + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + const txBody = eTx.body(); + const ttl = txBody.ttl(); + invariant(Maybe.isJust(ttl), "TTL must be set in the transaction body"); + safeFreeRustObjects(eTx, txBody); + + const validTo = getTimeFromSlotMagic(networkEnv, ttl); + const txId = LiqwidProviderV2.getTxHash(txRaw); + + return { + txRaw, + txId, + validTo: validTo.getTime(), + }; + }; + + /** + * Build SHORT_WITHDRAW transaction: Withdraw asset A (ADA) from Liqwid + */ + export const handleShortWithdraw = async (options: HandleBuildTxOptions): Promise => { + const { order, marketConfig, userAddress, networkEnv, utxos, supplyAmountOut } = options; + invariant(order.orderType === ShortOrderType.SHORT_WITHDRAW, "Invalid order type for handleShortWithdraw"); + invariant(order.assetIn, "assetIn is required for SHORT_WITHDRAW order"); + invariant(order.amountIn, "amountIn is required for SHORT_WITHDRAW order"); + invariant(supplyAmountOut, "supplyAmountOut is required for SHORT_WITHDRAW order"); + + const apiConfig = LiqwidProviderV2.createConfig(networkEnv); + + const buildTxResult = await LiqwidProviderV2.Transactions.withdraw(apiConfig, { + address: userAddress, + utxos, + marketId: marketConfig.shortCollateralMarketId as LiqwidProviderV2.MarketId, + amount: Number(supplyAmountOut), + }); + + if (buildTxResult.type === "err") { + throw new Error(`Failed to build withdraw transaction: ${buildTxResult.error.message}`); + } + + const txRaw = buildTxResult.value; + const ECSL = RustModule.getE; + const eTx = ECSL.Transaction.from_hex(txRaw); + const txBody = eTx.body(); + const ttl = txBody.ttl(); + invariant(Maybe.isJust(ttl), "TTL must be set in the transaction body"); + safeFreeRustObjects(eTx, txBody); + + const validTo = getTimeFromSlotMagic(networkEnv, ttl); + const txId = LiqwidProviderV2.getTxHash(txRaw); + + return { + txRaw, + txId, + validTo: validTo.getTime(), + }; + }; + + // Common waiting result type for all waiting functions + export type WaitingResult = + | { isConfirmed: false } + | { + isConfirmed: true; + nextOrderType: LongOrderType | ShortOrderType; + assetIn: string; + amountIn: string; + assetOut: string; + /** Amount received from this order (to update order.amount_out) */ + amountOut: string; + } + | { + isConfirmed: true; + isFinal: true; + positionStatus: PositionStatus; + /** Amount received from this order (to update order.amount_out) */ + amountOut: string; + }; + + export type WaitingOptions = { + marketConfig: MarketConfig; + txHash: string; + userAddress: Address; + cardanoscanProvider: CardanoscanProvider; + /** Current order type being waited on */ + orderType: string; + /** Order output index (used for LONG_BUY to check if output is spent) */ + orderOutputIndex?: number; + /** Asset out from order (used for LONG_BUY to find the received token) */ + assetOut?: Asset; + /** Position amount_in (used for LONG_BORROW to calculate borrow amount) */ + positionAmountIn?: string; + /** Loan transaction ID (used for LONG_REPAY to identify the loan) */ + loanTxId?: string; + }; + + /** + * Wait for LONG_BUY or LONG_BUY_MORE order output to be spent (consumed by DEX) + * - For LONG_BUY: prepare the next LONG_SUPPLY order details + * - For LONG_BUY_MORE: this is the final step, position becomes OPEN + */ + export const waitingLongBuy = async (options: WaitingOptions): Promise => { + const { marketConfig, txHash, orderOutputIndex, userAddress, cardanoscanProvider, assetOut, orderType } = options; + invariant(orderOutputIndex !== undefined, "orderOutputIndex is required for waitingLongBuy"); + invariant(assetOut, "assetOut is required for waitingLongBuy"); + + const userAddressHex = userAddress.toHex(); + + // Search for the transaction that spent this UTXO + const spendingTx = await cardanoscanProvider.findTransactionHasSpent( + userAddress, + txHash, + orderOutputIndex, + 5, // pageSize + 10, // maxPage + ); + + if (spendingTx) { + const assetOutUnit = assetOut.toBlockFrostString(); + + for (const output of spendingTx.outputs) { + if (output.address === userAddressHex) { + if (output.tokens && output.tokens.length > 0) { + const matchingToken = output.tokens.find((token) => token.assetId === assetOutUnit); + if (matchingToken) { + const amountOut = BigInt(matchingToken.value); + + // For LONG_BUY_MORE, this is the final step - position becomes OPEN + if (orderType === LongOrderType.LONG_BUY_MORE) { + return { + isConfirmed: true, + isFinal: true, + positionStatus: PositionStatus.OPEN, + amountOut: amountOut.toString(), + }; + } + + // For LONG_BUY, transition to LONG_SUPPLY + return { + isConfirmed: true, + nextOrderType: LongOrderType.LONG_SUPPLY, + assetIn: assetOut.toString(), + amountIn: amountOut.toString(), + assetOut: marketConfig.assetBQTokenRaw, + amountOut: amountOut.toString(), + }; + } + } + } + } + + throw new Error( + `Order output spent (tx: ${spendingTx.hash}) but could not find matching output with asset ${assetOut.toString()}`, + ); + } + + return { isConfirmed: false }; + }; + + /** + * Wait for LONG_SUPPLY transaction to be confirmed + * and prepare the next LONG_BORROW order details + */ + export const waitingLongSupply = async (options: WaitingOptions): Promise => { + const { marketConfig, txHash, userAddress, cardanoscanProvider } = options; + + const txFoundOnChain = await cardanoscanProvider.findTransactionByHash(userAddress, txHash, 50, 10); + + if (txFoundOnChain) { + const userAddressHex = userAddress.toHex(); + const qTokenAsset = Asset.fromString(marketConfig.assetBQTokenRaw); + const qTokenUnit = qTokenAsset.toBlockFrostString(); + + for (const output of txFoundOnChain.outputs) { + if (output.address === userAddressHex) { + if (output.tokens && output.tokens.length > 0) { + const matchingToken = output.tokens.find((token) => token.assetId === qTokenUnit); + if (matchingToken) { + const amountReceived = BigInt(matchingToken.value); + return { + isConfirmed: true, + nextOrderType: LongOrderType.LONG_BORROW, + assetIn: marketConfig.assetBQTokenRaw, + amountIn: amountReceived.toString(), + assetOut: marketConfig.assetA.toString(), + amountOut: amountReceived.toString(), + }; + } + } + } + } + + throw new Error( + `LONG_SUPPLY tx confirmed (${txHash}) but could not find output with qToken ${marketConfig.assetBQTokenRaw}`, + ); + } + + return { isConfirmed: false }; + }; + + /** + * Wait for LONG_BORROW transaction to be confirmed + * and prepare the next LONG_BUY_MORE order details + */ + export const waitingLongBorrow = async (options: WaitingOptions): Promise => { + const { marketConfig, txHash, userAddress, cardanoscanProvider, positionAmountIn } = options; + invariant(positionAmountIn, "positionAmountIn is required for waitingLongBorrow"); + + const txFoundOnChain = await cardanoscanProvider.findTransactionByHash(userAddress, txHash, 50, 10); + + if (txFoundOnChain) { + // Calculate borrow amount: position.amount_in * (leverage - 1) + const amountBorrow = BigInt(Math.floor(Number(positionAmountIn) * (marketConfig.longLeverage - 1))); + + return { + isConfirmed: true, + nextOrderType: LongOrderType.LONG_BUY_MORE, + assetIn: marketConfig.assetA.toString(), + amountIn: amountBorrow.toString(), + assetOut: marketConfig.assetB.toString(), + amountOut: amountBorrow.toString(), + }; + } + + return { isConfirmed: false }; + }; + + /** + * Wait for LONG_SELL or LONG_SELL_ALL order output to be spent (consumed by DEX) + * - For LONG_SELL: prepare the next LONG_REPAY order details + * - For LONG_SELL_ALL: this is the final step, position becomes CLOSED + */ + export const waitingLongSell = async (options: WaitingOptions): Promise => { + const { marketConfig, txHash, orderOutputIndex, userAddress, cardanoscanProvider, orderType } = options; + invariant(orderOutputIndex !== undefined, "orderOutputIndex is required for waitingLongSell"); + + const userAddressHex = userAddress.toHex(); + + // Search for the transaction that spent this UTXO + const spendingTx = await cardanoscanProvider.findTransactionHasSpent( + userAddress, + txHash, + orderOutputIndex, + 5, // pageSize + 10, // maxPage + ); + + if (spendingTx) { + // For LONG_SELL/LONG_SELL_ALL, assetOut is asset A (ADA), so we look for ADA in outputs + // ADA is represented as "lovelace" in the output value + for (const output of spendingTx.outputs) { + if (output.address === userAddressHex) { + // For ADA, the value is in the output.value field directly + const amountOut = BigInt(output.value); + + // For LONG_SELL_ALL, this is the final step - position becomes CLOSED + if (orderType === LongOrderType.LONG_SELL_ALL) { + return { + isConfirmed: true, + isFinal: true, + positionStatus: PositionStatus.CLOSED, + amountOut: amountOut.toString(), + }; + } + + // For LONG_SELL, transition to LONG_REPAY + return { + isConfirmed: true, + nextOrderType: LongOrderType.LONG_REPAY, + assetIn: marketConfig.assetA.toString(), + amountIn: amountOut.toString(), + assetOut: marketConfig.assetBQTokenRaw, // qToken to be redeemed + amountOut: amountOut.toString(), + }; + } + } + + throw new Error(`Order output spent (tx: ${spendingTx.hash}) but could not find matching output for user`); + } + + return { isConfirmed: false }; + }; + + /** + * Wait for LONG_REPAY transaction to be confirmed + * and prepare the next LONG_WITHDRAW order details + */ + export const waitingLongRepay = async (options: WaitingOptions): Promise => { + const { marketConfig, txHash, userAddress, cardanoscanProvider } = options; + + const txFoundOnChain = await cardanoscanProvider.findTransactionByHash(userAddress, txHash, 50, 10); + + if (txFoundOnChain) { + const userAddressHex = userAddress.toHex(); + const qTokenAsset = Asset.fromString(marketConfig.assetBQTokenRaw); + const qTokenUnit = qTokenAsset.toBlockFrostString(); + + // Find qToken received after repaying (collateral redeemed) + for (const output of txFoundOnChain.outputs) { + if (output.address === userAddressHex) { + if (output.tokens && output.tokens.length > 0) { + const matchingToken = output.tokens.find((token) => token.assetId === qTokenUnit); + if (matchingToken) { + const qTokenAmount = BigInt(matchingToken.value); + return { + isConfirmed: true, + nextOrderType: LongOrderType.LONG_WITHDRAW, + assetIn: marketConfig.assetBQTokenRaw, + amountIn: qTokenAmount.toString(), + assetOut: marketConfig.assetB.toString(), + amountOut: qTokenAmount.toString(), + }; + } + } + } + } + + throw new Error( + `LONG_REPAY tx confirmed (${txHash}) but could not find output with qToken ${marketConfig.assetBQTokenRaw}`, + ); + } + + return { isConfirmed: false }; + }; + + /** + * Wait for LONG_WITHDRAW transaction to be confirmed + * and prepare the next LONG_SELL_ALL order details + */ + export const waitingLongWithdraw = async (options: WaitingOptions): Promise => { + const { marketConfig, txHash, userAddress, cardanoscanProvider } = options; + + const txFoundOnChain = await cardanoscanProvider.findTransactionByHash(userAddress, txHash, 50, 10); + + if (txFoundOnChain) { + const userAddressHex = userAddress.toHex(); + const assetBUnit = marketConfig.assetB.toBlockFrostString(); + + // Find asset B received after withdraw + for (const output of txFoundOnChain.outputs) { + if (output.address === userAddressHex) { + if (output.tokens && output.tokens.length > 0) { + const matchingToken = output.tokens.find((token) => token.assetId === assetBUnit); + if (matchingToken) { + const amountOut = BigInt(matchingToken.value); + return { + isConfirmed: true, + nextOrderType: LongOrderType.LONG_SELL_ALL, + assetIn: marketConfig.assetB.toString(), + amountIn: amountOut.toString(), + assetOut: marketConfig.assetA.toString(), + amountOut: amountOut.toString(), + }; + } + } + } + } + + throw new Error( + `LONG_WITHDRAW tx confirmed (${txHash}) but could not find output with asset ${marketConfig.assetB.toString()}`, + ); + } + + return { isConfirmed: false }; + }; + + // ============================================================================ + // SHORT Waiting Functions + // ============================================================================ + + /** + * Wait for SHORT_SUPPLY transaction to be confirmed + * Extract qADA amount and transition to SHORT_BORROW + */ + export const waitingShortSupply = async (options: WaitingOptions): Promise => { + const { marketConfig, txHash, userAddress, cardanoscanProvider } = options; + + const txFoundOnChain = await cardanoscanProvider.findTransactionByHash(userAddress, txHash, 50, 10); + + if (txFoundOnChain) { + const userAddressHex = userAddress.toHex(); + const qTokenAsset = Asset.fromString(marketConfig.assetAQTokenRaw); + const qTokenUnit = qTokenAsset.toBlockFrostString(); + + for (const output of txFoundOnChain.outputs) { + if (output.address === userAddressHex) { + if (output.tokens && output.tokens.length > 0) { + const matchingToken = output.tokens.find((token) => token.assetId === qTokenUnit); + if (matchingToken) { + const amountReceived = BigInt(matchingToken.value); + return { + isConfirmed: true, + nextOrderType: ShortOrderType.SHORT_BORROW, + assetIn: marketConfig.assetAQTokenRaw, + amountIn: amountReceived.toString(), + assetOut: marketConfig.assetB.toString(), + amountOut: amountReceived.toString(), + }; + } + } + } + } + + throw new Error( + `SHORT_SUPPLY tx confirmed (${txHash}) but could not find output with qToken ${marketConfig.assetAQTokenRaw}`, + ); + } + + return { isConfirmed: false }; + }; + + /** + * Wait for SHORT_BORROW transaction to be confirmed + * Calculate borrowed amount and transition to SHORT_SELL + */ + export const waitingShortBorrow = async (options: WaitingOptions): Promise => { + const { marketConfig, txHash, userAddress, cardanoscanProvider, positionAmountIn } = options; + invariant(positionAmountIn, "positionAmountIn is required for waitingShortBorrow"); + + const txFoundOnChain = await cardanoscanProvider.findTransactionByHash(userAddress, txHash, 50, 10); + + if (txFoundOnChain) { + const userAddressHex = userAddress.toHex(); + const assetBUnit = marketConfig.assetB.toBlockFrostString(); + + // Find asset B (borrowed token) in outputs + for (const output of txFoundOnChain.outputs) { + if (output.address === userAddressHex) { + if (output.tokens && output.tokens.length > 0) { + const matchingToken = output.tokens.find((token) => token.assetId === assetBUnit); + if (matchingToken) { + const amountBorrowed = BigInt(matchingToken.value); + return { + isConfirmed: true, + nextOrderType: ShortOrderType.SHORT_SELL, + assetIn: marketConfig.assetB.toString(), + amountIn: amountBorrowed.toString(), + assetOut: marketConfig.assetA.toString(), + amountOut: amountBorrowed.toString(), + }; + } + } + } + } + + throw new Error( + `SHORT_BORROW tx confirmed (${txHash}) but could not find output with asset ${marketConfig.assetB.toString()}`, + ); + } + + return { isConfirmed: false }; + }; + + /** + * Wait for SHORT_SELL order output to be spent (consumed by DEX) + * This is the final opening step — position becomes OPEN + */ + export const waitingShortSell = async (options: WaitingOptions): Promise => { + const { txHash, orderOutputIndex, userAddress, cardanoscanProvider } = options; + invariant(orderOutputIndex !== undefined, "orderOutputIndex is required for waitingShortSell"); + + const userAddressHex = userAddress.toHex(); + + const spendingTx = await cardanoscanProvider.findTransactionHasSpent(userAddress, txHash, orderOutputIndex, 5, 10); + + if (spendingTx) { + // SHORT_SELL sells asset B for ADA, so we look for ADA in outputs + for (const output of spendingTx.outputs) { + if (output.address === userAddressHex) { + const amountOut = BigInt(output.value); + return { + isConfirmed: true, + isFinal: true, + positionStatus: PositionStatus.OPEN, + amountOut: amountOut.toString(), + }; + } + } + + throw new Error(`Order output spent (tx: ${spendingTx.hash}) but could not find matching output for user`); + } + + return { isConfirmed: false }; + }; + + /** + * Wait for SHORT_BUY order output to be spent (consumed by DEX) + * Extract asset B received and transition to SHORT_REPAY + */ + export const waitingShortBuy = async (options: WaitingOptions): Promise => { + const { marketConfig, txHash, orderOutputIndex, userAddress, cardanoscanProvider, assetOut } = options; + invariant(orderOutputIndex !== undefined, "orderOutputIndex is required for waitingShortBuy"); + invariant(assetOut, "assetOut is required for waitingShortBuy"); + + const userAddressHex = userAddress.toHex(); + + const spendingTx = await cardanoscanProvider.findTransactionHasSpent(userAddress, txHash, orderOutputIndex, 5, 10); + + if (spendingTx) { + const assetOutUnit = assetOut.toBlockFrostString(); + + for (const output of spendingTx.outputs) { + if (output.address === userAddressHex) { + if (output.tokens && output.tokens.length > 0) { + const matchingToken = output.tokens.find((token) => token.assetId === assetOutUnit); + if (matchingToken) { + const amountOut = BigInt(matchingToken.value); + return { + isConfirmed: true, + nextOrderType: ShortOrderType.SHORT_REPAY, + assetIn: assetOut.toString(), + amountIn: amountOut.toString(), + assetOut: marketConfig.assetAQTokenRaw, // qADA to be redeemed + amountOut: amountOut.toString(), + }; + } + } + } + } + + throw new Error( + `Order output spent (tx: ${spendingTx.hash}) but could not find matching output with asset ${assetOut.toString()}`, + ); + } + + return { isConfirmed: false }; + }; + + /** + * Wait for SHORT_REPAY transaction to be confirmed + * Extract redeemed qADA and transition to SHORT_WITHDRAW + */ + export const waitingShortRepay = async (options: WaitingOptions): Promise => { + const { marketConfig, txHash, userAddress, cardanoscanProvider } = options; + + const txFoundOnChain = await cardanoscanProvider.findTransactionByHash(userAddress, txHash, 50, 10); + + if (txFoundOnChain) { + const userAddressHex = userAddress.toHex(); + const qTokenAsset = Asset.fromString(marketConfig.assetAQTokenRaw); + const qTokenUnit = qTokenAsset.toBlockFrostString(); + + for (const output of txFoundOnChain.outputs) { + if (output.address === userAddressHex) { + if (output.tokens && output.tokens.length > 0) { + const matchingToken = output.tokens.find((token) => token.assetId === qTokenUnit); + if (matchingToken) { + const qTokenAmount = BigInt(matchingToken.value); + return { + isConfirmed: true, + nextOrderType: ShortOrderType.SHORT_WITHDRAW, + assetIn: marketConfig.assetAQTokenRaw, + amountIn: qTokenAmount.toString(), + assetOut: marketConfig.assetA.toString(), + amountOut: qTokenAmount.toString(), + }; + } + } + } + } + + throw new Error( + `SHORT_REPAY tx confirmed (${txHash}) but could not find output with qToken ${marketConfig.assetAQTokenRaw}`, + ); + } + + return { isConfirmed: false }; + }; + + /** + * Wait for SHORT_WITHDRAW transaction to be confirmed + * This is the final closing step — position becomes CLOSED + */ + export const waitingShortWithdraw = async (options: WaitingOptions): Promise => { + const { txHash, userAddress, cardanoscanProvider } = options; + + const txFoundOnChain = await cardanoscanProvider.findTransactionByHash(userAddress, txHash, 50, 10); + + if (txFoundOnChain) { + const userAddressHex = userAddress.toHex(); + + // For SHORT_WITHDRAW, we withdraw ADA — look for ADA value in outputs + for (const output of txFoundOnChain.outputs) { + if (output.address === userAddressHex) { + const amountOut = BigInt(output.value); + return { + isConfirmed: true, + isFinal: true, + positionStatus: PositionStatus.CLOSED, + amountOut: amountOut.toString(), + }; + } + } + + throw new Error(`SHORT_WITHDRAW tx confirmed (${txHash}) but could not find output for user ${userAddressHex}`); + } + + return { isConfirmed: false }; + }; + + /** Map of order types to their build transaction functions */ + export const MAP_BUILD_TX_FN: Record Promise> = { + [LongOrderType.LONG_BUY]: handleLongBuy, + [LongOrderType.LONG_SUPPLY]: handleLongSupply, + [LongOrderType.LONG_BORROW]: handleLongBorrow, + [LongOrderType.LONG_BUY_MORE]: handleLongBuy, + [LongOrderType.LONG_SELL]: handleLongSell, + [LongOrderType.LONG_REPAY]: handleLongRepay, + [LongOrderType.LONG_WITHDRAW]: handleLongWithdraw, + [LongOrderType.LONG_SELL_ALL]: handleLongSell, + [ShortOrderType.SHORT_SUPPLY]: handleShortSupply, + [ShortOrderType.SHORT_BORROW]: handleShortBorrow, + [ShortOrderType.SHORT_SELL]: handleShortSell, + [ShortOrderType.SHORT_BUY]: handleShortBuy, + [ShortOrderType.SHORT_REPAY]: handleShortRepay, + [ShortOrderType.SHORT_WITHDRAW]: handleShortWithdraw, + }; + + /** Map of order types to their waiting functions */ + export const MAP_WAITING_FN: Record Promise> = { + [LongOrderType.LONG_BUY]: waitingLongBuy, + [LongOrderType.LONG_SUPPLY]: waitingLongSupply, + [LongOrderType.LONG_BORROW]: waitingLongBorrow, + [LongOrderType.LONG_BUY_MORE]: waitingLongBuy, // Reuse waitingLongBuy + [LongOrderType.LONG_SELL]: waitingLongSell, + [LongOrderType.LONG_REPAY]: waitingLongRepay, + [LongOrderType.LONG_WITHDRAW]: waitingLongWithdraw, + [LongOrderType.LONG_SELL_ALL]: waitingLongSell, + [ShortOrderType.SHORT_SUPPLY]: waitingShortSupply, + [ShortOrderType.SHORT_BORROW]: waitingShortBorrow, + [ShortOrderType.SHORT_SELL]: waitingShortSell, + [ShortOrderType.SHORT_BUY]: waitingShortBuy, + [ShortOrderType.SHORT_REPAY]: waitingShortRepay, + [ShortOrderType.SHORT_WITHDRAW]: waitingShortWithdraw, + }; +} diff --git a/apps/long-short-backend/src/cmd/run-api.ts b/apps/long-short-backend/src/cmd/run-api.ts new file mode 100644 index 0000000..ed05ae9 --- /dev/null +++ b/apps/long-short-backend/src/cmd/run-api.ts @@ -0,0 +1,67 @@ +import { NetworkEnvironment } from "@minswap/felis-ledger-core"; +import { RustModule } from "@minswap/felis-ledger-utils"; +import { createApiServer } from "../api/server"; +import { loadMarketConfigs } from "../config/market"; +import type { DB } from "../database"; +import { newKyselyClient } from "../database/postgres"; +import { CardanoscanProvider } from "../provider"; +import { logger } from "../utils"; + +const API_PORT = Number(process.env.API_PORT) || 9999; +const API_HOST = process.env.API_HOST || "0.0.0.0"; +const DATABASE_URL = process.env.DATABASE_URL; +const NETWORK = process.env.NETWORK || "mainnet"; +const CARDANOSCAN_API_KEY = process.env.CARDANOSCAN_API_KEY; + +async function main() { + // Validate environment + if (!DATABASE_URL) { + throw new Error("DATABASE_URL environment variable is required"); + } + if (!CARDANOSCAN_API_KEY) { + throw new Error("CARDANOSCAN_API_KEY environment variable is required"); + } + + // Parse network environment + const networkEnv = NETWORK === "mainnet" ? NetworkEnvironment.MAINNET : NetworkEnvironment.TESTNET_PREVIEW; + logger.info(`Network environment: ${NETWORK}`); + + logger.info("Loading WASM modules..."); + await RustModule.load(); + logger.info("WASM modules loaded"); + + // Connect to database + logger.info("Connecting to database..."); + const db = await newKyselyClient(DATABASE_URL, { logSQL: false }); + logger.info("Database connected"); + + // Load market configs from database + logger.info("Loading market configs..."); + const marketConfigs = await loadMarketConfigs(db); + logger.info(`Loaded ${marketConfigs.size} market configs`); + + // Create Cardanoscan provider + const cardanoScanBaseUrl = networkEnv === NetworkEnvironment.MAINNET ? CardanoscanProvider.MAINNET_URL : CardanoscanProvider.PREVIEW_URL; + const cardanoscanProvider = new CardanoscanProvider(cardanoScanBaseUrl, CARDANOSCAN_API_KEY); + + // Start API server + logger.info("Starting API server..."); + await createApiServer({ + port: API_PORT, + host: API_HOST, + db, + networkEnv, + cardanoscanProvider, + }); + + logger.info("Long-Short Backend started successfully", { + port: API_PORT, + host: API_HOST, + network: NETWORK, + }); +} + +main().catch((error) => { + logger.error("Failed to start application", { error }); + process.exit(1); +}); diff --git a/apps/long-short-backend/src/config/index.ts b/apps/long-short-backend/src/config/index.ts new file mode 100644 index 0000000..674239a --- /dev/null +++ b/apps/long-short-backend/src/config/index.ts @@ -0,0 +1 @@ +export * from "./market"; diff --git a/apps/long-short-backend/src/config/market.ts b/apps/long-short-backend/src/config/market.ts new file mode 100644 index 0000000..bdb11f5 --- /dev/null +++ b/apps/long-short-backend/src/config/market.ts @@ -0,0 +1,99 @@ +import { Asset } from "@minswap/felis-ledger-core"; +import type { Kysely } from "kysely"; +import type { DB } from "../database"; + +/** + * Market configuration loaded from database + */ +export type MarketConfig = { + marketId: string; + assetA: Asset; + assetB: Asset; + ammLpAsset: string; + assetAQTokenTicker: string; + assetAQTokenRaw: string; + assetBQTokenTicker: string; + assetBQTokenRaw: string; + longCollateralMarketId: string; + shortCollateralMarketId: string; + borrowMarketIdLong: string; + borrowMarketIdShort: string; + longLeverage: number; + shortLeverage: number; + minCollateral: bigint; + enable: boolean; +}; + +/** + * In-memory cache for market configs + */ +let marketConfigCache: Map | null = null; + +/** + * Load all market configs from database + */ +export async function loadMarketConfigs(db: Kysely): Promise> { + const rows = await db.selectFrom("market_config").selectAll().execute(); + + const configs = new Map(); + for (const row of rows) { + configs.set(row.market_id, { + marketId: row.market_id, + assetA: Asset.fromString(row.asset_a), + assetB: Asset.fromString(row.asset_b), + ammLpAsset: row.amm_lp_asset, + assetAQTokenTicker: row.asset_a_q_token_ticker, + assetAQTokenRaw: row.asset_a_q_token_raw, + assetBQTokenTicker: row.asset_b_q_token_ticker, + assetBQTokenRaw: row.asset_b_q_token_raw, + longCollateralMarketId: row.long_collateral_market_id, + shortCollateralMarketId: row.short_collateral_market_id, + borrowMarketIdLong: row.borrow_market_id_long, + borrowMarketIdShort: row.borrow_market_id_short, + longLeverage: Number(row.long_leverage), + shortLeverage: Number(row.short_leverage), + minCollateral: BigInt(row.min_collateral), + enable: row.enable, + }); + } + + // Update cache + marketConfigCache = configs; + return configs; +} + +/** + * Get all enabled market configs from cache + * Call loadMarketConfigs first to populate cache + */ +export function getEnabledMarketConfigs(): MarketConfig[] { + if (!marketConfigCache) { + throw new Error("Market configs not loaded. Call loadMarketConfigs first."); + } + return Array.from(marketConfigCache.values()).filter((c) => c.enable); +} + +/** + * Get market config by market ID from cache + */ +export function getMarketConfig(marketId: string): MarketConfig | null { + if (!marketConfigCache) { + throw new Error("Market configs not loaded. Call loadMarketConfigs first."); + } + return marketConfigCache.get(marketId) ?? null; +} + +/** + * Check if market is supported and enabled + */ +export function isSupportedMarket(marketId: string): boolean { + const config = getMarketConfig(marketId); + return config ? config.enable : false; +} + +/** + * Reload market configs from database (for hot reload) + */ +export async function reloadMarketConfigs(db: Kysely): Promise { + await loadMarketConfigs(db); +} diff --git a/apps/long-short-backend/src/constants.ts b/apps/long-short-backend/src/constants.ts new file mode 100644 index 0000000..242c6a3 --- /dev/null +++ b/apps/long-short-backend/src/constants.ts @@ -0,0 +1,10 @@ +// MUST sort endpoints alphabetically +export const API_ENDPOINTS = { + HEALTH: "/health", + LIQWID_SUBMIT: "/liqwid/submit", + METADATA: "/metadata", + POSITION_BUILD_TX: "/position/build-tx", + POSITION_CLOSE: "/position/close", + POSITION_CREATE: "/position/create", + POSITION_GET: "/position/get", +}; diff --git a/apps/long-short-backend/src/database/db.d.ts b/apps/long-short-backend/src/database/db.d.ts new file mode 100644 index 0000000..a966096 --- /dev/null +++ b/apps/long-short-backend/src/database/db.d.ts @@ -0,0 +1,68 @@ +/** + * This file was generated by kysely-codegen. + * Please do not edit it manually. + */ + +import type { ColumnType } from "kysely"; + +export type Generated = T extends ColumnType + ? ColumnType + : ColumnType; + +export type Int8 = ColumnType; + +export type Numeric = ColumnType; + +export type Timestamp = ColumnType; + +export interface MarketConfig { + amm_lp_asset: string; + asset_a: string; + asset_a_q_token_raw: string; + asset_a_q_token_ticker: string; + asset_b: string; + asset_b_q_token_raw: string; + asset_b_q_token_ticker: string; + borrow_market_id_long: Generated; + borrow_market_id_short: Generated; + enable: Generated; + long_collateral_market_id: string; + long_leverage: Numeric; + market_id: string; + min_collateral: Numeric; + short_collateral_market_id: Generated; + short_leverage: Generated; +} + +export interface Order { + amount_in: Numeric | null; + amount_out: Numeric | null; + asset_in: string | null; + asset_out: string | null; + built_tx_id: string | null; + built_valid_to: Timestamp | null; + created_tx_id: string | null; + created_tx_index: number | null; + id: Generated; + order_type: string; + position_id: Int8; + waiting: Generated; +} + +export interface Position { + amount_borrow: Numeric; + amount_in: Numeric; + closed_at: Timestamp | null; + created_at: Generated; + id: Generated; + market_id: string; + side: string; + status: Generated; + user_address: string; +} + +export interface DB { + market_config: MarketConfig; + order: Order; + position: Position; +} diff --git a/apps/long-short-backend/src/database/index.ts b/apps/long-short-backend/src/database/index.ts new file mode 100644 index 0000000..66a3de0 --- /dev/null +++ b/apps/long-short-backend/src/database/index.ts @@ -0,0 +1,3 @@ +export type { DB } from "./db"; +export * from "./postgres"; +export * from "./redis"; diff --git a/apps/long-short-backend/src/database/postgres.ts b/apps/long-short-backend/src/database/postgres.ts new file mode 100644 index 0000000..5d8c972 --- /dev/null +++ b/apps/long-short-backend/src/database/postgres.ts @@ -0,0 +1,77 @@ +/** biome-ignore-all lint/suspicious/noExplicitAny: skip file */ +import { Duration } from "@minswap/felis-ledger-utils"; +import * as Kysely from "kysely"; +import * as Pg from "pg"; +import { logger } from "../utils"; + +export const DEFAULT_DATABASE_TIMEOUT = Duration.newSeconds(30); + +export type PostgresOptions = { + logSQL: boolean; + logSlowSQL: Duration; + skipHealthcheck?: boolean; +}; + +const DEFAULT_POSTGRES_OPTIONS: PostgresOptions = { + logSQL: false, + logSlowSQL: Duration.newSeconds(1), + skipHealthcheck: true, +}; + +/** + * Precedence: + * 1. Env var + * 2. Options passed in code + * 3. Default options + */ +function parsePostgresOptions(userOptions?: Partial): PostgresOptions { + let finalOptions: PostgresOptions = DEFAULT_POSTGRES_OPTIONS; + if (userOptions) { + finalOptions = Object.assign(finalOptions, userOptions); + } + return finalOptions; +} + +export async function newKyselyClient( + url: string, + userOptions?: Partial, +): Promise> { + const { logSQL, logSlowSQL, skipHealthcheck } = parsePostgresOptions(userOptions); + + function formatEvent(e: Kysely.LogEvent): Record { + const ret: Record = { + sql: e.query.sql, + params: e.query.parameters, + duration: Duration.newMilliseconds(e.queryDurationMillis), + }; + if (e.level === "error") { + ret.error = e.error; + } + return ret; + } + + const dialect = new Kysely.PostgresDialect({ pool: await newPgPool(url, skipHealthcheck) }); + const client = new Kysely.Kysely({ + dialect, + log(event) { + if (event.level === "error") { + logger.error("kysely query fail", formatEvent(event)); + } else { + if (event.queryDurationMillis >= logSlowSQL.milliseconds) { + logger.warn("kysely slow query", formatEvent(event)); + } else if (logSQL) { + logger.info("kysely raw query", formatEvent(event)); + } + } + }, + }); + return client; +} + +export async function newPgPool(url: string, skipHealthcheck?: boolean): Promise { + const pool = new Pg.Pool({ connectionString: url }); + if (!skipHealthcheck) { + await pool.query("SELECT 1;"); // healthcheck + } + return pool; +} diff --git a/apps/long-short-backend/src/database/redis.ts b/apps/long-short-backend/src/database/redis.ts new file mode 100644 index 0000000..3be721a --- /dev/null +++ b/apps/long-short-backend/src/database/redis.ts @@ -0,0 +1,16 @@ +import { Redis, type RedisOptions } from "ioredis"; + +export function newRedis(url: string, connectionName: string, options?: RedisOptions): Redis { + const redis = new Redis(url, { + ...options, + db: 0, + connectionName, + enableReadyCheck: true, + maxRetriesPerRequest: 10, + showFriendlyErrorStack: false, + retryStrategy(times): number { + return Math.min(times * 10, 2000); + }, + }); + return redis; +} diff --git a/apps/long-short-backend/src/healthchecker.ts b/apps/long-short-backend/src/healthchecker.ts new file mode 100644 index 0000000..55bdb5e --- /dev/null +++ b/apps/long-short-backend/src/healthchecker.ts @@ -0,0 +1,24 @@ +const DEFAULT_HEALTH_PORT = 9999; + +type HealthCheckFn = () => Promise; + +// This is an /health API that K8s will check often, if service is unhealthy then K8s will restart it +export class HealthChecker { + private checkers: HealthCheckFn[] = []; + constructor(port: number = DEFAULT_HEALTH_PORT) { + Bun.serve({ + port, + routes: { + "/health": async () => { + await Promise.all(this.checkers.map((fn) => fn())); + return new Response("ok"); + }, + }, + }); + } + + // To report unhealthy, just throw error + public addChecker(fn: HealthCheckFn): void { + this.checkers.push(fn); + } +} diff --git a/apps/long-short-backend/src/provider/cardanoscan.ts b/apps/long-short-backend/src/provider/cardanoscan.ts new file mode 100644 index 0000000..0c751a4 --- /dev/null +++ b/apps/long-short-backend/src/provider/cardanoscan.ts @@ -0,0 +1,326 @@ +import type { Address } from "@minswap/felis-ledger-core"; +import { logger } from "../utils"; + +/** + * Transaction input/output structure from Cardanoscan API + */ +export type CardanoscanTxIO = { + address: string; + value: string; + tokens?: Array<{ + value: string; + assetId: string; + }>; + datum?: string; + scriptRef?: string; +}; + +/** + * Transaction object from Cardanoscan API + */ +export type CardanoscanTransaction = { + hash: string; + blockHash: string; + fees: string; + slot: number; + epoch: number; + blockHeight: number; + timestamp: string; // ISO 8601 date-time + index: number; + inputs: (CardanoscanTxIO & { txId: string; index: number })[]; + outputs: CardanoscanTxIO[]; + collateral: CardanoscanTxIO[]; + certificates?: { + stakeDelegations?: Array<{ + stakeAddress: string; + poolId: string; + }>; + poolRegistrations?: Array>; + governanceActions?: Array>; + }; + withdrawals?: Array<{ + address: string; + amount: string; + }>; + metadata?: { + hash: string; + labels: Record; + }; + mint?: Array<{ + quantity: string; + unit: string; + }>; + redeemers?: Array<{ + index: number; + purpose: string; + scriptHash: string; + redeemerDataHash: string; + executionUnits: { + memory: number; + steps: number; + }; + }>; + status: boolean; + votingProcedures?: Array>; +}; + +/** + * Response from Cardanoscan transaction list API + */ +export type CardanoscanTransactionListResponse = { + pageNo: number; + limit: number; + transactions: CardanoscanTransaction[]; +}; + +/** + * Options for fetching transaction list + */ +export type GetTransactionListOptions = { + address: string; + pageNo: number; + limit?: number; // 1-50, default 20 + order: "asc" | "desc"; // default "desc" +}; + +/** + * Cardanoscan API Provider + * Provides access to Cardano blockchain data via Cardanoscan API + */ +export class CardanoscanProvider { + public static readonly MAINNET_URL = "https://api.cardanoscan.io/api/v1"; + public static readonly PREVIEW_URL = "https://api-preview.cardanoscan.io/api/v1"; + + private readonly baseUrl: string; + private readonly apiKey: string; + + constructor(baseUrl: string, apiKey: string) { + this.baseUrl = baseUrl.replace(/\/$/, ""); // Remove trailing slash + this.apiKey = apiKey; + } + + /** + * Get transaction list for a specific address + * @param options - Request options including address, pagination, and ordering + * @returns Transaction list response with pagination info + */ + async getTransactionList(options: GetTransactionListOptions): Promise { + const { address, pageNo, limit = 20, order } = options; + + // Validate parameters + if (!address || address.length > 200) { + throw new Error("Address is required and must be max 200 characters"); + } + if (pageNo < 1) { + throw new Error("pageNo must be at least 1"); + } + if (limit && (limit < 1 || limit > 50)) { + throw new Error("limit must be between 1 and 50"); + } + if (order && !["asc", "desc"].includes(order)) { + throw new Error("order must be 'asc' or 'desc'"); + } + + const url = new URL(`${this.baseUrl}/transaction/list`); + url.searchParams.set("address", address); + url.searchParams.set("pageNo", pageNo.toString()); + url.searchParams.set("limit", limit.toString()); + url.searchParams.set("order", order); + + try { + const response = await fetch(url.toString(), { + method: "GET", + headers: { + Accept: "application/json", + apiKey: this.apiKey, + }, + }); + + if (!response.ok) { + const errorText = await response.text(); + logger.error("Cardanoscan API error", { + status: response.status, + statusText: response.statusText, + body: errorText, + }); + throw new Error(`Cardanoscan API error: ${response.status} ${response.statusText}`); + } + + const data = (await response.json()) as CardanoscanTransactionListResponse; + return data; + } catch (error) { + logger.error("Failed to fetch transaction list from Cardanoscan", error); + throw error; + } + } + + /** + * Get transaction list for an Address object + * @param address - Address to query + * @param pageNo - Page number (1-indexed) + * @param limit - Results per page (1-50, default 20) + * @param order - Sort order (asc or desc, default desc) + * @returns Transaction list response + */ + async getTransactionListByAddress( + address: Address, + pageNo: number, + limit: number, + order: "asc" | "desc", + ): Promise { + return this.getTransactionList({ + address: address.toHex(), + pageNo, + limit, + order, + }); + } + + /** + * Get all transactions for an address (handles pagination automatically) + * Warning: This can make many API calls for addresses with many transactions + * @param address - Address to query + * @param maxPages - Maximum number of pages to fetch (default: no limit) + * @returns All transactions + */ + async getAllTransactionsByAddress(address: Address, maxPages?: number): Promise { + const allTransactions: CardanoscanTransaction[] = []; + let pageNo = 1; + let hasMore = true; + + while (hasMore && (!maxPages || pageNo <= maxPages)) { + const response = await this.getTransactionListByAddress(address, pageNo, 50, "desc"); + allTransactions.push(...response.transactions); + + // If we got fewer transactions than the limit, we've reached the end + hasMore = response.transactions.length === response.limit; + pageNo++; + } + + logger.info(`Fetched ${allTransactions.length} transactions for ${address.bech32}`, { + pages: pageNo - 1, + }); + + return allTransactions; + } + + /** + * Get the most recent transaction for an address + * @param address - Address to query + * @returns Most recent transaction or null if no transactions found + */ + async getLatestTransaction(address: Address): Promise { + const response = await this.getTransactionListByAddress(address, 1, 1, "desc"); + return response.transactions[0] || null; + } + + /** + * Get transactions within a specific slot range + * @param address - Address to query + * @param minSlot - Minimum slot number (inclusive) + * @param maxSlot - Maximum slot number (inclusive) + * @returns Transactions within the slot range + */ + async getTransactionsBySlotRange( + address: Address, + minSlot: number, + maxSlot: number, + ): Promise { + const allTransactions = await this.getAllTransactionsByAddress(address); + return allTransactions.filter((tx) => tx.slot >= minSlot && tx.slot <= maxSlot); + } + + /** + * Check if a transaction exists for an address + * @param address - Address to query + * @param txHash - Transaction hash to find + * @returns The transaction if found, null otherwise + */ + async findTransactionByHash( + address: Address, + txHash: string, + pageSize?: number, + maxPage?: number, + ): Promise { + // Start from most recent and work backwards + let pageNo = 1; + let hasMore = true; + + while (hasMore) { + const response = await this.getTransactionListByAddress(address, pageNo, pageSize ?? 50, "desc"); + + const found = response.transactions.find((tx) => tx.hash === txHash); + if (found) { + return found; + } + + hasMore = response.transactions.length === response.limit; + pageNo++; + + // Safety limit to prevent infinite loops + if (pageNo > (maxPage ?? 100)) { + logger.warn(`Searched ${maxPage ?? 100} pages without finding transaction ${txHash}`); + break; + } + } + + return null; + } + + /** + * Find the transaction that spent a specific UTXO + * @param address - Address to query + * @param txHash - Transaction hash of the UTXO + * @param index - Output index of the UTXO + * @param pageSize - Number of transactions per page (default: 50) + * @param maxPage - Maximum pages to search (default: 100) + * @returns The transaction that spent the UTXO, or null if not found + */ + async findTransactionHasSpent( + address: Address, + txHash: string, + index: number, + pageSize?: number, + maxPage?: number, + ): Promise { + // Start from most recent and work backwards + let pageNo = 1; + let hasMore = true; + + logger.info("Searching for transaction that spent UTXO", { + address: address.bech32, + txHash, + outputIndex: index, + }); + + while (hasMore) { + const response = await this.getTransactionListByAddress(address, pageNo, pageSize ?? 50, "desc"); + + // Check each transaction's inputs for the specified UTXO + for (const tx of response.transactions) { + const spentInput = tx.inputs.find((input) => input.txId === txHash && input.index === index); + + if (spentInput) { + logger.info("Found transaction that spent UTXO", { + spendingTxHash: tx.hash, + utxoTxHash: txHash, + utxoIndex: index, + }); + return tx; + } + } + + hasMore = response.transactions.length === response.limit; + pageNo++; + + // Safety limit to prevent infinite loops + if (pageNo > (maxPage ?? 100)) { + logger.warn(`Searched ${maxPage ?? 100} pages without finding spending transaction for ${txHash}:${index}`); + break; + } + } + + logger.info("UTXO not spent yet", { txHash, outputIndex: index }); + return null; + } +} diff --git a/apps/long-short-backend/src/provider/index.ts b/apps/long-short-backend/src/provider/index.ts new file mode 100644 index 0000000..c5adcdf --- /dev/null +++ b/apps/long-short-backend/src/provider/index.ts @@ -0,0 +1,3 @@ +export * from "./cardanoscan"; +export * from "./kupo"; +export * from "./minswap-aggregator"; diff --git a/apps/long-short-backend/src/provider/kupo.ts b/apps/long-short-backend/src/provider/kupo.ts new file mode 100644 index 0000000..fb8798d --- /dev/null +++ b/apps/long-short-backend/src/provider/kupo.ts @@ -0,0 +1,352 @@ +import { Address, type Asset, type Bytes, type KupoUtxo, type TxIn, Utxo, Value } from "@minswap/felis-ledger-core"; +import { type CborHex, type CSLTransactionUnspentOutput, Maybe } from "@minswap/felis-ledger-utils"; +import invariant from "@minswap/tiny-invariant"; +import * as R from "remeda"; +import { logger, uniq } from "../utils"; + +const KUPO_MAX_REQUEST = 100; + +const DATUM_REGEX = /^[0-9a-fA-F]{64}$/; + +export type KupoHealthResponse = { + connection_status: "connected" | "disconnected"; + most_recent_checkpoint: number | null; + most_recent_node_tip: number | null; + version: string; + network_synchronization?: number; // new from v2.11.0+de9c52 +}; + +export type KupoHealth = { + connectionStatus: "connected" | "disconnected"; + isHealthy: boolean; + latestSyncedSlot: number; + latestNodeSyncedSlot: number; + syncPercent: number; + version: string; +}; + +export type KupoUtxosResponse = { + transaction_id: string; + transaction_index: bigint; + output_index: bigint; + address: string; + value: { + coins: bigint; + assets?: Record; + }; + datum_hash: string | null; + datum_type?: "inline" | "hash" | null; + script_hash: string | null; + created_at: { + slot_no: bigint; + header_hash: string; + }; + spent_at: { + slot_no: bigint; + header_hash: string; + } | null; +}; + +export class KupoService { + static readonly MAX_REQUEST_THRESHOLD = 20; + kupoBaseUrl: string; + + constructor(kupoBaseUrl: string) { + this.kupoBaseUrl = kupoBaseUrl; + } + + static async new(kupoBaseUrl: string, healthCheck?: boolean): Promise { + const kupoService = new KupoService(kupoBaseUrl); + if (healthCheck) { + const health = await kupoService.health(); + logger.info("Kupo service health", { connection_status: health.connectionStatus }); + if (health.connectionStatus === "disconnected") { + throw new Error("Kupo is not connected"); + } else if (!health.isHealthy) { + throw new Error("Kupo is not healthy"); + } + } + return kupoService; + } + + async getCurrentSlot(): Promise { + const health = await this.health(); + return health.latestNodeSyncedSlot; + } + + async utxosByAddress(addresses: Address[]): Promise { + let utxos: KupoUtxo[] = []; + for (let i = 0; i < addresses.length; i += KUPO_MAX_REQUEST) { + const queryAddresses = addresses.slice(i, i + KUPO_MAX_REQUEST); + const tasks: Promise[] = []; + for (const address of queryAddresses) { + const matches = `${address.bech32}?unspent`; + tasks.push(this.getUtxosByMatches(matches)); + } + const queriedUtxos = await Promise.all(tasks); + utxos = utxos.concat(queriedUtxos.flat()); + } + return await this.parseUtxo(utxos); + } + + async utxosRawByAddress(addresses: Address): Promise[]> { + const utxos = await this.utxosByAddress([addresses]); + return utxos.map(Utxo.toHex); + } + + async utxosByTxIn(...txIns: TxIn[]): Promise { + const utxos = await this.kupoUtxosByTxIns(...txIns); + const sdkUtxos = await this.parseUtxo(utxos); + return sdkUtxos; + } + + async kupoUtxosByTxHash(txHash: string): Promise { + const matches = `*@${txHash}?unspent`; + return await this.getUtxosByMatches(matches); + } + + async kupoUtxosByTxIn(txIn: TxIn): Promise { + const matches = `${txIn.index}@${txIn.txId.hex}?unspent`; + return await this.getUtxosByMatches(matches); + } + + async kupoUtxosByTxIns(...txIns: TxIn[]): Promise { + const utxos: KupoUtxo[] = []; + for (let i = 0; i < txIns.length; i += KUPO_MAX_REQUEST) { + const queryTxIns = txIns.slice(i, i + KUPO_MAX_REQUEST); + const tasks: Promise[] = []; + for (const txIn of queryTxIns) { + tasks.push(this.kupoUtxosByTxIn(txIn)); + } + const queriedUtxos = await Promise.all(tasks); + utxos.push(...queriedUtxos.flat()); + } + return utxos; + } + + async kupoUtxosByAddress(address: Address): Promise { + const matches = `${address.bech32}?unspent`; + return await this.getUtxosByMatches(matches); + } + + async kupoUtxosByAddresses(addresses: Address[]): Promise { + if (addresses.length === 0) { + return []; + } + + let utxos: KupoUtxo[] = []; + for (let i = 0; i < addresses.length; i += KUPO_MAX_REQUEST) { + const queryAddresses = addresses.slice(i, i + KUPO_MAX_REQUEST); + const tasks: Promise[] = []; + for (const address of queryAddresses) { + tasks.push(this.kupoUtxosByAddress(address)); + } + const queriedUtxos = await Promise.all(tasks); + utxos = utxos.concat(queriedUtxos.flat()); + } + + return utxos; + } + + async kupoUtxosByAsset(asset: Asset): Promise { + try { + const matches = `${asset.currencySymbol.hex}.${asset.tokenName.hex}?unspent`; + return await this.getUtxosByMatches(matches); + } catch (_) { + logger.error(`fail to fetch utxos for asset ${asset.toString()}`); + return []; + } + } + + async kupoUtxosByAssets(assets: Asset[]): Promise { + if (assets.length === 0) { + return []; + } + const chunks = R.chunk(assets, KupoService.MAX_REQUEST_THRESHOLD); + let utxos: KupoUtxo[] = []; + for (const chunk of chunks) { + const inner = await Promise.all(chunk.map((a) => this.kupoUtxosByAsset(a))); + utxos = utxos.concat(inner.flat()); + } + return utxos; + } + + async kupoUtxosByPolicyID(policyID: Bytes): Promise { + const matches = `${policyID.hex}.*?unspent`; + return await this.getUtxosByMatches(matches); + } + + async health(): Promise { + const kupoUrl = `${this.kupoBaseUrl}/health`; + const fetchRes = await fetch(kupoUrl, { + headers: { Accept: "application/json;charset=utf-8" }, + }); + const response = (await fetchRes.json()) as KupoHealthResponse; + + // We assume slot won't exceed MAX_SAFE_INTEGER + const latestSyncedSlot = response.most_recent_checkpoint ?? 0; + const latestNodeSyncedSlot = response.most_recent_node_tip ?? 0; + const syncPercent = latestNodeSyncedSlot > 0 ? latestSyncedSlot / latestNodeSyncedSlot : 0; + const isHealthy = latestSyncedSlot === latestNodeSyncedSlot; + return { + connectionStatus: response.connection_status, + isHealthy: isHealthy, + latestSyncedSlot: latestSyncedSlot, + latestNodeSyncedSlot: latestNodeSyncedSlot, + syncPercent: syncPercent, + version: response.version, + }; + } + + async getUtxosByMatches(matches: string): Promise { + const kupoUrl = `${this.kupoBaseUrl}/matches/${matches}`; + const fetchRes = await fetch(kupoUrl); + const data = (await fetchRes.json()) as KupoUtxosResponse[]; + return data.map((d) => ({ + ...d, + transaction_index: Number(d.transaction_index.toString()), + output_index: Number(d.output_index.toString()), + })); + } + + async getUtxosByPaymentCredential(paymentCredential: Bytes): Promise { + const matches = `${paymentCredential.hex}/*?unspent`; + const kupoUtxos = await this.getUtxosByMatches(matches); + const sdkUtxos = await this.parseUtxo(kupoUtxos.flat()); + return sdkUtxos; + } + + async getKupoUtxosByPaymentCredential(paymentCredential: Bytes): Promise { + const matches = `${paymentCredential.hex}/*?unspent`; + return await this.getUtxosByMatches(matches); + } + + async getKupoUtxosByPaymentCredentials(paymentCredentials: Bytes[]): Promise { + if (paymentCredentials.length === 0) { + return []; + } + + let utxos: KupoUtxo[] = []; + for (let i = 0; i < paymentCredentials.length; i += KUPO_MAX_REQUEST) { + const queryCredentials = paymentCredentials.slice(i, i + KUPO_MAX_REQUEST); + const tasks: Promise[] = []; + for (const credential of queryCredentials) { + tasks.push(this.getKupoUtxosByPaymentCredential(credential)); + } + const queriedUtxos = await Promise.all(tasks); + utxos = utxos.concat(queriedUtxos.flat()); + } + + return utxos; + } + + private isValidDatumHash(hash: string): boolean { + // Check if the hash is a valid base16 string + return DATUM_REGEX.test(hash); + } + + async getDatum(datumHash: string): Promise { + if (!this.isValidDatumHash(datumHash)) { + return null; + } + const kupoUrl = `${this.kupoBaseUrl}/datums/${datumHash}`; + const fetchRes = await fetch(kupoUrl); + const data = (await fetchRes.json()) as { datum: string } | null; + return data ? data.datum : null; + } + + async getDatums(datumHashes: string[]): Promise> { + if (datumHashes.length === 0) { + return {}; + } + const uniqueDatumHashes = uniq(datumHashes); + const mapDatum: Record = {}; + for (let i = 0; i < uniqueDatumHashes.length; i += KUPO_MAX_REQUEST) { + const queryDatumHashes = uniqueDatumHashes.slice(i, i + KUPO_MAX_REQUEST); + const tasks: Promise[] = []; + for (const datumHash of queryDatumHashes) { + tasks.push(this.getDatum(datumHash)); + } + const datums: (string | null)[] = await Promise.all(tasks); + for (let i = 0; i < queryDatumHashes.length; i++) { + const datum = datums[i]; + if (datum) { + mapDatum[queryDatumHashes[i]] = datum; + } + } + } + return mapDatum; + } + + async getDatumHashByAssets(...assets: Asset[]): Promise> { + if (assets.length === 0) { + return {}; + } + const mapDatumHash: Record = {}; + for (let i = 0; i < assets.length; i += KUPO_MAX_REQUEST) { + const queryAssets = assets.slice(i, i + KUPO_MAX_REQUEST); + const tasks: Promise[] = []; + for (const asset of queryAssets) { + tasks.push(this.kupoUtxosByAsset(asset)); + } + const queriedUtxos = await Promise.all(tasks); + for (let i = 0; i < queryAssets.length; i++) { + const utxos = queriedUtxos[i]; + if (utxos.length > 0 && utxos[0].datum_hash) { + mapDatumHash[queryAssets[i].toString()] = utxos[0].datum_hash; + } + } + } + return mapDatumHash; + } + + private async parseUtxo(kupoUtxos: KupoUtxo[]): Promise { + const datumHashes = kupoUtxos.filter((u) => u.datum_hash && u.datum_type).map((u) => u.datum_hash) as string[]; + + if (!datumHashes.length) { + return kupoUtxos.map((u) => Utxo.fromKupo(u)); + } + const mapDatum: Record = await this.getDatums(datumHashes); + const utxos: Utxo[] = []; + for (const u of kupoUtxos) { + /** + * In Kupo, inline datums are not stored in the UTXO, so we need to fetch them separately. + */ + if (u.datum_hash && u.datum_type === "inline") { + const datumRaw = mapDatum[u.datum_hash]; + invariant(datumRaw, `InlineDatum requires a valid datum, not found datum for ${u.datum_hash}`); + utxos.push(Utxo.fromKupo(u, datumRaw)); + } else if (u.datum_hash && u.datum_type === "hash") { + utxos.push(Utxo.fromKupo(u, mapDatum[u.datum_hash])); + } else { + utxos.push(Utxo.fromKupo(u)); + } + } + return utxos; + } + + async getBalance(...addresses: Address[]): Promise { + return Utxo.sumValue(await this.utxosByAddress(addresses)); + } + + async getBalanceOfPubKeyAddress(address: string): Promise { + const matches = `${address}?unspent`; + const kupoUtxos = await this.getUtxosByMatches(matches); + const balance = new Value(); + for (const utxo of kupoUtxos) { + const address = Address.fromBech32(utxo.address); + if (Maybe.isJust(address.toScriptHash())) { + continue; + } + const utxoVal = Value.fromKupo(utxo.value); + balance.addAll(utxoVal); + } + return balance; + } + + async utxosAtWithAsset(address: Address, asset: Asset): Promise { + const matches = `${address.bech32}?unspent&policy_id=${asset.currencySymbol.hex}${asset.tokenName.hex ? `&asset_name=${asset.tokenName.hex}` : ""}`; + const utxos: KupoUtxo[] = await this.getUtxosByMatches(matches); + return await this.parseUtxo(utxos); + } +} diff --git a/apps/long-short-backend/src/provider/minswap-aggregator.ts b/apps/long-short-backend/src/provider/minswap-aggregator.ts new file mode 100644 index 0000000..ce8190e --- /dev/null +++ b/apps/long-short-backend/src/provider/minswap-aggregator.ts @@ -0,0 +1,76 @@ +import { NetworkEnvironment } from "@minswap/felis-ledger-core"; +import { logger } from "../utils"; + +const BASE_URLS: Record = { + [NetworkEnvironment.MAINNET]: "https://monorepo-mainnet-prod.minswap.org", + [NetworkEnvironment.TESTNET_PREVIEW]: "https://aggr.dev-3.minswap.org", + [NetworkEnvironment.TESTNET_PREPROD]: "https://aggr.dev-3.minswap.org", +}; + +export type EstimateRequest = { + /** Amount to swap (as string, in smallest unit) */ + amount: string; + /** Input token ID (e.g. "lovelace" or concatenated policyId + tokenName) */ + tokenIn: string; + /** Output token ID (e.g. concatenated policyId + tokenName) */ + tokenOut: string; + /** Slippage tolerance in percent (default: 1) */ + slippage?: number; +}; + +export type EstimateResponse = { + amountIn: string; + amountOut: string; + minAmountOut: string; + avgPriceImpact: number; +}; + +export class MinswapAggregatorProvider { + private readonly baseUrl: string; + + constructor(networkEnv: NetworkEnvironment) { + this.baseUrl = BASE_URLS[networkEnv]; + } + + /** + * Estimate swap output amount via Minswap aggregator. + * Token IDs use concatenated format (no dot): policyId + tokenName, or "lovelace". + */ + async estimate(request: EstimateRequest): Promise { + const { amount, tokenIn, tokenOut, slippage = 1 } = request; + + const url = `${this.baseUrl}/aggregator/estimate`; + const body = JSON.stringify({ + amount, + token_in: tokenIn, + token_out: tokenOut, + slippage, + exclude_protocols: ["MuesliSwap"], + allow_multi_hops: true, + }); + + const response = await fetch(url, { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json", + }, + body, + }); + + if (!response.ok) { + const text = await response.text(); + logger.error("Minswap aggregator estimate failed", { status: response.status, body: text }); + throw new Error(`Minswap aggregator estimate failed: ${response.status} ${text}`); + } + + const data = await response.json(); + + return { + amountIn: data.amount_in, + amountOut: data.amount_out, + minAmountOut: data.min_amount_out, + avgPriceImpact: data.avg_price_impact, + }; + } +} diff --git a/apps/long-short-backend/src/repository/index.ts b/apps/long-short-backend/src/repository/index.ts new file mode 100644 index 0000000..55dc146 --- /dev/null +++ b/apps/long-short-backend/src/repository/index.ts @@ -0,0 +1,3 @@ +export * from "./position-repository"; +export * from "./redis-repo"; +export * from "./repository"; diff --git a/apps/long-short-backend/src/repository/market-config-repository.ts b/apps/long-short-backend/src/repository/market-config-repository.ts new file mode 100644 index 0000000..cdefa3b --- /dev/null +++ b/apps/long-short-backend/src/repository/market-config-repository.ts @@ -0,0 +1,32 @@ +import type { Kysely, Selectable, Transaction } from "kysely"; +import type { DB } from "../database"; + +export type MarketConfigRow = Selectable; + +export namespace MarketConfigRepository { + /** + * Get market config row by market ID + */ + export async function getMarketConfigRowById( + db: Kysely | Transaction, + marketId: string, + ): Promise { + const result = await db + .selectFrom("market_config") + .selectAll() + .where("market_id", "=", marketId) + .executeTakeFirst(); + + return result ?? null; + } + + /** + * Get market config row by market ID (throws if not found) + */ + export async function getMarketConfigRowByIdOrThrow( + db: Kysely | Transaction, + marketId: string, + ): Promise { + return db.selectFrom("market_config").selectAll().where("market_id", "=", marketId).executeTakeFirstOrThrow(); + } +} diff --git a/apps/long-short-backend/src/repository/order-repository.ts b/apps/long-short-backend/src/repository/order-repository.ts new file mode 100644 index 0000000..1f228c3 --- /dev/null +++ b/apps/long-short-backend/src/repository/order-repository.ts @@ -0,0 +1,312 @@ +import type { Kysely, Transaction } from "kysely"; +import type { DB } from "../database"; + +export type CreateOrderParams = { + positionId: bigint; + orderType: string; + createdTxId?: string | null; + createdTxIndex?: number | null; + assetIn?: string | null; + amountIn?: string | null; + assetOut?: string | null; + amountOut?: string | null; +}; + +export type Order = { + id: bigint; + positionId: bigint; + orderType: string; + createdTxId: string | null; + createdTxIndex: number | null; + assetIn: string | null; + amountIn: string | null; + assetOut: string | null; + amountOut: string | null; + builtTxId: string | null; + builtValidTo: Date | null; + waiting: boolean; +}; + +export namespace OrderRepository { + export async function createOrder(db: Kysely | Transaction, params: CreateOrderParams): Promise { + const result = await db + .insertInto("order") + .values({ + position_id: params.positionId.toString(), + order_type: params.orderType, + created_tx_id: params.createdTxId ?? null, + created_tx_index: params.createdTxIndex ?? null, + asset_in: params.assetIn ?? null, + amount_in: params.amountIn ?? null, + asset_out: params.assetOut ?? null, + amount_out: params.amountOut ?? null, + }) + .returningAll() + .executeTakeFirstOrThrow(); + + return mapOrderRow(result); + } + + export async function createOrders(db: Kysely | Transaction, params: CreateOrderParams[]): Promise { + const results = await db + .insertInto("order") + .values( + params.map((p) => ({ + position_id: p.positionId.toString(), + order_type: p.orderType, + created_tx_id: p.createdTxId ?? null, + created_tx_index: p.createdTxIndex ?? null, + asset_in: p.assetIn ?? null, + amount_in: p.amountIn ?? null, + asset_out: p.assetOut ?? null, + amount_out: p.amountOut ?? null, + })), + ) + .returningAll() + .execute(); + + return results.map(mapOrderRow); + } + + export async function getOrdersByPositionId(db: Kysely | Transaction, positionId: bigint): Promise { + const results = await db.selectFrom("order").selectAll().where("position_id", "=", positionId.toString()).execute(); + + return results.map(mapOrderRow); + } + + /** + * Find the next unhandled order for a position. + * An order is ready to handle if it has asset_in, amount_in, asset_out and created_tx_id is null or empty. + */ + export async function getNextUnhandledOrder( + db: Kysely | Transaction, + positionId: bigint, + ): Promise { + const result = await db + .selectFrom("order") + .selectAll() + .where("position_id", "=", positionId.toString()) + .where((eb) => eb.or([eb("created_tx_id", "is", null), eb("created_tx_id", "=", "")])) + .where("asset_in", "is not", null) + .where("amount_in", "is not", null) + .where("asset_out", "is not", null) + .orderBy("id", "asc") + .executeTakeFirst(); + + return result ? mapOrderRow(result) : null; + } + + /** + * Update order built_tx fields after building transaction + */ + export async function updateOrderBuiltTx( + db: Kysely | Transaction, + orderId: bigint, + builtTxId: string, + builtValidTo: Date, + ): Promise { + await db + .updateTable("order") + .set({ + built_tx_id: builtTxId, + built_valid_to: builtValidTo, + }) + .where("id", "=", orderId.toString()) + .execute(); + } + + /** + * Update order created_tx fields when transaction is found on chain + */ + export async function updateOrderCreatedTx( + db: Kysely | Transaction, + orderId: bigint, + createdTxId: string, + createdTxIndex: number, + ): Promise { + await db + .updateTable("order") + .set({ + created_tx_id: createdTxId, + created_tx_index: createdTxIndex, + }) + .where("id", "=", orderId.toString()) + .execute(); + } + + /** + * Update order with next order details when current order output is spent + */ + export async function updateOrderNextDetails( + db: Kysely | Transaction, + orderId: bigint, + assetIn: string, + amountIn: string, + assetOut: string, + ): Promise { + await db + .updateTable("order") + .set({ + asset_in: assetIn, + amount_in: amountIn, + asset_out: assetOut, + }) + .where("id", "=", orderId.toString()) + .execute(); + } + + /** + * Find an order that has created_tx_id not null and waiting = true + * This order has been confirmed on chain and is waiting for its output to be spent + */ + export async function getWaitingOrder(db: Kysely | Transaction, positionId: bigint): Promise { + const result = await db + .selectFrom("order") + .selectAll() + .where("position_id", "=", positionId.toString()) + .where("created_tx_id", "is not", null) + .where("waiting", "=", true) + .orderBy("id", "asc") + .executeTakeFirst(); + + return result ? mapOrderRow(result) : null; + } + + /** + * Set order waiting flag to false + */ + export async function setOrderWaiting( + db: Kysely | Transaction, + orderId: bigint, + waiting: boolean, + ): Promise { + await db.updateTable("order").set({ waiting }).where("id", "=", orderId.toString()).execute(); + } + + /** + * Update order amount_out after transaction is confirmed + */ + export async function updateOrderAmountOut( + db: Kysely | Transaction, + orderId: bigint, + amountOut: string, + ): Promise { + await db.updateTable("order").set({ amount_out: amountOut }).where("id", "=", orderId.toString()).execute(); + } + + /** + * Complete an order: set amount_out and waiting = false + */ + export async function completeOrder( + db: Kysely | Transaction, + orderId: bigint, + amountOut: string, + ): Promise { + await db + .updateTable("order") + .set({ amount_out: amountOut, waiting: false }) + .where("id", "=", orderId.toString()) + .execute(); + } + + /** + * Get order by position ID and order type + */ + export async function getOrderByPositionAndType( + db: Kysely | Transaction, + positionId: bigint, + orderType: string, + ): Promise { + const result = await db + .selectFrom("order") + .selectAll() + .where("position_id", "=", positionId.toString()) + .where("order_type", "=", orderType) + .executeTakeFirst(); + + return result ? mapOrderRow(result) : null; + } + + /** + * Get order by ID + */ + export async function getOrderById(db: Kysely | Transaction, orderId: bigint): Promise { + const result = await db.selectFrom("order").selectAll().where("id", "=", orderId.toString()).executeTakeFirst(); + + return result ? mapOrderRow(result) : null; + } + + export type TransitionToNextOrderParams = { + currentOrderId: bigint; + positionId: bigint; + nextOrderType: string; + assetIn: string; + amountIn: string; + assetOut: string; + /** Amount out of current order (to update order.amount_out) */ + amountOut: string; + }; + + export type TransitionToNextOrderResult = { success: true; nextOrder: Order } | { success: false; error: string }; + + /** + * Transition from current order to next order: + * 1. Find the next order by position and type + * 2. Update current order amount_out (= next order amountIn) + * 3. Update next order with assetIn, amountIn, assetOut + * 4. Set current order waiting = false + */ + export async function transitionToNextOrder( + db: Kysely | Transaction, + params: TransitionToNextOrderParams, + ): Promise { + const { currentOrderId, positionId, nextOrderType, assetIn, amountIn, assetOut, amountOut } = params; + + // Find the next order + const nextOrder = await getOrderByPositionAndType(db, positionId, nextOrderType); + if (!nextOrder) { + return { + success: false, + error: `Next order with type "${nextOrderType}" not found`, + }; + } + + // Update current order amount_out + await updateOrderAmountOut(db, currentOrderId, amountOut); + + // Update next order details + await updateOrderNextDetails(db, nextOrder.id, assetIn, amountIn, assetOut); + + // Set current order waiting = false + await setOrderWaiting(db, currentOrderId, false); + + // Return updated next order + return { + success: true, + nextOrder: { + ...nextOrder, + assetIn, + amountIn, + assetOut, + }, + }; + } + + // biome-ignore lint/suspicious/noExplicitAny: DB row type + function mapOrderRow(row: any): Order { + return { + id: BigInt(row.id), + positionId: BigInt(row.position_id), + orderType: row.order_type, + createdTxId: row.created_tx_id, + createdTxIndex: row.created_tx_index, + assetIn: row.asset_in, + amountIn: row.amount_in, + assetOut: row.asset_out, + amountOut: row.amount_out, + builtTxId: row.built_tx_id, + builtValidTo: row.built_valid_to ? new Date(row.built_valid_to) : null, + waiting: row.waiting, + }; + } +} diff --git a/apps/long-short-backend/src/repository/position-repository.ts b/apps/long-short-backend/src/repository/position-repository.ts new file mode 100644 index 0000000..106658d --- /dev/null +++ b/apps/long-short-backend/src/repository/position-repository.ts @@ -0,0 +1,150 @@ +import type { Kysely, Transaction } from "kysely"; +import { StateMachine } from "../api/state-machine"; +import type { DB } from "../database"; + +export type CreatePositionParams = { + marketId: string; + userAddress: string; + side: StateMachine.PositionSide; + amountIn: string; + amountBorrow: string; +}; + +export type Position = { + id: bigint; + marketId: string; + userAddress: string; + side: StateMachine.PositionSide; + status: StateMachine.PositionStatus; + amountIn: string; + amountBorrow: string; + createdAt: Date; + closedAt: Date | null; +}; + +export namespace PositionRepository { + export async function createPosition( + db: Kysely | Transaction, + params: CreatePositionParams, + ): Promise { + const result = await db + .insertInto("position") + .values({ + market_id: params.marketId, + user_address: params.userAddress, + side: params.side, + status: StateMachine.PositionStatus.PENDING, + amount_in: params.amountIn, + amount_borrow: params.amountBorrow, + }) + .returningAll() + .executeTakeFirstOrThrow(); + + return mapPositionRow(result); + } + + export async function getPositionById(db: Kysely | Transaction, id: bigint): Promise { + const result = await db.selectFrom("position").selectAll().where("id", "=", id.toString()).executeTakeFirst(); + + return result ? mapPositionRow(result) : null; + } + + export async function getOpenPositionByUser( + db: Kysely | Transaction, + userAddress: string, + ): Promise { + const result = await db + .selectFrom("position") + .selectAll() + .where("user_address", "=", userAddress) + .where("closed_at", "is", null) + .executeTakeFirst(); + + return result ? mapPositionRow(result) : null; + } + + export async function getOpenPositionByUserAndMarket( + db: Kysely | Transaction, + userAddress: string, + marketId: string, + ): Promise { + const result = await db + .selectFrom("position") + .selectAll() + .where("user_address", "=", userAddress) + .where("market_id", "=", marketId) + .where("closed_at", "is", null) + .executeTakeFirst(); + + return result ? mapPositionRow(result) : null; + } + + export async function getUserOpenPositions( + db: Kysely | Transaction, + userAddress: string, + ): Promise { + const results = await db + .selectFrom("position") + .selectAll() + .where("user_address", "=", userAddress) + .where("closed_at", "is", null) + .orderBy("created_at", "desc") + .execute(); + + return results.map(mapPositionRow); + } + + export async function getUserPositions( + db: Kysely | Transaction, + userAddress: string, + options?: { includesClosed?: boolean; limit?: number; offset?: number }, + ): Promise { + let query = db.selectFrom("position").selectAll().where("user_address", "=", userAddress); + + if (!options?.includesClosed) { + query = query.where("closed_at", "is", null); + } + + query = query.orderBy("created_at", "desc"); + + if (options?.limit) { + query = query.limit(options.limit); + } + + if (options?.offset) { + query = query.offset(options.offset); + } + + const results = await query.execute(); + return results.map(mapPositionRow); + } + + export async function updatePositionStatus( + db: Kysely | Transaction, + id: bigint, + status: StateMachine.PositionStatus, + ): Promise { + const updates: Record = { status }; + + if (status === StateMachine.PositionStatus.CLOSED) { + updates.closed_at = new Date(); + } + + await db.updateTable("position").set(updates).where("id", "=", id.toString()).execute(); + } + + // biome-ignore lint/suspicious/noExplicitAny: DB row type + function mapPositionRow(row: any): Position { + return { + id: BigInt(row.id), + marketId: row.market_id, + userAddress: row.user_address, + side: row.side as StateMachine.PositionSide, + status: row.status as StateMachine.PositionStatus, + amountIn: row.amount_in, + amountBorrow: row.amount_borrow, + createdAt: new Date(row.created_at), + closedAt: row.closed_at ? new Date(row.closed_at) : null, + }; + } +} diff --git a/apps/long-short-backend/src/repository/redis-repo.ts b/apps/long-short-backend/src/repository/redis-repo.ts new file mode 100644 index 0000000..a83967d --- /dev/null +++ b/apps/long-short-backend/src/repository/redis-repo.ts @@ -0,0 +1,61 @@ +import type * as OgmiosSchema from "@cardano-ogmios/schema"; +import type Redis from "ioredis"; + +export enum RedisKey { + INTERSECTION_CANDIDATES = "intersection_candidates", + LAST_SYNC_SLOT = "last_sync_slot", +} + +export namespace RedisRepo { + export const getIntersectionCandidates = async (redis: Redis) => { + const data = await redis.lrange(RedisKey.INTERSECTION_CANDIDATES, 0, -1); + if (!data) { + return []; + } + return data.map((d) => JSON.parse(d)); + }; + + export const rollbackIntersectionCandidates = async (redis: Redis, point: OgmiosSchema.PointOrOrigin) => { + if (point === "origin") { + await redis.del(RedisKey.INTERSECTION_CANDIDATES); + return; + } + const slot = point.slot; + const intersectionCandidates = await RedisRepo.getIntersectionCandidates(redis); + let pop_count = 0; + let foundIntersectionCandidate = false; + for (let index = 0; index < intersectionCandidates.length; index++) { + if (intersectionCandidates[index].slot <= slot) { + pop_count = index; + foundIntersectionCandidate = true; + break; + } + } + if (!foundIntersectionCandidate) { + await redis.del(RedisKey.INTERSECTION_CANDIDATES); + } else { + if (pop_count > 0) { + await redis.lpop(RedisKey.INTERSECTION_CANDIDATES, pop_count); + } + } + }; + + export const pushIntersectionCandidate = async (redis: Redis, block: { slot: number; id: string }) => { + const key = RedisKey.INTERSECTION_CANDIDATES; + const currentLength = await redis.llen(key); + const multi = redis.multi(); + if (currentLength >= 2160) { + multi.rpop(key); + } + const point: OgmiosSchema.Point = { + slot: block.slot, + id: block.id, + }; + multi.lpush(key, JSON.stringify(point)); + await multi.exec(); + }; + + export const set = (redis: Redis, key: RedisKey, value: string | number) => { + return redis.set(key, value); + }; +} diff --git a/apps/long-short-backend/src/repository/repository.ts b/apps/long-short-backend/src/repository/repository.ts new file mode 100644 index 0000000..6772dca --- /dev/null +++ b/apps/long-short-backend/src/repository/repository.ts @@ -0,0 +1,6 @@ +// This file is reserved for future blockchain syncer repository functions +// Currently, the long-short-backend only uses position-repository.ts + +export namespace Repository { + // Placeholder for future blockchain syncing functions +} diff --git a/apps/long-short-backend/src/services/index.ts b/apps/long-short-backend/src/services/index.ts new file mode 100644 index 0000000..99da2e4 --- /dev/null +++ b/apps/long-short-backend/src/services/index.ts @@ -0,0 +1 @@ +export { type CreatePositionInput, type CreatePositionResult, PositionService } from "./position-service"; diff --git a/apps/long-short-backend/src/services/position-service.ts b/apps/long-short-backend/src/services/position-service.ts new file mode 100644 index 0000000..4823224 --- /dev/null +++ b/apps/long-short-backend/src/services/position-service.ts @@ -0,0 +1,594 @@ +import { Address, Asset, type NetworkEnvironment } from "@minswap/felis-ledger-core"; +import invariant from "@minswap/tiny-invariant"; +import type { Kysely } from "kysely"; +import { StateMachine } from "../api/state-machine"; +import { getMarketConfig, isSupportedMarket } from "../config/market"; +import type { DB } from "../database"; +import type { CardanoscanProvider, MinswapAggregatorProvider } from "../provider"; +import { OrderRepository } from "../repository/order-repository"; +import { type Position, PositionRepository } from "../repository/position-repository"; +import { logger } from "../utils"; + +export type CreatePositionInput = { + userAddress: string; + marketId: string; + side: "LONG" | "SHORT"; + amountIn: bigint; +}; + +export type CreatePositionResult = { success: true; position: Position } | { success: false; error: string }; + +export type BuildTxInput = { + userAddress: string; + marketId: string; + utxos: string[]; +}; + +export type BuildTxResult = + | { success: true; txRaw: string; txId: string; orderType: string } + | { success: true; waiting: true; orderType: string; message: string } + | { success: false; error: string }; + +export type ClosePositionInput = { + userAddress: string; + marketId: string; +}; + +export type ClosePositionResult = { success: true; position: Position } | { success: false; error: string }; + +export class PositionService { + constructor( + private readonly db: Kysely, + private readonly networkEnv: NetworkEnvironment, + private readonly cardanoscanProvider: CardanoscanProvider, + private readonly aggregatorProvider: MinswapAggregatorProvider, + ) {} + + async createPosition(input: CreatePositionInput): Promise { + const { userAddress, marketId, side, amountIn } = input; + + // Validate side + if (side !== "LONG" && side !== "SHORT") { + return { success: false, error: "Side must be LONG or SHORT" }; + } + + // Validate market + if (!isSupportedMarket(marketId)) { + return { success: false, error: `Market "${marketId}" is not supported or disabled` }; + } + + const marketConfig = getMarketConfig(marketId); + if (!marketConfig) { + return { success: false, error: `Market "${marketId}" configuration not found` }; + } + + // Validate minimum collateral + if (amountIn < marketConfig.minCollateral) { + return { + success: false, + error: `Minimum collateral is ${marketConfig.minCollateral} lovelace`, + }; + } + + // Check for existing open position in this market + const existingPosition = await PositionRepository.getOpenPositionByUserAndMarket(this.db, userAddress, marketId); + + if (existingPosition) { + return { + success: false, + error: "User already has an open position for this market", + }; + } + + // Calculate amount_borrow + let amountBorrow: bigint; + if (side === StateMachine.PositionSide.LONG) { + // LONG: borrow ADA = amountIn * (leverage - 1) + fee + amountBorrow = BigInt(Math.floor(Number(amountIn) * (marketConfig.longLeverage - 1))) + 4_000_000n; + } else { + // SHORT: borrow asset B equivalent to amountIn * shortLeverage ADA + // e.g. short 600 ADA with leverage 0.5 => estimate 300 ADA worth of asset B + const adaAmountToEstimate = BigInt(Math.floor(Number(amountIn) * marketConfig.shortLeverage)); + const estimate = await this.aggregatorProvider.estimate({ + amount: adaAmountToEstimate.toString(), + tokenIn: marketConfig.assetA.toBlockFrostString(), + tokenOut: marketConfig.assetB.toBlockFrostString(), + }); + amountBorrow = BigInt(estimate.amountOut); + } + // Execute transaction: create position + orders + const position = await this.db.transaction().execute(async (trx) => { + const pos = await PositionRepository.createPosition(trx, { + marketId, + userAddress, + side: side as StateMachine.PositionSide, + amountIn: amountIn.toString(), + amountBorrow: amountBorrow.toString(), + }); + + if (side === "LONG") { + // Create 4 LONG opening orders + await OrderRepository.createOrders(trx, [ + { + positionId: pos.id, + orderType: StateMachine.LongOrderType.LONG_BUY, + assetIn: marketConfig.assetA.toString(), + amountIn: pos.amountIn, + assetOut: marketConfig.assetB.toString(), + }, + { + positionId: pos.id, + orderType: StateMachine.LongOrderType.LONG_SUPPLY, + }, + { + positionId: pos.id, + orderType: StateMachine.LongOrderType.LONG_BORROW, + }, + { + positionId: pos.id, + orderType: StateMachine.LongOrderType.LONG_BUY_MORE, + }, + ]); + } else { + // Create 3 SHORT opening orders: supply ADA → borrow asset B → sell asset B + await OrderRepository.createOrders(trx, [ + { + positionId: pos.id, + orderType: StateMachine.ShortOrderType.SHORT_SUPPLY, + assetIn: marketConfig.assetA.toString(), + amountIn: pos.amountIn, + assetOut: marketConfig.assetAQTokenRaw, + }, + { + positionId: pos.id, + orderType: StateMachine.ShortOrderType.SHORT_BORROW, + }, + { + positionId: pos.id, + orderType: StateMachine.ShortOrderType.SHORT_SELL, + }, + ]); + } + + return pos; + }); + + return { success: true, position }; + } + + async buildTx(input: BuildTxInput): Promise { + const { userAddress, marketId, utxos } = input; + + // Validate market + if (!isSupportedMarket(marketId)) { + return { success: false, error: `Market "${marketId}" is not supported or disabled` }; + } + + // Check if user has an open position for this market + const position = await PositionRepository.getOpenPositionByUserAndMarket(this.db, userAddress, marketId); + + if (!position) { + return { success: false, error: "No open position found for this market" }; + } + + const marketConfig = getMarketConfig(marketId); + if (!marketConfig) { + return { success: false, error: `Market "${marketId}" configuration not found` }; + } + + try { + // STEP 1: Check if there's a waiting order (created_tx_id not null, waiting = true) + const waitingOrder = await OrderRepository.getWaitingOrder(this.db, position.id); + if (waitingOrder) { + logger.info("Found waiting order, checking status", { + orderId: waitingOrder.id, + orderType: waitingOrder.orderType, + createdTxId: waitingOrder.createdTxId, + }); + invariant(waitingOrder.assetOut, "Waiting order must have assetOut defined"); + invariant(waitingOrder.createdTxId, "Waiting order must have createdTxId defined"); + + // Get the waiting function for this order type + const waitingFn = StateMachine.MAP_WAITING_FN[waitingOrder.orderType]; + if (!waitingFn) { + return { + success: false, + error: `Waiting logic for order type "${waitingOrder.orderType}" is not implemented yet`, + }; + } + + // Build common waiting options + const waitingOptions: StateMachine.WaitingOptions = { + marketConfig, + txHash: waitingOrder.createdTxId, + userAddress: Address.fromBech32(userAddress), + cardanoscanProvider: this.cardanoscanProvider, + orderType: waitingOrder.orderType, + orderOutputIndex: waitingOrder.createdTxIndex ?? 0, + assetOut: Asset.fromString(waitingOrder.assetOut), + positionAmountIn: position.amountIn, + }; + + const waitingResult = await waitingFn(waitingOptions); + + if (waitingResult.isConfirmed) { + // Check if this is a transition state (has nextOrderType) + if ("nextOrderType" in waitingResult) { + const transitionResult = await OrderRepository.transitionToNextOrder(this.db, { + currentOrderId: waitingOrder.id, + positionId: waitingOrder.positionId, + nextOrderType: waitingResult.nextOrderType, + assetIn: waitingResult.assetIn, + amountIn: waitingResult.amountIn, + assetOut: waitingResult.assetOut, + amountOut: waitingResult.amountOut, + }); + + if (!transitionResult.success) { + logger.error("Failed to transition to next order", { error: transitionResult.error }); + return { success: false, error: transitionResult.error }; + } + + logger.info("Order completed, transitioned to next order", { + currentOrderId: waitingOrder.id, + currentOrderType: waitingOrder.orderType, + nextOrderId: transitionResult.nextOrder.id, + nextOrderType: waitingResult.nextOrderType, + }); + + return { + success: false, + error: `${waitingOrder.orderType} completed. ${waitingResult.nextOrderType} order ready. Call build-tx again to continue.`, + }; + } + + // This is the final state (no more orders to process) + await this.db.transaction().execute(async (trx) => { + // Update position status + await PositionRepository.updatePositionStatus(trx, position.id, waitingResult.positionStatus); + // Complete order: set amount_out and waiting = false + await OrderRepository.completeOrder(trx, waitingOrder.id, waitingResult.amountOut); + }); + + logger.info("Position completed, status updated to OPEN", { + positionId: position.id, + currentOrderId: waitingOrder.id, + currentOrderType: waitingOrder.orderType, + newStatus: waitingResult.positionStatus, + amountOut: waitingResult.amountOut, + }); + + return { + success: false, + error: `${waitingOrder.orderType} completed. Position is now ${waitingResult.positionStatus}.`, + }; + } else { + return { + success: false, + error: `${waitingOrder.orderType} transaction not yet confirmed on chain.`, + }; + } + } + + // STEP 2: No waiting order, find next unhandled order + const order = await OrderRepository.getNextUnhandledOrder(this.db, position.id); + if (!order) { + return { success: false, error: "No unhandled order found" }; + } + + // STEP 3: Handle order - check if transaction already built + if (order.builtTxId) { + logger.info("Order has built_tx_id, checking transaction status", { + orderId: order.id, + builtTxId: order.builtTxId, + hasCreatedTxId: !!order.createdTxId, + }); + + const address = Address.fromBech32(userAddress); + + // If order.createdTxId exists, transaction was already found on chain + if (order.createdTxId) { + logger.info("Transaction already confirmed on chain", { + orderId: order.id, + createdTxId: order.createdTxId, + }); + // Transaction is confirmed, waiting for it to be spent + return { + success: false, + error: "Transaction confirmed on chain. Waiting for order to be processed.", + }; + } + + // Search for transaction on chain + const txFoundOnChain = await this.cardanoscanProvider.findTransactionByHash( + address, + order.builtTxId, + 50, // pageSize + 10, // maxPage - search up to 10 pages (500 transactions) + ); + + if (txFoundOnChain) { + logger.info("Transaction found on chain", { + orderId: order.id, + txHash: txFoundOnChain.hash, + }); + + // Update order with created_tx_id (this will set waiting = true by default) + await OrderRepository.updateOrderCreatedTx(this.db, order.id, txFoundOnChain.hash, 0); + + return { + success: false, + error: "Transaction confirmed on chain. Waiting for order to be processed.", + }; + } + + // Transaction not found on chain - check if expired + const now = new Date(); + const validTo = order.builtValidTo; + + if (!validTo) { + logger.warn("Order has built_tx_id but no built_valid_to, rebuilding", { + orderId: order.id, + }); + // Fall through to rebuild + } else if (validTo < now) { + // Transaction expired => rebuild + logger.info("Transaction expired, rebuilding", { + orderId: order.id, + validTo: validTo.toISOString(), + now: now.toISOString(), + }); + // Fall through to rebuild + } else { + // Transaction not expired yet => wait + const remainingMs = validTo.getTime() - now.getTime(); + const remainingMinutes = Math.ceil(remainingMs / 1000 / 60); + logger.info("Transaction not yet expired, waiting", { + orderId: order.id, + validTo: validTo.toISOString(), + remainingMinutes, + }); + return { + success: true, + waiting: true, + orderType: order.orderType, + message: `Transaction already built and waiting for confirmation. Expires in ${remainingMinutes} minutes.`, + }; + } + } + + // STEP 4: Build new transaction + logger.info("Building new transaction", { + orderId: order.id, + orderType: order.orderType, + hasPreviousBuild: !!order.builtTxId, + }); + + // Get the build function for this order type + const buildFn = StateMachine.MAP_BUILD_TX_FN[order.orderType]; + if (!buildFn) { + return { success: false, error: `Order type "${order.orderType}" is not implemented` }; + } + + // Build common options + const buildOptions: StateMachine.HandleBuildTxOptions = { + order: { + orderType: order.orderType, + assetIn: order.assetIn, + amountIn: order.amountIn, + assetOut: order.assetOut, + }, + marketConfig, + userAddress, + networkEnv: this.networkEnv, + utxos, + amountBorrow: position.amountBorrow, + }; + + // For LONG_REPAY, we need the loan transaction ID, output index, and collateral amount from LONG_BORROW + if (order.orderType === StateMachine.LongOrderType.LONG_REPAY) { + const borrowOrder = await OrderRepository.getOrderByPositionAndType( + this.db, + position.id, + StateMachine.LongOrderType.LONG_BORROW, + ); + if (!borrowOrder?.createdTxId) { + return { success: false, error: "LONG_BORROW order not found or not confirmed yet" }; + } + if (!borrowOrder.amountIn) { + return { success: false, error: "LONG_BORROW order amountIn (collateral amount) not set" }; + } + buildOptions.loanTxId = borrowOrder.createdTxId; + buildOptions.loanOutputIndex = borrowOrder.createdTxIndex ?? 0; + buildOptions.collateralAmount = borrowOrder.amountIn; // qToken amount used as collateral + } + + // For LONG_WITHDRAW, we need the amountOut from LONG_SUPPLY order + if (order.orderType === StateMachine.LongOrderType.LONG_WITHDRAW) { + const supplyOrder = await OrderRepository.getOrderByPositionAndType( + this.db, + position.id, + StateMachine.LongOrderType.LONG_SUPPLY, + ); + if (!supplyOrder?.amountOut) { + return { success: false, error: "LONG_SUPPLY order not found or amountOut not set" }; + } + buildOptions.supplyAmountOut = supplyOrder.amountOut; + } + + // For SHORT_REPAY, we need the loan transaction ID, output index, and collateral amount from SHORT_BORROW + if (order.orderType === StateMachine.ShortOrderType.SHORT_REPAY) { + const borrowOrder = await OrderRepository.getOrderByPositionAndType( + this.db, + position.id, + StateMachine.ShortOrderType.SHORT_BORROW, + ); + if (!borrowOrder?.createdTxId) { + return { success: false, error: "SHORT_BORROW order not found or not confirmed yet" }; + } + if (!borrowOrder.amountIn) { + return { success: false, error: "SHORT_BORROW order amountIn (collateral amount) not set" }; + } + buildOptions.loanTxId = borrowOrder.createdTxId; + buildOptions.loanOutputIndex = borrowOrder.createdTxIndex ?? 0; + buildOptions.collateralAmount = borrowOrder.amountIn; // qADA amount used as collateral + } + + // For SHORT_WITHDRAW, we need the amountIn from SHORT_SUPPLY order (original ADA supplied, not qADA) + if (order.orderType === StateMachine.ShortOrderType.SHORT_WITHDRAW) { + const supplyOrder = await OrderRepository.getOrderByPositionAndType( + this.db, + position.id, + StateMachine.ShortOrderType.SHORT_SUPPLY, + ); + if (!supplyOrder?.amountIn) { + return { success: false, error: "SHORT_SUPPLY order not found or amountIn not set" }; + } + buildOptions.supplyAmountOut = supplyOrder.amountIn; + } + + const txResult = await buildFn(buildOptions); + + // Update order built_tx fields + await OrderRepository.updateOrderBuiltTx( + this.db, + order.id, + txResult.txId, + new Date(txResult.validTo), + ); + + logger.info("Transaction built successfully", { + orderId: order.id, + txId: txResult.txId, + validTo: new Date(txResult.validTo).toISOString(), + }); + + return { success: true, txRaw: txResult.txRaw, txId: txResult.txId, orderType: order.orderType }; + } catch (error) { + logger.error("error building tx", error); + return { + success: false, + error: error instanceof Error ? error.message : "Failed to build transaction", + }; + } + } + + async getOpenPositionByUser(userAddress: string): Promise { + return PositionRepository.getOpenPositionByUser(this.db, userAddress); + } + + async closePosition(input: ClosePositionInput): Promise { + const { userAddress, marketId } = input; + + // Validate market + if (!isSupportedMarket(marketId)) { + return { success: false, error: `Market "${marketId}" is not supported or disabled` }; + } + + const marketConfig = getMarketConfig(marketId); + if (!marketConfig) { + return { success: false, error: `Market "${marketId}" configuration not found` }; + } + + // Check if user has an open position for this market + const position = await PositionRepository.getOpenPositionByUserAndMarket(this.db, userAddress, marketId); + + if (!position) { + return { success: false, error: "No open position found for this market" }; + } + + // Check if position is OPEN (only OPEN positions can be closed) + if (position.status !== StateMachine.PositionStatus.OPEN) { + return { + success: false, + error: `Position is in "${position.status}" status. Only OPEN positions can be closed.`, + }; + } + + // Execute transaction: update position status + create closing orders + const updatedPosition = await this.db.transaction().execute(async (trx) => { + // Update position status to CLOSING + await PositionRepository.updatePositionStatus(trx, position.id, StateMachine.PositionStatus.CLOSING); + + if (position.side === StateMachine.PositionSide.LONG) { + // Get the LONG_BUY_MORE order to get the amountOut (total asset B received) + const longBuyMoreOrder = await OrderRepository.getOrderByPositionAndType( + this.db, + position.id, + StateMachine.LongOrderType.LONG_BUY_MORE, + ); + if (!longBuyMoreOrder || !longBuyMoreOrder.amountOut) { + throw new Error("LONG_BUY_MORE order not found or amountOut not set"); + } + + // Create 4 LONG closing orders + await OrderRepository.createOrders(trx, [ + { + positionId: position.id, + orderType: StateMachine.LongOrderType.LONG_SELL, + assetIn: marketConfig.assetB.toString(), + amountIn: longBuyMoreOrder.amountOut, + assetOut: marketConfig.assetA.toString(), + }, + { + positionId: position.id, + orderType: StateMachine.LongOrderType.LONG_REPAY, + }, + { + positionId: position.id, + orderType: StateMachine.LongOrderType.LONG_WITHDRAW, + }, + { + positionId: position.id, + orderType: StateMachine.LongOrderType.LONG_SELL_ALL, + }, + ]); + } else { + // Get the SHORT_SELL order to get the amountOut (ADA received from selling) + const shortSellOrder = await OrderRepository.getOrderByPositionAndType( + this.db, + position.id, + StateMachine.ShortOrderType.SHORT_SELL, + ); + if (!shortSellOrder || !shortSellOrder.amountOut) { + throw new Error("SHORT_SELL order not found or amountOut not set"); + } + + // Create 3 SHORT closing orders: buy asset B → repay loan → withdraw ADA + await OrderRepository.createOrders(trx, [ + { + positionId: position.id, + orderType: StateMachine.ShortOrderType.SHORT_BUY, + assetIn: marketConfig.assetA.toString(), + amountIn: shortSellOrder.amountOut, + assetOut: marketConfig.assetB.toString(), + }, + { + positionId: position.id, + orderType: StateMachine.ShortOrderType.SHORT_REPAY, + }, + { + positionId: position.id, + orderType: StateMachine.ShortOrderType.SHORT_WITHDRAW, + }, + ]); + } + + // Return updated position + return { + ...position, + status: StateMachine.PositionStatus.CLOSING, + }; + }); + + logger.info("Position close initiated", { + positionId: position.id, + userAddress, + marketId, + previousStatus: position.status, + newStatus: StateMachine.PositionStatus.CLOSING, + }); + + return { success: true, position: updatedPosition }; + } +} diff --git a/apps/long-short-backend/src/utils/common.ts b/apps/long-short-backend/src/utils/common.ts new file mode 100644 index 0000000..30423bd --- /dev/null +++ b/apps/long-short-backend/src/utils/common.ts @@ -0,0 +1,60 @@ +import type * as OgmiosSchema from "@cardano-ogmios/schema"; +import { parseIntSafe } from "@minswap/felis-ledger-utils"; +import invariant from "@minswap/tiny-invariant"; +import { logger } from "./logger"; + +export enum EnvKey { + DATABASE_URL = "DATABASE_URL", + OGMIOS_HOST = "OGMIOS_HOST", + REDIS_URL = "REDIS_URL", + SYNCER_START_POINT = "SYNCER_START_POINT", +} + +export function parseHostPort(env: string): { host: string; port: number } { + const parts = env.split(":"); + invariant(parts.length === 2, `expect format host:port, get ${env}`); + return { + host: parts[0], + port: parseIntSafe(parts[1]), + }; +} + +export function getEnvOr(key: string, defaultValue: string): string { + const val = process.env[key]; + if (!val) { + return defaultValue; + } + return val; +} + +export function getEnv(key: string): string { + const val = process.env[key]; + if (!val) { + logger.error(`Require environment variable ${key}`); + throw new Error("Server init error"); + } + return val; +} + +export function getEnvOptional(key: string): string | undefined { + return process.env[key]; +} + +export function parsePointOrOrigin(env: string): OgmiosSchema.PointOrOrigin { + if (env === "origin") { + return env; + } + return parsePoint(env); +} + +export function parsePoint(env: string): OgmiosSchema.Point { + try { + const parts = env.split("."); + return { + id: parts[0], + slot: parseIntSafe(parts[1]), + }; + } catch (_err) { + throw new Error(`fail to parse point, expect format $headerHash.$slot, got ${env}`); + } +} diff --git a/apps/long-short-backend/src/utils/expiring-variable.ts b/apps/long-short-backend/src/utils/expiring-variable.ts new file mode 100644 index 0000000..f6a178c --- /dev/null +++ b/apps/long-short-backend/src/utils/expiring-variable.ts @@ -0,0 +1,22 @@ +import type { Duration } from "@minswap/felis-ledger-utils"; + +type TimeoutType = ReturnType; + +export class ExpiringVariable { + private _value: T | null = null; + private expirationTimer?: TimeoutType; + + set(newVal: T, ttl: Duration): void { + this._value = newVal; + if (this.expirationTimer) { + clearTimeout(this.expirationTimer); + } + this.expirationTimer = setTimeout(() => { + this._value = null; + }, ttl.milliseconds); + } + + get value(): T | null { + return this._value; + } +} diff --git a/apps/long-short-backend/src/utils/hash.ts b/apps/long-short-backend/src/utils/hash.ts new file mode 100644 index 0000000..ceec7fd --- /dev/null +++ b/apps/long-short-backend/src/utils/hash.ts @@ -0,0 +1,7 @@ +import CryptoJS from "crypto-js"; + +export namespace HashUtils { + export function sha256(data: string): string { + return CryptoJS.SHA256(data).toString(); + } +} diff --git a/apps/long-short-backend/src/utils/index.ts b/apps/long-short-backend/src/utils/index.ts new file mode 100644 index 0000000..0b61367 --- /dev/null +++ b/apps/long-short-backend/src/utils/index.ts @@ -0,0 +1,6 @@ +export * from "./common"; +export * from "./expiring-variable"; +export * from "./hash"; +export * from "./lodash"; +export * from "./logger"; +export * from "./signature"; diff --git a/apps/long-short-backend/src/utils/lodash.ts b/apps/long-short-backend/src/utils/lodash.ts new file mode 100644 index 0000000..29355c5 --- /dev/null +++ b/apps/long-short-backend/src/utils/lodash.ts @@ -0,0 +1,36 @@ +export function uniq(arr: T[]): T[] { + const seen = new Set(); + const result: T[] = []; + + for (const item of arr) { + if (!seen.has(item)) { + seen.add(item); + result.push(item); + } + } + + return result; +} + +export function uniqBy(arr: T[], keySelector: ((item: T) => Key) | keyof T): T[] { + const seen = new Set(); + const result: T[] = []; + + let selector: (item: T) => Key; + if (typeof keySelector === "function") { + selector = keySelector; + } else { + selector = (v: T): Key => v[keySelector] as Key; + } + + for (const item of arr) { + const key = selector(item); + + if (!seen.has(key)) { + seen.add(key); + result.push(item); + } + } + + return result; +} diff --git a/apps/long-short-backend/src/utils/logger.ts b/apps/long-short-backend/src/utils/logger.ts new file mode 100644 index 0000000..67c6fbd --- /dev/null +++ b/apps/long-short-backend/src/utils/logger.ts @@ -0,0 +1,139 @@ +import util from "node:util"; + +enum LogLevel { + ERROR = "ERROR", + WARN = "WARN", + INFO = "INFO", +} + +// biome-ignore lint/suspicious/noExplicitAny: skip +type LogFunc = (message: string, extra?: Record | unknown) => void; + +type Logger = { + error: LogFunc; + warn: LogFunc; + info: LogFunc; + // biome-ignore lint/suspicious/noExplicitAny: skip + prettyJson: (...message: any) => void; +}; + +// Example: 2023-10-18 16:01:45.012 +function formatDateDevEnv(d: Date): string { + function pad0s(x: number, length = 2): string { + let s = x.toString(); + while (s.length < length) { + s = `0${s}`; + } + return s; + } + + return `${d.getFullYear()}-${pad0s(d.getMonth() + 1)}-${pad0s(d.getDate())} ${pad0s(d.getHours())}:${pad0s( + d.getMinutes(), + )}:${pad0s(d.getSeconds())}.${pad0s(d.getMilliseconds(), 3)}`; +} + +function formatLevelDevEnv(lvl: LogLevel): string { + const padLevel = lvl.padEnd(8); + let coloredLevel: string; + switch (lvl) { + case LogLevel.ERROR: + coloredLevel = `\x1b[31m${padLevel}\x1b[0m`; + break; + case LogLevel.WARN: + coloredLevel = `\x1b[33m${padLevel}\x1b[0m`; + break; + case LogLevel.INFO: + // default color + coloredLevel = padLevel; + break; + } + return coloredLevel; +} + +// biome-ignore lint/suspicious/noExplicitAny: skip +function formatLabelsDevEnv(labels: Record): string { + return Object.entries(labels) + .map(([k, v]) => ` ${k}=${v}`) + .join("\n"); +} + +// biome-ignore lint/suspicious/noExplicitAny: skip +export function shouldUseUtilInspectOnValue(val: any): boolean { + if (val instanceof Error) { + return true; + } + if (typeof val === "object" && val !== null && typeof val.toString === "function") { + const str = val.toString(); + if (typeof str === "string" && str.startsWith("[object")) { + return true; + } + } + return false; +} + +// biome-ignore lint/suspicious/noExplicitAny: skip +function log(level: LogLevel, _message: string, extra?: Record | unknown): void { + // build labels object + // biome-ignore lint/suspicious/noExplicitAny: skip + const labels: Record = { + time: new Date().toISOString(), + level, + message: _message.trim(), + }; + if (extra instanceof Error) { + labels.error = extra; + } else if (typeof extra === "object" && extra !== null) { + Object.assign(labels, extra); + } else if (extra !== null && extra !== undefined) { + labels.extra = extra; + } + + for (const key in labels) { + if (shouldUseUtilInspectOnValue(labels[key])) { + labels[key] = util.inspect(labels[key], { + colors: true, + numericSeparator: true, + }); + } + } + + // format log line + let str: string; + const printedLabels = formatLabelsDevEnv(labels); + const separator = labels.message && printedLabels ? "\n" : ""; + str = `${formatDateDevEnv(new Date())} ${formatLevelDevEnv(level)} ${labels.message}${separator}${printedLabels}`; + + switch (level) { + case LogLevel.ERROR: + case LogLevel.WARN: + // write to stderr + console.error(str); + break; + case LogLevel.INFO: + // write to stdout + console.info(str); + break; + } +} + +export const logger: Logger = { + error: (message, extra) => log(LogLevel.ERROR, message, extra), + warn: (message, extra) => log(LogLevel.WARN, message, extra), + info: (message, extra) => log(LogLevel.INFO, message, extra), + prettyJson(...message) { + for (const msg of message) { + if (typeof msg === "object" && msg !== null) { + const jsonObj = JSON.parse(JSON.stringify(msg)); + console.log( + util.inspect(jsonObj, { + colors: true, + numericSeparator: true, + depth: Number.POSITIVE_INFINITY, + }), + ); + } else { + console.log(msg); + } + } + }, +}; diff --git a/apps/long-short-backend/src/utils/signature.ts b/apps/long-short-backend/src/utils/signature.ts new file mode 100644 index 0000000..3eb18d3 --- /dev/null +++ b/apps/long-short-backend/src/utils/signature.ts @@ -0,0 +1,118 @@ +import * as CMS from "@emurgo/cardano-message-signing-nodejs"; +import { Address, type PrivateKey } from "@minswap/felis-ledger-core"; +import { RustModule } from "@minswap/felis-ledger-utils"; + +export type VerifySignDataOptions = { + message: string; // The original message that was signed (hex encoded) + address: string; // User's Cardano address (bech32) + key: string; // CBOR hex of COSEKey + signature: string; // CBOR hex of COSESign1 +}; + +/** + * Verify a CIP-8/CIP-30 message signature + * + * Based on: + * - https://github.com/Emurgo/message-signing/blob/master/examples/rust/src/main.rs + * - https://github.com/input-output-hk/nami/blob/main/MessageSigning.md + * + * @returns true if signature is valid + */ +export function verifySignData(options: VerifySignDataOptions): boolean { + const { message, address, key, signature } = options; + + try { + const CSL = RustModule.getE; + + // 1. Parse the COSESign1 message + const coseSign1 = CMS.COSESign1.from_bytes(Buffer.from(signature, "hex")); + + // 2. Parse the COSEKey and extract public key using label -2 (COSE key identifier for EC2 x-coordinate) + const coseKey = CMS.COSEKey.from_bytes(Buffer.from(key, "hex")); + const pubKeyBytes = coseKey.header(CMS.Label.new_int(CMS.Int.new_negative(CMS.BigNum.from_str("2"))))?.as_bytes(); + + if (!pubKeyBytes) { + return false; + } + + const publicKey = CSL.PublicKey.from_bytes(pubKeyBytes); + + // 3. Verify the payload matches the expected message + const payload = coseSign1.payload(); + if (!payload) { + return false; + } + + // Compare payload with expected message + const payloadHex = Buffer.from(payload).toString("hex"); + if (payloadHex !== message) { + return false; + } + + // 4. Verify the signature + // Get the SigStructure bytes that were originally signed + const signedData = coseSign1.signed_data(undefined, undefined).to_bytes(); + const sig = CSL.Ed25519Signature.from_bytes(coseSign1.signature()); + + const isValidSignature = publicKey.verify(signedData, sig); + if (!isValidSignature) { + return false; + } + + // 5. Verify the public key matches the address + const keyHash = publicKey.hash(); + const addressObj = Address.fromBech32(address); + const addressPubKeyHash = addressObj.toPubKeyHash(); + + if (!addressPubKeyHash) { + return false; + } + + if (keyHash.to_hex() !== addressPubKeyHash.keyHash.hex) { + return false; + } + + return true; + } catch { + return false; + } +} + +export function signData(privateKey: PrivateKey, address: string, payload: string): { signature: string; key: string } { + const cslPrivateKey = privateKey.toECSL(); + const cslPublicKey = cslPrivateKey.to_public(); + + // Build protected header with address + const protectedHeaders = CMS.HeaderMap.new(); + protectedHeaders.set_algorithm_id(CMS.Label.from_algorithm_id(CMS.AlgorithmId.EdDSA)); + protectedHeaders.set_header(CMS.Label.new_text("address"), CMS.CBORValue.new_bytes(Buffer.from(address, "hex"))); + + const protectedSerialized = CMS.ProtectedHeaderMap.new(protectedHeaders); + const unprotectedHeaders = CMS.HeaderMap.new(); + const headers = CMS.Headers.new(protectedSerialized, unprotectedHeaders); + + // Build COSESign1 + const builder = CMS.COSESign1Builder.new(headers, Buffer.from(payload, "hex"), false); + const toSign = builder.make_data_to_sign().to_bytes(); + + // Sign with Ed25519 + const signedSigStructure = cslPrivateKey.sign(toSign).to_bytes(); + const coseSign1 = builder.build(signedSigStructure); + + // Build COSEKey with public key at label -2 + const coseKey = CMS.COSEKey.new(CMS.Label.from_key_type(CMS.KeyType.OKP)); + coseKey.set_algorithm_id(CMS.Label.from_algorithm_id(CMS.AlgorithmId.EdDSA)); + coseKey.set_header( + CMS.Label.new_int(CMS.Int.new_negative(CMS.BigNum.from_str("1"))), // crv = -1 + CMS.CBORValue.new_int(CMS.Int.new_i32(6)), // Ed25519 = 6 + ); + coseKey.set_header( + CMS.Label.new_int(CMS.Int.new_negative(CMS.BigNum.from_str("2"))), // x = -2 (public key) + CMS.CBORValue.new_bytes(cslPublicKey.as_bytes()), + ); + + return { + signature: Buffer.from(coseSign1.to_bytes()).toString("hex"), + key: Buffer.from(coseKey.to_bytes()).toString("hex"), + }; +} diff --git a/apps/long-short-backend/test/sign-data.test.ts b/apps/long-short-backend/test/sign-data.test.ts new file mode 100644 index 0000000..958614e --- /dev/null +++ b/apps/long-short-backend/test/sign-data.test.ts @@ -0,0 +1,271 @@ +import { describe, it, expect, beforeAll } from "vitest"; +import { RustModule } from "@minswap/felis-ledger-utils"; +import { baseAddressWalletFromSeed } from "@minswap/felis-cip"; +import { + Address, + NetworkEnvironment, + PrivateKey, + XJSON, +} from "@minswap/felis-ledger-core"; +import * as CMS from "@emurgo/cardano-message-signing-nodejs"; +import { verifySignData } from "../src/utils/signature"; +import CryptoJS from "crypto-js"; +import { OrderV2Datum } from "@minswap/felis-dex-v2"; + +function getSignMessage(address: string): string { + return Buffer.from(address).toString("hex"); +} + +/** + * Sign data using CIP-8/CIP-30 pattern + * Returns { signature, key } where: + * - signature: CBOR hex of COSESign1 + * - key: CBOR hex of COSEKey + */ +function signData( + privateKey: PrivateKey, + address: string, + payload: string, +): { signature: string; key: string } { + const cslPrivateKey = privateKey.toECSL(); + const cslPublicKey = cslPrivateKey.to_public(); + + // Build protected header with address + const protectedHeaders = CMS.HeaderMap.new(); + protectedHeaders.set_algorithm_id( + CMS.Label.from_algorithm_id(CMS.AlgorithmId.EdDSA), + ); + protectedHeaders.set_header( + CMS.Label.new_text("address"), + CMS.CBORValue.new_bytes(Buffer.from(address, "hex")), + ); + + const protectedSerialized = CMS.ProtectedHeaderMap.new(protectedHeaders); + const unprotectedHeaders = CMS.HeaderMap.new(); + const headers = CMS.Headers.new(protectedSerialized, unprotectedHeaders); + + // Build COSESign1 + const builder = CMS.COSESign1Builder.new( + headers, + Buffer.from(payload, "hex"), + false, + ); + const toSign = builder.make_data_to_sign().to_bytes(); + + // Sign with Ed25519 + const signedSigStructure = cslPrivateKey.sign(toSign).to_bytes(); + const coseSign1 = builder.build(signedSigStructure); + + // Build COSEKey with public key at label -2 + const coseKey = CMS.COSEKey.new(CMS.Label.from_key_type(CMS.KeyType.OKP)); + coseKey.set_algorithm_id(CMS.Label.from_algorithm_id(CMS.AlgorithmId.EdDSA)); + coseKey.set_header( + CMS.Label.new_int(CMS.Int.new_negative(CMS.BigNum.from_str("1"))), // crv = -1 + CMS.CBORValue.new_int(CMS.Int.new_i32(6)), // Ed25519 = 6 + ); + coseKey.set_header( + CMS.Label.new_int(CMS.Int.new_negative(CMS.BigNum.from_str("2"))), // x = -2 (public key) + CMS.CBORValue.new_bytes(cslPublicKey.as_bytes()), + ); + + return { + signature: Buffer.from(coseSign1.to_bytes()).toString("hex"), + key: Buffer.from(coseKey.to_bytes()).toString("hex"), + }; +} + +describe("verifySignData", () => { + beforeAll(async () => { + await RustModule.load(); + }); + + it("should verify a valid signature", () => { + const seed = + "melt enemy surface feed kiss helmet suffer demise toilet insane human refuse park insect lawsuit custom inch spirit throw radio alarm creek chat symptom"; + const wallet = baseAddressWalletFromSeed( + seed, + NetworkEnvironment.TESTNET_PREVIEW, + ); + + const data = { + market: "ADA-MIN", + side: "LONG", + amount: 500000000, + }; + const message = Buffer.from(JSON.stringify(data)).toString("hex"); + + // Sign the message + const { signature, key } = signData( + wallet.paymentKey, + wallet.address.bech32, + message, + ); + // Verify + const isValid = verifySignData({ + message, + address: wallet.address.bech32, + key, + signature, + }); + + expect(isValid).toBe(true); + }); + + it("should reject invalid signature", () => { + const seed = + "melt enemy surface feed kiss helmet suffer demise toilet insane human refuse park insect lawsuit custom inch spirit throw radio alarm creek chat symptom"; + const wallet = baseAddressWalletFromSeed( + seed, + NetworkEnvironment.TESTNET_PREVIEW, + ); + + const message = getSignMessage(wallet.address.bech32); + const { signature, key } = signData( + wallet.paymentKey, + wallet.address.bech32, + message, + ); + + // Tamper with signature + const tamperedSignature = signature.slice(0, -4) + "0000"; + + const isValid = verifySignData({ + message, + address: wallet.address.bech32, + key, + signature: tamperedSignature, + }); + + expect(isValid).toBe(false); + }); + + it("should reject wrong address", () => { + const seed = + "melt enemy surface feed kiss helmet suffer demise toilet insane human refuse park insect lawsuit custom inch spirit throw radio alarm creek chat symptom"; + const wallet = baseAddressWalletFromSeed( + seed, + NetworkEnvironment.TESTNET_PREVIEW, + ); + + // Use a different seed to get different address + const otherSeed = + "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"; + const otherWallet = baseAddressWalletFromSeed( + otherSeed, + NetworkEnvironment.TESTNET_PREVIEW, + ); + + const message = getSignMessage(wallet.address.bech32); + const { signature, key } = signData( + wallet.paymentKey, + wallet.address.bech32, + message, + ); + + // Verify with different address should fail + const isValid = verifySignData({ + message, + address: otherWallet.address.bech32, + key, + signature, + }); + + expect(isValid).toBe(false); + }); + + it("should reject wrong message", () => { + const seed = + "melt enemy surface feed kiss helmet suffer demise toilet insane human refuse park insect lawsuit custom inch spirit throw radio alarm creek chat symptom"; + const wallet = baseAddressWalletFromSeed( + seed, + NetworkEnvironment.TESTNET_PREVIEW, + ); + + const message = getSignMessage(wallet.address.bech32); + const { signature, key } = signData( + wallet.paymentKey, + wallet.address.bech32, + message, + ); + + // Verify with different message should fail + const wrongMessage = Buffer.from("wrong message").toString("hex"); + const isValid = verifySignData({ + message: wrongMessage, + address: wallet.address.bech32, + key, + signature, + }); + + expect(isValid).toBe(false); + }); + + it("should work with authen_token format (signature:key)", () => { + const seed = + "melt enemy surface feed kiss helmet suffer demise toilet insane human refuse park insect lawsuit custom inch spirit throw radio alarm creek chat symptom"; + const wallet = baseAddressWalletFromSeed( + seed, + NetworkEnvironment.TESTNET_PREVIEW, + ); + + const message = getSignMessage(wallet.address.bech32); + const { signature, key } = signData( + wallet.paymentKey, + wallet.address.bech32, + message, + ); + + // Create authen_token in the format used by the API + const authenToken = `${signature}:${key}`; + const [sig, k] = authenToken.split(":"); + + const isValid = verifySignData({ + message, + address: wallet.address.bech32, + key: k, + signature: sig, + }); + + expect(isValid).toBe(true); + }); + + it("wallet CIP-30 test", () => { + const message = `Hello! How are you?`; + const signedData = { + key: "a401010327200621582050ac53f148ce5ce4d4278db4d6c4187265da319984920c2b9e5e4029b5b7b1bb", + signature: + "845846a2012767616464726573735839006f7f6cf2c50c559594c0bf8aecd22e7c6bc5df47c4ed7aa8ef87cb49ad7ffe3cda0cd3175a52bff1e5066a67785c47f3a786b434bdc998eea166686173686564f45348656c6c6f2120486f772061726520796f753f584093f6be1a792f5c1767d9e20ba767bf3787c8d6bb5e34d2b48130288e81f759270f9bf88acc5dd48feb7e8eadec98218ca33ff419cc3e2f65086ca99a4b8f510f", + }; + const isValid = verifySignData({ + message: Buffer.from(message).toString("hex"), + address: + "addr_test1qphh7m8jc5x9t9v5czlc4mxj9e7xh3wlglzw674ga7rukjdd0llreksv6vt4554l78jsv6n80pwy0ua8s66rf0wfnrhq73a20h", + key: signedData.key, + signature: signedData.signature, + }); + expect(isValid).toBe(true); + }); + + it.skip("check hash", () => { + const message = `Hello! How are you?`; + const hash = CryptoJS.SHA256(message).toString(); + console.log(hash); + }); + + it.skip("parse order v2 datum", () => { + const oderV2Datum = XJSON.parse( + `{"author":{"canceller":{"type":0,"hash":{"$bytes":"fc56f17f54b4b9c0da5a18b10c9acb7bd6f10b66e242fb4d4717859e"}},"refundReceiver":{"$address":"addr1q879dutl2j6tnsx6tgvtzry6edaadugtvm3y976dgutct8hjzl6rta0nfkaxnqcdntdqzw6u9y9yamnq0qm3etj49x9sgrzg6p"},"refundReceiverDatum":{"type":0},"successReceiver":{"$address":"addr1q879dutl2j6tnsx6tgvtzry6edaadugtvm3y976dgutct8hjzl6rta0nfkaxnqcdntdqzw6u9y9yamnq0qm3etj49x9sgrzg6p"},"successReceiverDatum":{"type":0}},"step":{"type":0,"direction":1,"swapAmountOption":{"type":0,"swapAmount":{"$bigint":"100000000"}},"minimumReceived":{"$bigint":"1"},"killable":0},"lpAsset":{"$asset":"f5808c2c990d86da54bfc97d89cee6efa20cd8461616359478d96b4c.e74c52975908a612d5ce68327040d449aae99f8b463bb6de046a1b23c5713169"},"maxBatcherFee":{"$bigint":"700000"}}`, + ) as OrderV2Datum; + console.log(oderV2Datum); + const raw = OrderV2Datum.toDataHex(oderV2Datum); + console.log(raw); + console.log(OrderV2Datum.toPlutusJson(oderV2Datum )); + }); + + it("address to hex", () => { + const addr = Address.fromBech32("addr_test1qphh7m8jc5x9t9v5czlc4mxj9e7xh3wlglzw674ga7rukjdd0llreksv6vt4554l78jsv6n80pwy0ua8s66rf0wfnrhq73a20h"); + const ECSL = RustModule.getE; + const ea = ECSL.Address.from_bech32(addr.bech32); + const a1 = ea.to_hex(); + }); +}); diff --git a/apps/long-short-backend/tsconfig.json b/apps/long-short-backend/tsconfig.json new file mode 100644 index 0000000..d640c41 --- /dev/null +++ b/apps/long-short-backend/tsconfig.json @@ -0,0 +1,28 @@ +{ + "ts-node": { + "transpileOnly": true, + "experimentalSpecifierResolution": "node" + }, + "compilerOptions": { + "outDir": "./dist", + "strict": true, + "forceConsistentCasingInFileNames": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noFallthroughCasesInSwitch": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "target": "ES2022", + "lib": ["ES2022", "DOM"], + "module": "ES2022", + "moduleResolution": "bundler", + "declaration": true, + "sourceMap": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "test", "dist", ".config"] +} diff --git a/apps/long-short-backend/vitest.config.mts b/apps/long-short-backend/vitest.config.mts new file mode 100644 index 0000000..c9906de --- /dev/null +++ b/apps/long-short-backend/vitest.config.mts @@ -0,0 +1,22 @@ +// vitest.config.mts +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + globals: true, + environment: "node", // use node env for vitest + include: ["test/**/*.{test,spec}.{ts,tsx}"], + exclude: ["node_modules", "dist"], + }, + define: { + // Force Vitest to think there's no window + "typeof window": '"undefined"', + }, + resolve: { + alias: { + "@minswap/cardano-serialization-lib-browser": "@minswap/cardano-serialization-lib-nodejs", + "@emurgo/cardano-serialization-lib-browser": "@emurgo/cardano-serialization-lib-nodejs", + "@repo/uplc-web": "@repo/uplc-node", + }, + }, +}); diff --git a/apps/web/app/components/nitro-wallet-connector.tsx b/apps/web/app/components/nitro-wallet-connector.tsx index 06440bf..a52a730 100644 --- a/apps/web/app/components/nitro-wallet-connector.tsx +++ b/apps/web/app/components/nitro-wallet-connector.tsx @@ -1,12 +1,12 @@ "use client"; import { CopyOutlined, LogoutOutlined } from "@ant-design/icons"; +import { Address } from "@minswap/felis-ledger-core"; +import { NitroWallet } from "@minswap/felis-lending-market"; import invariant from "@minswap/tiny-invariant"; import { Alert, App, Button, Card, Col, Row, Space, Statistic, Tooltip } from "antd"; import { useAtom, useAtomValue, useSetAtom } from "jotai"; import { useCallback, useEffect, useState } from "react"; -import { Address } from "../../../../packages/ledger-core/dist/address"; -import { NitroWallet } from "../../../../packages/minswap-lending-market/dist/nitro-wallet"; import { type NitroWalletData, nitroBalanceAtom, diff --git a/apps/web/package.json b/apps/web/package.json index c9e2d60..4043ced 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -4,7 +4,7 @@ "type": "module", "private": true, "scripts": { - "dev": "next dev --turbopack --port 3001", + "dev": "next dev --turbopack --port 3002", "build": "next build", "start": "next start", "lint": "next lint --max-warnings 0", diff --git a/biome.json b/biome.json index 7c52aaf..5b43dfe 100644 --- a/biome.json +++ b/biome.json @@ -15,7 +15,8 @@ "packages/minswap-dex-v2/**", "packages/minswap-lending-market/**", "packages/tx-builder/**", - "apps/web/**" + "apps/web/**", + "apps/long-short-backend/src/**" ], "experimentalScannerIgnores": ["**/uplc/**"] }, diff --git a/docker-compose.yml b/docker-compose.yml index 199df26..c609bc9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,18 +1,83 @@ -version: "3.8" +x-base-backend: &base-backend + build: + context: . + dockerfile: Dockerfile.backend + extra_hosts: + - "host.docker.internal:host-gateway" + restart: always + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + environment: + DATABASE_URL: postgres://postgres:JBNGlQ9wNFLlYWc2mG@postgres:5432/margin + REDIS_URL: redis://default:7obaQyYSDDLDk3ECYA@redis:6379 + API_PORT: 9999 + API_HOST: 0.0.0.0 + NETWORK: testnet-preview + CARDANOSCAN_API_KEY: ${CARDANOSCAN_API_KEY} + healthcheck: + test: ["CMD-SHELL", "curl -f http://localhost:9999/health || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s services: - web: - build: - context: . - dockerfile: Dockerfile + long-short-backend: + container_name: margin-api + <<: *base-backend + ports: + - "9999:9999" + command: ["pnpm", "--filter=long-short-backend", "start"] + + # web: + # build: + # context: . + # dockerfile: Dockerfile + # network_mode: host + # environment: + # NODE_ENV: production + # NEXT_PUBLIC_NETWORK_ENV: TESTNET_PREVIEW + # PORT: 3002 + # expose: + # - "3002" + # restart: unless-stopped + + redis: + image: redis:8 + container_name: margin-redis + restart: always + command: --requirepass 7obaQyYSDDLDk3ECYA + ports: + - "6380:6379" + volumes: + - redis-data:/data + healthcheck: + test: ["CMD", "redis-cli", "-a", "7obaQyYSDDLDk3ECYA", "--raw", "incr", "ping"] + interval: 5s + timeout: 3s + retries: 5 + + postgres: + image: postgres:18 + container_name: margin-postgres + restart: always + ports: + - "5433:5432" environment: - NODE_ENV: production - NEXT_PUBLIC_NETWORK_ENV: TESTNET_PREVIEW - restart: unless-stopped - networks: - - dev-network + POSTGRES_PASSWORD: JBNGlQ9wNFLlYWc2mG + POSTGRES_DB: margin + command: ["-c", "max_connections=50"] + volumes: + - postgres-data:/var/lib/postgresql + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 -networks: - dev-network: - name: dev-network - driver: bridge +volumes: + postgres-data: {} + redis-data: {} diff --git a/packages/ledger-core/src/address.ts b/packages/ledger-core/src/address.ts index 3b757b5..d00692e 100644 --- a/packages/ledger-core/src/address.ts +++ b/packages/ledger-core/src/address.ts @@ -343,6 +343,13 @@ export class Address { return ret; } + toHex(): CborHex { + const cslAddress = this.toCSL(); + const hex = cslAddress.to_hex(); + safeFreeRustObjects(cslAddress); + return hex; + } + toString(): string { return this.bech32; } diff --git a/packages/minswap-dex-v2/src/order.ts b/packages/minswap-dex-v2/src/order.ts index 8d264b8..f8a6a1a 100644 --- a/packages/minswap-dex-v2/src/order.ts +++ b/packages/minswap-dex-v2/src/order.ts @@ -22,7 +22,13 @@ import { type CborHex, type CSLPlutusData, Maybe, Result } from "@minswap/felis- import { getDexV2Configs, getDexV2OrderScriptHash } from "./constants"; import { InvalidOrder } from "./invalid-order"; import { DexVersion, OrderV2StepType } from "./order-step"; -import { normalizePair } from "./utils"; + +function normalizePair([a, b]: [Asset, Asset]): [Asset, Asset] { + if (a.compare(b) > 0) { + return [b, a]; + } + return [a, b]; +} export enum OrderV2AuthorizationMethodType { SIGNATURE = 0, diff --git a/packages/minswap-dex-v2/src/pool.ts b/packages/minswap-dex-v2/src/pool.ts index fc05281..77dadbb 100644 --- a/packages/minswap-dex-v2/src/pool.ts +++ b/packages/minswap-dex-v2/src/pool.ts @@ -24,7 +24,13 @@ import invariant from "@minswap/tiny-invariant"; import { DEX_V2_DEFAULT_POOL_ADA, getDexV2Configs } from "./constants"; import { OrderV2Direction } from "./order"; -import { normalizePair } from "./utils"; + +function normalizePair([a, b]: [Asset, Asset]): [Asset, Asset] { + if (a.compare(b) > 0) { + return [b, a]; + } + return [a, b]; +} export type PoolV2BaseFee = { feeANumerator: bigint; diff --git a/packages/minswap-dex-v2/src/utils.ts b/packages/minswap-dex-v2/src/utils.ts index 3ff8636..a5d4226 100644 --- a/packages/minswap-dex-v2/src/utils.ts +++ b/packages/minswap-dex-v2/src/utils.ts @@ -1,16 +1,8 @@ -import type { Asset } from "@minswap/felis-ledger-core"; import { type Maybe, Result } from "@minswap/felis-ledger-utils"; import { OrderV2Direction } from "./order"; import type { PoolV2BaseFee } from "./pool"; import { bigIntPow, sqrt } from "./sqrt"; -export function normalizePair([a, b]: [Asset, Asset]): [Asset, Asset] { - if (a.compare(b) > 0) { - return [b, a]; - } - return [a, b]; -} - export type PoolFee = { // Trading Fee is the total Fee that is taken from the Traders by the Liquidity Pool tradingFee: number; diff --git a/packages/minswap-lending-market/src/index.ts b/packages/minswap-lending-market/src/index.ts index bb0eaf1..9bcd8b0 100644 --- a/packages/minswap-lending-market/src/index.ts +++ b/packages/minswap-lending-market/src/index.ts @@ -1,3 +1,4 @@ export * from "./lending-market"; export * from "./liqwid-provider"; +export * from "./liqwid-provider-v2"; export * from "./nitro-wallet"; diff --git a/packages/minswap-lending-market/src/lending-market.ts b/packages/minswap-lending-market/src/lending-market.ts index 1d36876..94f0c9c 100644 --- a/packages/minswap-lending-market/src/lending-market.ts +++ b/packages/minswap-lending-market/src/lending-market.ts @@ -257,6 +257,7 @@ export namespace LendingMarket { const mapMarket: Record = { Ada: qAdaToken, MIN: qMinToken, + NIGHT: Asset.fromString("c45fa8aefc662c003a32be67f6a4652d8ce56bd9e54d7696efd40c86"), }; const collaterals: LiqwidProvider.LoanCalculationInput["collaterals"] = []; const buildTxCollaterals: LiqwidProvider.BorrowCollateral[] = []; diff --git a/packages/minswap-lending-market/src/liqwid-provider-v2.ts b/packages/minswap-lending-market/src/liqwid-provider-v2.ts new file mode 100644 index 0000000..77c5f2f --- /dev/null +++ b/packages/minswap-lending-market/src/liqwid-provider-v2.ts @@ -0,0 +1,839 @@ +import { NetworkEnvironment, type PrivateKey, XJSON } from "@minswap/felis-ledger-core"; +import { blake2b256, Result, RustModule } from "@minswap/felis-ledger-utils"; +import * as cbor from "cbor"; + +/** + * Liqwid Protocol Provider V2 + * + * A cleaner, more type-safe implementation for interacting with the Liqwid Finance API. + * Based on the Liqwid GraphQL schema. + */ +export namespace LiqwidProviderV2 { + // ============================================================================ + // Common Types + // ============================================================================ + + export type Currency = "EUR" | "USD" | "GBP" | "CAD" | "BRL" | "JPY" | "VND" | "CZK" | "AUD" | "SGD" | "CHF"; + + export type SupportedWallet = "ETERNL" | "BEGIN"; + + export type Network = "MAINNET" | "PREVIEW"; + + /** Market IDs for different assets */ + export type MarketId = "Ada" | "MIN" | "DJED" | "iUSD" | "SHEN" | "LQ" | "HUNT" | "WMT" | "LENFI" | "NIGHT"; + + /** Collateral market identifiers (format: MarketId.PolicyId) */ + export type CollateralId = `${MarketId}.${string}`; + + // ============================================================================ + // API Configuration + // ============================================================================ + + const API_URLS: Record = { + [NetworkEnvironment.MAINNET]: "https://v2.api.liqwid.finance/graphql", + [NetworkEnvironment.TESTNET_PREPROD]: "https://v2.api.preprod.liqwid.dev/graphql", + [NetworkEnvironment.TESTNET_PREVIEW]: "https://v2.api.preview.liqwid.dev/graphql", + }; + + export type ApiConfig = { + networkEnv: NetworkEnvironment; + /** Optional client-side endpoint override (for browser proxying) */ + clientEndpoint?: string; + }; + + // ============================================================================ + // GraphQL Types - Inputs + // ============================================================================ + + export type UserAddressInput = { + address: string; + changeAddress?: string; + otherAddresses?: string[]; + utxos: string[]; + }; + + export type CustomOutput = { + address: string; + inlineDatum?: string; + }; + + export type InCurrencyInput = { + currency?: Currency; + }; + + // Transaction Inputs + export type SupplyTransactionInput = UserAddressInput & { + marketId: MarketId; + amount: number; + wallet?: SupportedWallet; + mintedQTokensDestination?: CustomOutput; + }; + + export type WithdrawTransactionInput = UserAddressInput & { + marketId: MarketId; + amount: number; + wallet?: SupportedWallet; + withdrawnUnderlyingDestination?: CustomOutput; + }; + + export type BorrowCollateralInput = { + id: string; + tokenName?: string; + amount: number; + }; + + export type BorrowTransactionInput = UserAddressInput & { + marketId: MarketId; + amount: number; + collaterals: BorrowCollateralInput[]; + principalDestination?: CustomOutput; + }; + + export type ModifyBorrowTransactionInput = UserAddressInput & { + txId: string; + amount: number; + collaterals: BorrowCollateralInput[]; + redeemCollateral?: boolean; + loanPrincipalAndCollateralDeltasDestination?: CustomOutput; + }; + + /** + * Input for repaying a loan (full repay) + * Based on Liqwid's GetRepayTransactionInput + */ + export type RepayLoanTransactionInput = UserAddressInput & { + /** Loan transaction ID with output index, format: "{txHash}-{outputIndex}" */ + loanUtxoId: string; + /** Collaterals to redeem after repaying, format: [{id: "MarketId.policyId", amount: qTokenAmount}] */ + collaterals: BorrowCollateralInput[]; + }; + + export type SubmitTransactionInput = { + transaction: string; + signature: string; + }; + + // Calculation Inputs + export type LoanCalculationCollateralInput = { + id: string; + amount: number; + }; + + export type LoanCalculationInput = { + market: MarketId; + debt: number; + collaterals: LoanCalculationCollateralInput[]; + }; + + export type SupplyCalculationInput = { + marketId: MarketId; + amount: number; + wallet?: SupportedWallet; + }; + + export type WithdrawCalculationInput = { + marketId: MarketId; + amount: number; + wallet?: SupportedWallet; + }; + + export type NetApySupplyInput = { + marketId: MarketId; + amount: number; + }; + + export type NetApyInput = { + paymentKeys: string[]; + supplies: NetApySupplyInput[]; + currency?: Currency; + }; + + // Query Inputs + export type LoansInput = { + paymentKeys?: string[]; + marketIds?: string[]; + sorts?: LoanSort[]; + filters?: LoanFilter[]; + page?: number; + perPage?: number; + search?: string; + }; + + export type MarketsInput = { + ids?: string[]; + sorts?: MarketSort[]; + filters?: MarketFilter[]; + page?: number; + perPage?: number; + search?: string; + }; + + export type YieldEarnedInput = { + addresses: string[]; + date?: { + startTime: string; + endTime: string; + }; + }; + + // ============================================================================ + // GraphQL Types - Enums + // ============================================================================ + + export type LoanSort = + | "MARKET_ID" + | "MARKET_ID_DESC" + | "DEBT" + | "DEBT_DESC" + | "COLLATERAL_IN_CURRENCY" + | "COLLATERAL_IN_CURRENCY_DESC" + | "HEALTH_FACTOR" + | "HEALTH_FACTOR_DESC" + | "APY" + | "APY_DESC"; + + export type LoanFilter = + | "STABLECOIN" + | "CNT" + | "BRIDGED" + | "PRIME" + | "HAS_DEBT" + | "NO_DEBT" + | "HAS_COLLATERAL" + | "NO_COLLATERAL" + | "CAN_BE_LIQUIDATED"; + + export type MarketSort = + | "ID" + | "ID_DESC" + | "SUPPLY" + | "SUPPLY_DESC" + | "SUPPLY_IN_CURRENCY" + | "SUPPLY_IN_CURRENCY_DESC" + | "BORROW" + | "BORROW_DESC" + | "BORROW_IN_CURRENCY" + | "BORROW_IN_CURRENCY_DESC" + | "LIQUIDITY" + | "LIQUIDITY_DESC" + | "LIQUIDITY_IN_CURRENCY" + | "LIQUIDITY_IN_CURRENCY_DESC" + | "SUPPLY_APY" + | "SUPPLY_APY_DESC" + | "BORROW_APY" + | "BORROW_APY_DESC"; + + export type MarketFilter = "STABLECOIN" | "CNT" | "BRIDGED" | "PRIME" | "PUBLIC" | "PRIVATE"; + + // ============================================================================ + // GraphQL Types - Outputs + // ============================================================================ + + export type Transaction = { + cbor: string; + }; + + // Calculation Results + export type LoanCalculationResult = { + healthFactor: number; + maxBorrow: number; + maxBorrowCap: number | null; + batchingFee: number; + protocolFee: number; + protocolFeePercentage: number; + collateral: number; + collaterals: Array<{ + id: string; + amount: number; + LTV: number; + healthFactor: number; + }>; + }; + + export type SupplyCalculationResult = { + batchingFee: number; + supplyCap: number | null; + walletFee: number; + }; + + export type WithdrawCalculationResult = { + batchingFee: number; + walletFee: number; + withdrawCap: number; + }; + + export type NetApyResult = { + netApy: number; + netApyLqRewards: number; + borrowApy: number; + totalBorrow: number; + supplyApy: number; + totalSupply: number; + }; + + // Data Types + export type Asset = { + id: string; + name: string; + symbol: string; + displayName: string; + decimals: number; + currencySymbol: string; + policyId: string; + hexName: string; + logo: string | null; + price: number; + priceUpdatedAt: string; + }; + + export type Market = { + id: string; + displayName: string; + symbol: string; + supply: number; + borrow: number; + liquidity: number; + supplyAPY: number; + borrowAPY: number; + lqSupplyAPY: number; + utilization: number; + exchangeRate: number; + batching: boolean; + batchExpired: boolean; + frozen: boolean; + private: boolean; + delisting: boolean; + prime: boolean; + loanOriginationFeePercentage: number; + asset: Asset; + receiptAsset: Asset; + }; + + export type LoanCollateral = { + id: string; + tokenName: string | null; + qTokenName: string | null; + amount: number; + qTokenAmount: number; + LTV: number; + healthFactor: number; + market: { + id: string; + displayName: string; + exchangeRate: number; + delisting: boolean; + } | null; + asset: Asset; + }; + + export type Loan = { + id: string; + transactionId: string; + transactionIndex: number; + marketId: string; + publicKey: string; + amount: number; + adjustedAmount: number; + collateral: number; + interest: number; + APY: number; + LTV: number; + healthFactor: number; + time: number; + collaterals: LoanCollateral[]; + market: Market; + asset: Asset; + }; + + export type YieldEarnedMarket = { + id: string; + displayName: string; + currencySymbol: string; + hexName: string; + amount: number; + amountInCurrency: number; + }; + + export type YieldEarnedResult = { + totalYieldEarned: number; + markets: YieldEarnedMarket[]; + }; + + // Pagination + export type Pagination = { + page: number; + perPage: number; + pagesCount: number; + totalCount: number; + results: T[]; + }; + + // ============================================================================ + // API Client + // ============================================================================ + + type GraphQLResponse = { + data?: T; + errors?: Array<{ message: string }>; + }; + + const getApiUrl = (config: ApiConfig): string => { + if (typeof window !== "undefined" && config.clientEndpoint) { + return config.clientEndpoint; + } + return API_URLS[config.networkEnv]; + }; + + const executeQuery = async ( + config: ApiConfig, + operationName: string, + query: string, + variables: TVariables, + ): Promise> => { + try { + const requestBody = JSON.stringify({ operationName, query, variables }); + // console.log(`[LiqwidProviderV2] ${operationName} request:`, requestBody); + + const response = await fetch(getApiUrl(config), { + method: "POST", + headers: { + accept: "application/json", + "content-type": "application/json", + }, + body: requestBody, + }); + + if (!response.ok) { + return Result.err(new Error(`Liqwid API error: ${response.status} ${response.statusText}`)); + } + + const json = (await response.json()) as GraphQLResponse; + // console.log(`[LiqwidProviderV2] ${operationName} response:`, JSON.stringify(json)); + + if (json.errors?.length) { + return Result.err(new Error(`GraphQL error: ${XJSON.stringify(json.errors)}`)); + } + + if (!json.data) { + return Result.err(new Error("No data returned from Liqwid API")); + } + + return Result.ok(json.data); + } catch (error) { + return Result.err( + new Error(`${operationName} failed: ${error instanceof Error ? error.message : String(error)}`), + ); + } + }; + + // ============================================================================ + // Transactions API + // ============================================================================ + + export namespace Transactions { + const SUPPLY_QUERY = ` + query Supply($input: SupplyTransactionInput!) { + liqwid { transactions { supply(input: $input) { cbor } } } + } + `; + + const WITHDRAW_QUERY = ` + query Withdraw($input: WithdrawTransactionInput!) { + liqwid { transactions { withdraw(input: $input) { cbor } } } + } + `; + + const BORROW_QUERY = ` + query Borrow($input: BorrowTransactionInput!) { + liqwid { transactions { borrow(input: $input) { cbor } } } + } + `; + + const MODIFY_BORROW_QUERY = ` + query ModifyBorrow($input: ModifyBorrowTransactionInput!) { + liqwid { transactions { modifyBorrow(input: $input) { cbor } } } + } + `; + + const SUBMIT_MUTATION = ` + mutation Submit($input: SubmitTransactionInput!) { + submitTransaction(input: $input) + } + `; + + type SupplyResponse = { liqwid: { transactions: { supply: Transaction } } }; + type WithdrawResponse = { liqwid: { transactions: { withdraw: Transaction } } }; + type BorrowResponse = { liqwid: { transactions: { borrow: Transaction } } }; + type ModifyBorrowResponse = { liqwid: { transactions: { modifyBorrow: Transaction } } }; + type SubmitResponse = { submitTransaction: string }; + + /** Build a supply transaction */ + export const supply = async (config: ApiConfig, input: SupplyTransactionInput): Promise> => { + const result = await executeQuery( + config, + "Supply", + SUPPLY_QUERY, + { + input: { + ...input, + changeAddress: input.changeAddress ?? input.address, + otherAddresses: input.otherAddresses ?? [input.address], + }, + }, + ); + return result.type === "ok" ? Result.ok(result.value.liqwid.transactions.supply.cbor) : result; + }; + + /** Build a withdraw transaction */ + export const withdraw = async ( + config: ApiConfig, + input: WithdrawTransactionInput, + ): Promise> => { + const result = await executeQuery( + config, + "Withdraw", + WITHDRAW_QUERY, + { + input: { + ...input, + changeAddress: input.changeAddress ?? input.address, + otherAddresses: input.otherAddresses ?? [input.address], + }, + }, + ); + return result.type === "ok" ? Result.ok(result.value.liqwid.transactions.withdraw.cbor) : result; + }; + + /** Build a borrow transaction */ + export const borrow = async (config: ApiConfig, input: BorrowTransactionInput): Promise> => { + const result = await executeQuery( + config, + "Borrow", + BORROW_QUERY, + { + input: { + ...input, + changeAddress: input.changeAddress ?? input.address, + otherAddresses: input.otherAddresses ?? [input.address], + }, + }, + ); + return result.type === "ok" ? Result.ok(result.value.liqwid.transactions.borrow.cbor) : result; + }; + + /** Build a modify borrow (repay/borrow more) transaction */ + export const modifyBorrow = async ( + config: ApiConfig, + input: ModifyBorrowTransactionInput, + ): Promise> => { + const result = await executeQuery( + config, + "ModifyBorrow", + MODIFY_BORROW_QUERY, + { + input: { + ...input, + changeAddress: input.changeAddress ?? input.address, + otherAddresses: input.otherAddresses ?? [input.address], + }, + }, + ); + return result.type === "ok" ? Result.ok(result.value.liqwid.transactions.modifyBorrow.cbor) : result; + }; + + /** + * Build a repay loan transaction (full repay with collateral redemption) + * Uses modifyBorrow internally with amount=0 to trigger full repay + */ + export const repayLoan = async ( + config: ApiConfig, + input: RepayLoanTransactionInput, + ): Promise> => { + const modifyBorrowInput: ModifyBorrowTransactionInput = { + address: input.address, + changeAddress: input.changeAddress, + otherAddresses: input.otherAddresses, + utxos: input.utxos, + txId: input.loanUtxoId, + amount: 0, // Full repay + collaterals: input.collaterals, + }; + return modifyBorrow(config, modifyBorrowInput); + }; + + /** Submit a signed transaction */ + export const submit = async (config: ApiConfig, input: SubmitTransactionInput): Promise> => { + const result = await executeQuery( + config, + "Submit", + SUBMIT_MUTATION, + { input }, + ); + return result.type === "ok" ? Result.ok(result.value.submitTransaction) : result; + }; + } + + // ============================================================================ + // Calculations API + // ============================================================================ + + export namespace Calculations { + const LOAN_QUERY = ` + query LoanCalc($input: LoanCalculationInput!, $currency: InCurrencyInput) { + liqwid { + calculations { + loan(input: $input) { + healthFactor maxBorrow maxBorrowCap batchingFee + protocolFee protocolFeePercentage + collateral(input: $currency) + collaterals { id amount LTV healthFactor } + } + } + } + } + `; + + const SUPPLY_QUERY = ` + query SupplyCalc($input: SupplyCalculationInput!) { + liqwid { calculations { supply(input: $input) { batchingFee supplyCap walletFee } } } + } + `; + + const WITHDRAW_QUERY = ` + query WithdrawCalc($input: WithdrawCalculationInput!) { + liqwid { calculations { withdraw(input: $input) { batchingFee walletFee withdrawCap } } } + } + `; + + const NET_APY_QUERY = ` + query NetApy($input: NetApyInput!) { + liqwid { + calculations { + netAPY(input: $input) { + netApy netApyLqRewards borrowApy totalBorrow supplyApy totalSupply + } + } + } + } + `; + + type LoanResponse = { liqwid: { calculations: { loan: LoanCalculationResult } } }; + type SupplyResponse = { liqwid: { calculations: { supply: SupplyCalculationResult } } }; + type WithdrawResponse = { liqwid: { calculations: { withdraw: WithdrawCalculationResult } } }; + type NetApyResponse = { liqwid: { calculations: { netAPY: NetApyResult } } }; + + /** Calculate loan parameters (health factor, max borrow, fees) */ + export const loan = async ( + config: ApiConfig, + input: LoanCalculationInput, + currency: Currency = "USD", + ): Promise> => { + const result = await executeQuery( + config, + "LoanCalc", + LOAN_QUERY, + { input, currency: { currency } }, + ); + return result.type === "ok" ? Result.ok(result.value.liqwid.calculations.loan) : result; + }; + + /** Calculate supply parameters */ + export const supply = async ( + config: ApiConfig, + input: SupplyCalculationInput, + ): Promise> => { + const result = await executeQuery( + config, + "SupplyCalc", + SUPPLY_QUERY, + { input }, + ); + return result.type === "ok" ? Result.ok(result.value.liqwid.calculations.supply) : result; + }; + + /** Calculate withdraw parameters */ + export const withdraw = async ( + config: ApiConfig, + input: WithdrawCalculationInput, + ): Promise> => { + const result = await executeQuery( + config, + "WithdrawCalc", + WITHDRAW_QUERY, + { input }, + ); + return result.type === "ok" ? Result.ok(result.value.liqwid.calculations.withdraw) : result; + }; + + /** Calculate net APY for a portfolio */ + export const netApy = async (config: ApiConfig, input: NetApyInput): Promise> => { + const result = await executeQuery(config, "NetApy", NET_APY_QUERY, { + input, + }); + return result.type === "ok" ? Result.ok(result.value.liqwid.calculations.netAPY) : result; + }; + } + + // ============================================================================ + // Data API + // ============================================================================ + + export namespace Data { + const MARKETS_QUERY = ` + query Markets($input: MarketsInput, $currency: InCurrencyInput) { + liqwid { + data { + markets(input: $input) { + page perPage pagesCount totalCount + results { + id displayName symbol + supply(input: $currency) borrow(input: $currency) liquidity(input: $currency) + supplyAPY borrowAPY lqSupplyAPY utilization exchangeRate + batching batchExpired frozen private delisting prime + loanOriginationFeePercentage + asset { id name symbol displayName decimals currencySymbol policyId hexName logo price(input: $currency) priceUpdatedAt } + receiptAsset { id name symbol displayName decimals currencySymbol policyId hexName logo price(input: $currency) priceUpdatedAt } + } + } + } + } + } + `; + + const LOANS_QUERY = ` + query Loans($input: LoansInput, $currency: InCurrencyInput) { + liqwid { + data { + loans(input: $input) { + page perPage pagesCount totalCount + results { + id transactionId transactionIndex marketId publicKey + amount(input: $currency) adjustedAmount(input: $currency) collateral(input: $currency) + interest APY LTV healthFactor time + collaterals { + id tokenName qTokenName amount(input: $currency) qTokenAmount LTV healthFactor + market { id displayName exchangeRate delisting } + asset { id name symbol displayName decimals currencySymbol policyId hexName logo price(input: $currency) priceUpdatedAt } + } + market { id displayName symbol supplyAPY borrowAPY exchangeRate } + asset { id name symbol displayName decimals currencySymbol policyId hexName logo price(input: $currency) priceUpdatedAt } + } + } + } + } + } + `; + + const YIELD_QUERY = ` + query Yield($input: YieldEarnedInput!, $currency: InCurrencyInput) { + historical { + yieldEarned(input: $input) { + totalYieldEarned(input: $currency) + markets { id displayName currencySymbol hexName amount amountInCurrency(input: $currency) } + } + } + } + `; + + type MarketsResponse = { liqwid: { data: { markets: Pagination } } }; + type LoansResponse = { liqwid: { data: { loans: Pagination } } }; + type YieldResponse = { historical: { yieldEarned: YieldEarnedResult } }; + + /** Get markets data */ + export const markets = async ( + config: ApiConfig, + input?: MarketsInput, + currency: Currency = "USD", + ): Promise, Error>> => { + const result = await executeQuery( + config, + "Markets", + MARKETS_QUERY, + { input: input ?? { perPage: 50 }, currency: { currency } }, + ); + return result.type === "ok" ? Result.ok(result.value.liqwid.data.markets) : result; + }; + + /** Get loans (borrow positions) data */ + export const loans = async ( + config: ApiConfig, + input: LoansInput, + currency: Currency = "USD", + ): Promise, Error>> => { + const result = await executeQuery( + config, + "Loans", + LOANS_QUERY, + { input, currency: { currency } }, + ); + return result.type === "ok" ? Result.ok(result.value.liqwid.data.loans) : result; + }; + + /** Get yield earned for addresses */ + export const yieldEarned = async ( + config: ApiConfig, + input: YieldEarnedInput, + currency: Currency = "USD", + ): Promise> => { + const result = await executeQuery( + config, + "Yield", + YIELD_QUERY, + { input, currency: { currency } }, + ); + return result.type === "ok" ? Result.ok(result.value.historical.yieldEarned) : result; + }; + + /** Get a single market by ID */ + export const market = async ( + config: ApiConfig, + marketId: MarketId, + currency: Currency = "USD", + ): Promise> => { + const result = await markets(config, { ids: [marketId], perPage: 1 }, currency); + return result.type === "ok" ? Result.ok(result.value.results[0] ?? null) : result; + }; + + /** Get loans for specific payment keys */ + export const loansForUser = async ( + config: ApiConfig, + paymentKeys: string[], + currency: Currency = "USD", + ): Promise> => { + const result = await loans(config, { paymentKeys, perPage: 100 }, currency); + return result.type === "ok" ? Result.ok(result.value.results) : result; + }; + } + + // ============================================================================ + // Utilities + // ============================================================================ + + /** Get the transaction hash from a CBOR-encoded transaction */ + export const getTxHash = (txCborHex: string): string => { + const decoded = cbor.decode(Buffer.from(txCborHex, "hex")); + const body = decoded[0]; + const bodyHex = Buffer.from(cbor.encode(body)).toString("hex"); + return blake2b256(Buffer.from(bodyHex, "hex")); + }; + + /** Sign a Liqwid transaction with a private key */ + export const signTx = (txCborHex: string, privateKey: PrivateKey): string => { + const txHash = getTxHash(txCborHex); + const ECSL = RustModule.getE; + const witnessSet = ECSL.TransactionWitnessSet.new(); + const vkeyWitnesses = ECSL.Vkeywitnesses.new(); + const pKey = privateKey.toECSL(); + const cslTxHash = ECSL.TransactionHash.from_hex(txHash); + const vKey = ECSL.make_vkey_witness(cslTxHash, pKey); + vkeyWitnesses.add(vKey); + witnessSet.set_vkeys(vkeyWitnesses); + return witnessSet.to_hex(); + }; + + /** Create an API config helper */ + export const createConfig = (networkEnv: NetworkEnvironment, clientEndpoint?: string): ApiConfig => ({ + networkEnv, + clientEndpoint, + }); +} diff --git a/packages/minswap-lending-market/src/liqwid-provider.ts b/packages/minswap-lending-market/src/liqwid-provider.ts index 7ff6bff..24df4df 100644 --- a/packages/minswap-lending-market/src/liqwid-provider.ts +++ b/packages/minswap-lending-market/src/liqwid-provider.ts @@ -3,7 +3,7 @@ import { blake2b256, Result, RustModule } from "@minswap/felis-ledger-utils"; import * as cbor from "cbor"; export namespace LiqwidProvider { - export type MarketId = "MIN" | "Ada"; + export type MarketId = "MIN" | "Ada" | "NIGHT"; export type CollateralMarket = | "Ada.186cd98a29585651c89f05807a876cf26cdf47a7f86f70be3b9e4cc0" | "MIN.50e015ec8204db83a4f57aa9ee40ce6ea157e3b7335a149fafe3f370"; diff --git a/packages/minswap-lending-market/src/schema.graphql b/packages/minswap-lending-market/src/schema.graphql new file mode 100644 index 0000000..331217e --- /dev/null +++ b/packages/minswap-lending-market/src/schema.graphql @@ -0,0 +1,1391 @@ +directive @priceConversion on FIELD_DEFINITION + +directive @cost( + """ + Assumes the cost of the annotated component + """ + weight: Int! +) on ARGUMENT_DEFINITION | ENUM | FIELD_DEFINITION | INPUT_FIELD_DEFINITION | OBJECT | SCALAR + +""" +Indicates exactly one field must be supplied and this field must not be `null`. +""" +directive @oneOf on INPUT_OBJECT + +input AquafarmersInput { + assets: [String!]! +} + +""" +Metadata for the aquafarmer +""" +type AquafarmerOnchainMetadata { + hat: String + tier: String + name: String + image: String + github: [String!] + medium: [String!] + discord: [String!] + project: [String!] + twitter: [String!] + website: [String!] + mediaType: String + background: String + farmerHead: String + description: String + armMechanics: String + leftHandTool: String + farmerClothing: String + rightHandTool: String + farmerBodyColor: String + backgroundAccessories: String +} + +enum AgoraStakeSort { + CREATED_DATE + CREATED_DATE_DESC + MODIFIED_DATE + MODIFIED_DATE_DESC + AMOUNT + AMOUNT_DESC +} + +enum AgoraStakeFilter { + OWNED + DELEGATED +} + +input AgoraStakesInput { + paymentKeys: [String!]! + page: Int = 0 + perPage: Int = 20 + sorts: [AgoraStakeSort!] + filters: [AgoraStakeFilter!] +} + +enum AgoraStakeLockType { + Created + Cosigned + Voted +} + +type AgoraStakeLock { + proposalId: Float! + resultId: Int + votedAt: Float + type: AgoraStakeLockType! +} + +type AgoraStakePortion { + amount: Float! + endSlot: Float + endTimestamp: Float + startSlot: Float! + startTimestamp: Float! +} + +type AgoraStake { + txId: String! + owner: String! + delegatedTo: String + stakedAmount: Float! + lockedBy: [AgoraStakeLock!]! + substakes: [AgoraStakePortion!]! +} + +type AgoraStakePagination { + page: Int! + perPage: Int! + pagesCount: Int! + totalCount: Int! + results: [AgoraStake!]! +} + +type AquafarmerBoost { + id: String! + boost: Float! +} + +type Aquafarmer { + asset: String! + policyId: String! + assetName: String! + metadata: AquafarmerOnchainMetadata! +} + +type PublicDelegatee { + address: String! + name: String + discord: String + title: String! +} + +enum ProposalStatus { + Draft + Review + Voting + Locked + Finished + Executed +} + +type Cosigner { + address: String! + name: String + discord: String + title: String +} + +type ProposalTiming { + status: ProposalStatus! + startTimestamp: Float! + endTimestamp: Float +} + +type ProposalVote { + resultId: Float! + amount: Float! + percentage: Float! + name: String! + description: String! +} + +type Proposal { + id: Float! + status: ProposalStatus! + cosigners: [Cosigner!]! + timings: [ProposalTiming!]! + totalVotes: Float! + votes(sorts: [ProposalVoteSort!]): [ProposalVote!]! + title: String! + description: String! + ipfsUrl: String + minStakeVotingTime: Float! +} + +enum ProposalSort { + ID + ID_DESC + CREATED_DATE + CREATED_DATE_DESC + UPDATED_DATE + UPDATED_DATE_DESC + AMOUNT + AMOUNT_DESC +} + +enum ProposalVoteSort { + RESULT_ID + RESULT_ID_DESC + AMOUNT + AMOUNT_DESC +} + +enum ProposalFilter { + STATUS_DRAFT + STATUS_VOTING + STATUS_LOCKED + STATUS_FINISHED + STATUS_EXECUTED + VOTED_ON + COSIGNED +} + +input ProposalsInput { + page: Int = 0 + perPage: Int = 50 + paymentKeys: [String!] + sorts: [ProposalSort!] + filters: [ProposalFilter!] + search: String +} + +type ProposalsPagination { + page: Int! + perPage: Int! + pagesCount: Int! + totalCount: Int! + results: [Proposal!]! +} + +input ProposalInput { + id: Float! +} + +input UserAddressInput { + address: String! + changeAddress: String + otherAddresses: [String!] + utxos: [String!]! +} + +type AgoraData { + aquafarmersBoost: [AquafarmerBoost!]! + aquafarmers(input: AquafarmersInput!): [Aquafarmer!]! + stakes(input: AgoraStakesInput!): AgoraStakePagination! + publicDelegatees: [PublicDelegatee!]! + proposal(input: ProposalInput!): Proposal! + proposals(input: ProposalsInput): ProposalsPagination! + hasStakesAndAquafarmersInSameAddress(input: UserAddressInput!): Boolean! +} + +type AgoraQueries { + data: AgoraData! + transactions: AgoraTransactions! +} + +type Query { + agora: AgoraQueries! + meta: MetaQueries! + + """ + Get the exchange rate of a currency + """ + currencyExchangeRate(input: CurrencyExchangeRateInput!): CurrencyExchangeRate! + liqwid: LiqwidQueries! + lq: LQQueries! + analytics: AnalyticsQueries! + historical: HistoricalQueries! +} + +enum Network { + MAINNET + PREVIEW +} + +type MetaQueries { + apiVersion: String! + network: Network! +} + +enum Currency { + """ + Euro € + """ + EUR + + """ + US Dollar $ + """ + USD + + """ + British Pound £ + """ + GBP + + """ + Canadian Dollar C$ + """ + CAD + + """ + Brazilian Real R$ + """ + BRL + + """ + Japanese Yen ¥ + """ + JPY + + """ + Vietnamese Dong ₫ + """ + VND + + """ + Czech Koruna Kč + """ + CZK + + """ + Australian Dollar A$ + """ + AUD + + """ + Singapore Dollar S$ + """ + SGD + + """ + Swiss Franc CHF + """ + CHF +} + +type CurrencyExchangeRate { + source: String! + target: String! + value: Float! + timestamp: Float! +} + +input CurrencyExchangeRateInput { + source: String! + target: String! +} + +type LiqwidQueries { + data: LiqwidData! + transactions: LiqwidTransactions! + calculations: LiqwidCalculations! +} + +type LQQueries { + totalSupply: Float! + circulatingSupply: Float! + treasury: Float! + + """ + Amount of LQ staked + """ + staked: Float! + price(input: InCurrencyInput): Float! + currencySymbol: String! +} + +type Transaction { + cbor: String! +} + +input SubmitTransactionInput { + transaction: String! + signature: String! +} + +type Mutation { + """ + Submit a CBOR transaction + """ + submitTransaction(input: SubmitTransactionInput!): String! +} + +type AnalyticsQueries { + overview(startDate: String, endDate: String): AnalyticsOverview! + markets(startDate: String, endDate: String): [MarketAnalytics!]! + + """ + Query protocol revenue metrics for a date range. + Returns revenue breakdown including origination fees, liquidation profit, + ADA staking rewards, and DAO costs (dividends/POL interest). + """ + revenue(startDate: String, endDate: String): ProtocolRevenue! +} + +enum TransactionType { + SUPPLY + WITHDRAW + BORROW + BORROW_MORE + LOAN_MODIFICATION + REPAY + FULL_REPAY + LIQUIDATE + LIQUIDATOR +} + +type CustomFee { + """ + Wallet fee name + """ + wallet: SupportedWallet! + displayName: String! + + """ + Amount taken in the asset being supplied or withdrawn + """ + amount: Float! +} + +type HistoricalTransaction { + """ + txHash + """ + id: String! + displayName: String! + logo: String! + time: String! + type: TransactionType! + oraclePrice: Float! + customFee: CustomFee + + """ + token amount for supply/withdraw + """ + qAmount: Float + amount: Float + amountUSD: Float + + """ + qToken exchangeRate for supply/withdraw + """ + exchangeRate: Float + principal: Float + principalUSD: Float + minInterest: Float + healthFactor: Float + loanOriginationFee: Float + + """ + Modification loan (borrow more/modify/repay) + """ + beforeHealthFactor: Float + beforePrincipal: Float + beforePrincipalUSD: Float + totalCollateralUSD: Float + beforetotalCollateralUSD: Float + collaterals: [HistoryLoanCollateral!] +} + +type HistoryLoanCollateral { + """ + txHash + unit + """ + id: String! + displayName: String! + logo: String! + oraclePrice: Float! + + """ + qToken exchangeRate + """ + exchangeRate: Float! + qAmount: Float! + amount: Float! + amountUSD: Float! + healthFactor: Float! + + """ + Modification loan (borrow more/modify/repay) + """ + beforeHealthFactor: Float + beforeQAmount: Float + beforeAmount: Float + beforeAmountUSD: Float +} + +type HistoricalTransactionPagination { + page: Int! + perPage: Int! + pagesCount: Int! + totalCount: Int! + results: [HistoricalTransaction!]! +} + +enum HistoricalTransactionSort { + DATE_ASC + DATE_DESC + TYPE_ASC + TYPE_DESC + DISPLAY_NAME_ASC + DISPLAY_NAME_DESC +} + +enum HistoricalTransactionFilter { + SUPPLY + WITHDRAW + BORROW + BORROW_MORE + LOAN_MODIFICATION + REPAY + FULL_REPAY + LIQUIDATE + LIQUIDATOR +} + +input HistoricalTransactionDate { + startTime: String! + endTime: String! +} + +input HistoricalTransactionInput { + addresses: [String!]! + sorts: [HistoricalTransactionSort!] + filters: [HistoricalTransactionFilter!] + date: HistoricalTransactionDate + page: Int = 0 + perPage: Int = 100 + disablePagination: Boolean +} + +input YieldEarnedDate { + startTime: String! + endTime: String! +} + +input YieldEarnedInput { + addresses: [String!]! + date: YieldEarnedDate +} + +type YieldEarned { + totalYieldEarned(input: InCurrencyInput): Float! + markets: [YieldEarnedMarket!]! +} + +type YieldEarnedMarket { + id: String! + displayName: String! + currencySymbol: String! + hexName: String! + amount: Float! + amountInCurrency(input: InCurrencyInput): Float! +} + +type HistoricalQueries { + transactions(input: HistoricalTransactionInput): HistoricalTransactionPagination! + yieldEarned(input: YieldEarnedInput!): YieldEarned! +} + +input AgoraCreateStakeTransactionInput { + address: String! + changeAddress: String + otherAddresses: [String!] + utxos: [String!]! + amount: Float! +} + +input AgoraUpdateStakeTransactionInput { + address: String! + changeAddress: String + otherAddresses: [String!] + utxos: [String!]! + txId: String! + amount: Float! +} + +input AgoraDestroyStakeTransactionInput { + address: String! + changeAddress: String + otherAddresses: [String!] + utxos: [String!]! + txId: String! +} + +input AgoraDelegateStakeTransactionInput { + address: String! + changeAddress: String + otherAddresses: [String!] + utxos: [String!]! + txId: String! + delegatee: String +} + +input AgoraUnlockStakesTransactionInput { + address: String! + changeAddress: String + otherAddresses: [String!] + utxos: [String!]! + txIds: [String!]! + proposalId: Int! +} + +input AgoraVoteProposalTransactionInput { + address: String! + changeAddress: String + otherAddresses: [String!] + utxos: [String!]! + txIds: [String!]! + proposalId: Int! + resultId: Int! +} + +input AgoraCosignProposalTransactionInput { + address: String! + changeAddress: String + otherAddresses: [String!] + utxos: [String!]! + txId: String! + proposalId: Int! +} + +type AgoraTransactions { + createStake(input: AgoraCreateStakeTransactionInput!): Transaction! + updateStake(input: AgoraUpdateStakeTransactionInput!): Transaction! + destroyStake(input: AgoraDestroyStakeTransactionInput!): Transaction! + delegateStake(input: AgoraDelegateStakeTransactionInput!): Transaction! + unlockStakes(input: AgoraUnlockStakesTransactionInput!): Transaction! + voteProposal(input: AgoraVoteProposalTransactionInput!): Transaction! + cosignProposal(input: AgoraCosignProposalTransactionInput!): Transaction! +} + +enum NetAPYFilter { + STABLECOIN + CNT + BRIDGED +} + +type LoanCalculation { + healthFactor: Float! + collateral(input: InCurrencyInput): Float! + collaterals: [LoanCollateral!]! + + """ + The maximum amount that can be borrowed with the current input + """ + maxBorrow: Float! + + """ + The maximum amount that can be borrowed due to the borrow cap + """ + maxBorrowCap: Float + protocolFee: Float! + protocolFeePercentage: Float! + + """ + The batching fee in ADA + """ + batchingFee: Float! +} + +input LoanCalculationCollateralInput { + id: String! + amount: Float! +} + +input LoanCalculationInput { + market: String! + debt: Float! + collaterals: [LoanCalculationCollateralInput!]! +} + +input LiquidateCalculationInput { + txId: String! + amount: Float! +} + +type LiquidationCalculationCollateral { + id: String! + amount(input: InCurrencyInput): Float +} + +type LiquidateCalculation { + liquidationProfit(input: InCurrencyInput): Float + liquidationProfitPercent: Float + + """ + Collaterals seized in the liquidation with the current input + """ + collaterals: [LiquidationCalculationCollateral!] + + """ + Health factor with the current input + """ + healthFactor: Float! +} + +input SupplyCalculationInput { + amount: Float! + marketId: String! + wallet: SupportedWallet +} + +type SupplyCalculation { + """ + The batching fee in ADA + """ + batchingFee: Float! + + """ + The maximum amount that can be supplied due to the supply cap + """ + supplyCap: Float + + """ + The fee applied when using a certain wallet in the current market (currently, only ETERNL charges fees when using their Dapp directly) + """ + walletFee: Float! +} + +input WithdrawCalculationInput { + amount: Float! + marketId: String! + wallet: SupportedWallet +} + +type WithdrawCalculation { + """ + The batching fee in ADA + """ + batchingFee: Float! + + """ + The fee applied when using a certain wallet in the current market (currently, only ETERNL charges fees when using their Dapp directly) + """ + walletFee: Float! + + """ + The maximum amount that can be withdrawn due to the borrow cap + """ + withdrawCap: Float! +} + +type netApyCalculation { + """ + The net APY represents the effective annual yield after considering both supply and borrow rates + """ + netApy: Float! + + """ + The net APY including LQ rewards + """ + netApyLqRewards: Float! + + """ + The borrow APY is the rate charged on borrowed assets. + """ + borrowApy: Float! + + """ + The total amount of assets borrowed across all positions + """ + totalBorrow: Float! + + """ + The supply APY is the rate earned on supplied assets. + """ + supplyApy: Float! + + """ + The total amount of assets supplied + """ + totalSupply: Float! +} + +input NetApySupplyInput { + marketId: String! + amount: Float! +} + +input NetApyInput { + filters: [NetAPYFilter!] + supplies: [NetApySupplyInput!]! + paymentKeys: [String!]! + currency: Currency +} + +type LiqwidCalculations { + supply(input: SupplyCalculationInput!): SupplyCalculation! + withdraw(input: WithdrawCalculationInput!): WithdrawCalculation! + loan(input: LoanCalculationInput!): LoanCalculation! + liquidate(input: LiquidateCalculationInput!): LiquidateCalculation! + + """ + Calculation of a user's net APY + """ + netAPY(input: NetApyInput!): netApyCalculation! +} + +input InCurrencyInput { + currency: Currency +} + +type LiqwidData { + """ + ADA staking APY + """ + proofOfStakeApy: Float! + asset(input: AssetInput!): Asset + assets(input: AssetsInput): AssetPagination! + market(input: MarketInput!): Market + markets(input: MarketsInput): MarketPagination! + loans(input: LoansInput): LoanPagination! + supply(input: InCurrencyInput): Float! + borrow(input: InCurrencyInput): Float! + liquidity(input: InCurrencyInput): Float! + interpolatedDeposits(input: InCurrencyInput): Float! +} + +input CustomOutput { + address: String! + inlineDatum: String +} + +input ModifyBorrowTransactionInputCollateral { + id: String! + tokenName: String + amount: Float! +} + +input ModifyBorrowTransactionInput { + txId: String! + address: String! + changeAddress: String + otherAddresses: [String!] + amount: Float! + collaterals: [ModifyBorrowTransactionInputCollateral!]! + redeemCollateral: Boolean + utxos: [String!]! + loanPrincipalAndCollateralDeltasDestination: CustomOutput +} + +type ModifyBorrowTransaction { + cbor: String! +} + +input MintPreviewTokensTransactionInput { + address: String! + changeAddress: String + otherAddresses: [String!] + utxos: [String!]! +} + +type MintPreviewTokensTransaction { + cbor: String! +} + +input LiquidationTransactionInput { + txId: String! + amount: Float! + address: String! + changeAddress: String + otherAddresses: [String!] + utxos: [String!]! +} + +enum SupportedWallet { + ETERNL + BEGIN +} + +input SupplyTransactionInput { + address: String! + changeAddress: String + otherAddresses: [String!] + amount: Float! + marketId: String! + utxos: [String!]! + wallet: SupportedWallet + mintedQTokensDestination: CustomOutput +} + +type SupplyTransaction { + cbor: String! +} + +input WithdrawTransactionInput { + address: String! + changeAddress: String + otherAddresses: [String!] + amount: Float! + marketId: String! + utxos: [String!]! + wallet: SupportedWallet + withdrawnUnderlyingDestination: CustomOutput +} + +type WithdrawTransaction { + cbor: String! +} + +input BorrowTransactionInputCollateral { + id: String! + tokenName: String + amount: Float! +} + +input BorrowTransactionInput { + marketId: String! + address: String! + changeAddress: String + otherAddresses: [String!] + amount: Float! + collaterals: [BorrowTransactionInputCollateral!]! + utxos: [String!]! + principalDestination: CustomOutput +} + +type BorrowTransaction { + cbor: String! +} + +type LiqwidTransactions { + liquidation(input: LiquidationTransactionInput!): Transaction! + supply(input: SupplyTransactionInput!): Transaction! + withdraw(input: WithdrawTransactionInput!): Transaction! + borrow(input: BorrowTransactionInput!): Transaction! + modifyBorrow(input: ModifyBorrowTransactionInput!): Transaction! + mintPreviewTokens(input: MintPreviewTokensTransactionInput!): Transaction! +} + +type MarketAnalyticsPeriod { + supply: Float! + supplyInUsd: Float! + borrow: Float! + borrowInUsd: Float! + supplyPercentage: Float! + borrowPercentage: Float! + utilizationPercentage: Float! + supplyApy: Float! + borrowApy: Float! +} + +type MarketAnalytics { + marketId: String! + displayName: String! + logo: String! + current: MarketAnalyticsPeriod! + previous: MarketAnalyticsPeriod! +} + +type AnalyticsOverviewPeriod { + fromDate: String! + toDate: String! + supplyInUsd: Float! + stablecoinSupplyInUsd: Float! + borrowInUsd: Float! + stablecoinBorrowInUsd: Float! + debtRepaidInUsd: Float! + interestRepaidInUsd: Float! + interestAccruedInUsd: Float! +} + +type AnalyticsOverview { + current: AnalyticsOverviewPeriod! + previous: AnalyticsOverviewPeriod! +} + +type ProtocolRevenuePeriod { + fromDate: String! + toDate: String! + loanOriginationFeesInUsd: Float! + loanOriginationFeesMinAdaInUsd: Float! + liquidationProfitInUsd: Float! + adaStakingRewards: Int! + adaStakingRewardsInUsd: Float! + revenueFromRepaidInterestInUsd: Float! + dividendsFromRepaidInterestInUsd: Float! + polLoanInterestAccruedInUsd: Float! +} + +type ProtocolRevenue { + current: ProtocolRevenuePeriod! + previous: ProtocolRevenuePeriod! +} + +type LoanCollateral { + id: String! + tokenName: String + qTokenName: String + market: Market + asset: Asset! + + """ + The amount of the collateral in the loan, by default in USD + """ + amount(input: InCurrencyInput): Float! + amountUSD: Float! + qTokenAmount: Float! + LTV: Float! + healthFactor: Float! +} + +type Loan { + id: String! + transactionId: String! + transactionIndex: Int! + marketId: String! + publicKey: String! + market: Market! + asset: Asset! + amount(input: InCurrencyInput): Float! + + """ + Adjusted debt amount by removing the initial minimum interest set by the protocol + """ + adjustedAmount(input: InCurrencyInput): Float! + + """ + The amount of the collateral in the loan, by default in USD + """ + collateral(input: InCurrencyInput): Float! + interest: Float! + APY: Float! + LTV: Float! + healthFactor: Float! + collaterals: [LoanCollateral!]! + time: Float! +} + +enum LoanSort { + MARKET_ID + MARKET_ID_DESC + DEBT + DEBT_DESC + COLLATERAL_IN_CURRENCY + COLLATERAL_IN_CURRENCY_DESC + HEALTH_FACTOR + HEALTH_FACTOR_DESC + APY + APY_DESC +} + +enum LoanFilter { + STABLECOIN + CNT + BRIDGED + PRIME + HAS_DEBT + NO_DEBT + HAS_COLLATERAL + NO_COLLATERAL + CAN_BE_LIQUIDATED +} + +input LoansInput { + page: Int = 0 + perPage: Int = 50 + paymentKeys: [String!] + sorts: [LoanSort!] + filters: [LoanFilter!] + marketIds: [String!] + search: String +} + +type LoanPagination { + page: Int! + perPage: Int! + pagesCount: Int! + totalCount: Int! + results: [Loan!]! +} + +enum MarketSort { + ID + ID_DESC + SUPPLY + SUPPLY_DESC + SUPPLY_IN_CURRENCY + SUPPLY_IN_CURRENCY_DESC + BORROW + BORROW_DESC + BORROW_IN_CURRENCY + BORROW_IN_CURRENCY_DESC + LIQUIDITY + LIQUIDITY_DESC + LIQUIDITY_IN_CURRENCY + LIQUIDITY_IN_CURRENCY_DESC + SUPPLY_APY + SUPPLY_APY_DESC + BORROW_APY + BORROW_APY_DESC +} + +enum MarketFilter { + STABLECOIN + CNT + BRIDGED + PRIME + PUBLIC + PRIVATE +} + +type MarketCollateralParameters { + id: String! + collateral: Collateral! + maxLoanToValue: Float! + weightedMaxLoanToValue: Float! + liquidationThreshold: Float! + weightedLiquidationThreshold: Float! + liquidationPenalty: Float + liquidationProfitability: Float + collateralWeight: Float +} + +type MarketInterestModelParameters { + baseRate: Float + kinkRate: Float + utilMultiplier: Float + utilMultiplierJump: Float +} + +type MarketIncomeParameters { + """ + Protocol + """ + reserve: Float + supplier: Float + staker: Float + + """ + DAO + """ + treasury: Float +} + +type MarketParameters { + id: String! + collateralParameters: [MarketCollateralParameters] + + """ + The income parameters in percentage + """ + incomeParameters: MarketIncomeParameters! + interestModelParameters: MarketInterestModelParameters! + + """ + The maximum percentage of the market value that can be borrowed + """ + borrowCap: Float + + """ + The maximum number of tokens that can be supplied to the market + """ + supplyCap: Float + + """ + The minimum amount required for each action + """ + minValue: Float! + + """ + The minimum health factor allowed for the market + """ + minHealthFactor: Float! + + """ + The number of actions that can be performed simultaneously + """ + actionCount: Int! + + """ + The maximum number of collateral that can be used for a single loan + """ + maxCollateralCount: Int! + maxBatchTime: Float! + minBatchSize: Float! + minBatchTime: Float! +} + +type UtilizationApy { + supplyMap: [Float!]! + borrowMap: [Float!]! + supplyApy: Float! + borrowApy: Float! +} + +type MarketRegistry { + actionScriptHash: String! +} + +type Market { + id: String! + displayName: String! + symbol: String! + + """ + The asset eligible for use as collateral in the market + """ + collaterals: [Collateral!]! + + """ + The asset used as collateral in the market + """ + collateralInMarkets: [Market!]! + asset: Asset! + receiptAsset: Asset! + + """ + The total amount of supply in the market + """ + supply(input: InCurrencyInput): Float! + + """ + The total amount of borrow in the market + """ + borrow(input: InCurrencyInput): Float! + + """ + The total liquidity in the market + """ + liquidity(input: InCurrencyInput): Float! + supplyAPY: Float! + borrowAPY: Float! + lqSupplyAPY: Float! + + """ + Market utilization in percentage + """ + utilization: Float! + + """ + Exchange rate between the qToken and the asset + """ + exchangeRate: Float! + + """ + The market parameters + """ + parameters: MarketParameters! + + """ + Boolean indicating if the market is batching + """ + batching: Boolean! + batchExpired: Boolean! + + """ + Boolean indiciating if the market is paused due to an epoch transition + """ + batchEpochPause: Boolean! + + """ + The timestamp of the most recent batch + """ + lastBatch: String! + frozen: Boolean! + + """ + If this field is true, the market is private and cannot be accessed by users + """ + private: Boolean! + delisting: Boolean! + + """ + Loan origination fee percentage charged when taking out a loan from this market (decimilal representation, e.g., 1% = 0.01) + """ + loanOriginationFeePercentage: Float! + + """ + The market is considered Prime when it's a direct lending + """ + prime: Boolean! + + """ + Array of supply/borrow APY based on utilization rate (0-100%), incremented 1% at a time + Useful for plotting the APY curve + """ + utilizationApy: UtilizationApy! + registry: MarketRegistry! + + """ + The timestamp of the last update update + """ + updatedAt: String! +} + +type MarketPagination { + page: Int! + perPage: Int! + pagesCount: Int! + totalCount: Int! + results: [Market!]! +} + +input MarketInput { + id: String! +} + +input MarketsInput { + ids: [String!] + page: Int = 0 + perPage: Int = 20 + sorts: [MarketSort!] + filters: [MarketFilter!] + search: String +} + +type Collateral { + id: String! + displayName: String! + symbol: String! + market: Market + asset: Asset! + name: String! + currencySymbol: String +} + +enum AssetSort { + ID + ID_DESC +} + +enum AssetFilter { + STABLECOIN + CNT + BRIDGED +} + +input AssetInput { + id: String! +} + +input AssetsInput { + ids: [String] + page: Int = 0 + perPage: Int = 20 + sorts: [AssetSort!] + filters: [AssetFilter!] + search: String +} + +""" +Information about the asset +""" +type AssetExtra { + bridge: Bridge + maxSupply: Float + totalSupply: Float + circulatingSupply: Float + discord: String + twitter: String + facebook: String + website: String + whitepaper: String + github: String + reddit: String + medium: String + telegram: String + coingecko: String + explorer: String +} + +type Bridge { + name: String! + url: String! +} + +type Hardcap { + low: Float + high: Float +} + +type Asset { + id: String! + name: String! + symbol: String! + displayName: String! + decimals: Int! + + """ + The PolicyId of the asset + """ + currencySymbol: String! + policyId: String! + hexName: String! + + """ + Path to access the asset logo in the API + """ + logo: String + price(input: InCurrencyInput): Float! + priceUpdatedAt: String! + + """ + Price boundaries beyond which the asset's oracle price cannot move + """ + hardcap: Hardcap + markets: [Market!]! + extra: AssetExtra +} + +type AssetPagination { + page: Int! + perPage: Int! + pagesCount: Int! + totalCount: Int! + results: [Asset!]! +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b5096f5..b0304b3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,11 +71,132 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 + apps/long-short-backend: + dependencies: + '@cardano-ogmios/client': + specifier: ^6.14.0 + version: 6.14.0 + '@emurgo/cardano-message-signing-nodejs': + specifier: ^1.0.1 + version: 1.1.0 + '@fastify/cors': + specifier: ^10.0.2 + version: 10.1.0 + '@minswap/felis-build-tx': + specifier: workspace:* + version: link:../../packages/minswap-build-tx + '@minswap/felis-cip': + specifier: workspace:* + version: link:../../packages/cip + '@minswap/felis-dex-v1': + specifier: workspace:* + version: link:../../packages/minswap-dex-v1 + '@minswap/felis-dex-v2': + specifier: workspace:* + version: link:../../packages/minswap-dex-v2 + '@minswap/felis-ledger-core': + specifier: workspace:* + version: link:../../packages/ledger-core + '@minswap/felis-ledger-utils': + specifier: workspace:* + version: link:../../packages/ledger-utils + '@minswap/felis-lending-market': + specifier: workspace:* + version: link:../../packages/minswap-lending-market + '@minswap/felis-tx-builder': + specifier: workspace:* + version: link:../../packages/tx-builder + '@minswap/tiny-invariant': + specifier: ^1.2.0 + version: 1.2.0 + '@sinclair/typebox': + specifier: ^0.34.33 + version: 0.34.48 + '@types/bun': + specifier: ^1.3.5 + version: 1.3.8 + bignumber.js: + specifier: ^9.1.2 + version: 9.3.1 + bip39: + specifier: ^3.1.0 + version: 3.1.0 + crypto-js: + specifier: ^4.2.0 + version: 4.2.0 + exponential-backoff: + specifier: ^3.1.3 + version: 3.1.3 + fastify: + specifier: ^5.2.2 + version: 5.7.4 + ioredis: + specifier: ^5.9.0 + version: 5.9.2 + kysely: + specifier: ^0.28.11 + version: 0.28.11 + p-timeout: + specifier: ^7.0.1 + version: 7.0.1 + pg: + specifier: ^8.16.3 + version: 8.18.0 + remeda: + specifier: ^2.33.1 + version: 2.33.4 + socket.io-client: + specifier: ^4.8.3 + version: 4.8.3 + devDependencies: + '@cardano-ogmios/schema': + specifier: ^6.11.0 + version: 6.14.0 + '@repo/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@repo/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config + '@types/crypto-js': + specifier: ^4.2.2 + version: 4.2.2 + '@types/json-bigint': + specifier: ^1.0.4 + version: 1.0.4 + '@types/node': + specifier: ^22.15.3 + version: 22.15.3 + '@types/pg': + specifier: ^8.16.0 + version: 8.16.0 + dpdm: + specifier: ^3.14.0 + version: 3.14.0 + eslint: + specifier: ^9.31.0 + version: 9.31.0(jiti@2.6.1) + kysely-codegen: + specifier: ^0.19.0 + version: 0.19.0(kysely@0.28.11)(pg@8.18.0)(typescript@5.8.2) + kysely-ctl: + specifier: ^0.19.0 + version: 0.19.0(kysely@0.28.11)(typescript@5.8.2) + tsx: + specifier: ^4.19.4 + version: 4.20.3 + typescript: + specifier: 5.8.2 + version: 5.8.2 + vitest: + specifier: ^3.2.4 + version: 3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) + apps/web: dependencies: '@ant-design/icons': @@ -144,7 +265,7 @@ importers: version: 19.2.3(@types/react@19.2.7) eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 @@ -181,13 +302,13 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.15.3)(tsx@4.20.3) + version: 3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) packages/eslint-config: devDependencies: @@ -199,22 +320,22 @@ importers: version: 15.4.2 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) eslint-config-prettier: specifier: ^10.1.1 - version: 10.1.1(eslint@9.31.0) + version: 10.1.1(eslint@9.31.0(jiti@2.6.1)) eslint-plugin-only-warn: specifier: ^1.1.0 version: 1.1.0 eslint-plugin-react: specifier: ^7.37.5 - version: 7.37.5(eslint@9.31.0) + version: 7.37.5(eslint@9.31.0(jiti@2.6.1)) eslint-plugin-react-hooks: specifier: ^5.2.0 - version: 5.2.0(eslint@9.31.0) + version: 5.2.0(eslint@9.31.0(jiti@2.6.1)) eslint-plugin-turbo: specifier: ^2.5.0 - version: 2.5.0(eslint@9.31.0)(turbo@2.5.5) + version: 2.5.0(eslint@9.31.0(jiti@2.6.1))(turbo@2.5.5) globals: specifier: ^16.3.0 version: 16.3.0 @@ -223,7 +344,7 @@ importers: version: 5.8.2 typescript-eslint: specifier: ^8.37.0 - version: 8.37.0(eslint@9.31.0)(typescript@5.8.2) + version: 8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2) packages/felis: dependencies: @@ -290,7 +411,7 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 @@ -345,7 +466,7 @@ importers: version: 5.8.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.15.3)(tsx@4.20.3) + version: 3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) packages/ledger-utils: dependencies: @@ -403,7 +524,7 @@ importers: version: 5.8.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.15.3)(tsx@4.20.3) + version: 3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) packages/minswap-build-tx: dependencies: @@ -443,13 +564,13 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.15.3)(tsx@4.20.3) + version: 3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) packages/minswap-dex-v1: dependencies: @@ -480,7 +601,7 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 @@ -514,13 +635,13 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.15.3)(tsx@4.20.3) + version: 3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) packages/minswap-lending-market: dependencies: @@ -575,7 +696,7 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 @@ -627,7 +748,7 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) fast-check: specifier: ^3.23.2 version: 3.23.2 @@ -636,7 +757,7 @@ importers: version: 5.8.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.15.3)(tsx@4.20.3) + version: 3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) packages/provider: dependencies: @@ -673,7 +794,7 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 @@ -710,7 +831,7 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 @@ -744,13 +865,13 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.15.3)(tsx@4.20.3) + version: 3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) packages/sundaeswap-v3: dependencies: @@ -781,7 +902,7 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 @@ -842,13 +963,13 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@22.15.3)(tsx@4.20.3) + version: 3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) packages/tx-builder: dependencies: @@ -891,7 +1012,7 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 @@ -924,7 +1045,7 @@ importers: version: 19.2.3(@types/react@19.2.7) eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.3 version: 5.8.3 @@ -962,7 +1083,7 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 @@ -999,7 +1120,7 @@ importers: version: 3.14.0 eslint: specifier: ^9.31.0 - version: 9.31.0 + version: 9.31.0(jiti@2.6.1) typescript: specifier: 5.8.2 version: 5.8.2 @@ -1049,6 +1170,14 @@ packages: peerDependencies: react: '>=16.9.0' + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + '@babel/runtime@7.28.4': resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==} engines: {node: '>=6.9.0'} @@ -1130,6 +1259,9 @@ packages: '@emotion/unitless@0.7.5': resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==} + '@emurgo/cardano-message-signing-nodejs@1.1.0': + resolution: {integrity: sha512-PQRc8K8wZshEdmQenNUzVtiI8oJNF/1uAnBhidee5C4o1l2mDLOW+ur46HWHIFKQ6x8mSJTllcjMscHgzju0gQ==} + '@emurgo/cardano-serialization-lib-browser@13.2.1': resolution: {integrity: sha512-7RfX1gI16Vj2DgCp/ZoXqyLAakWo6+X95ku/rYGbVzuS/1etrlSiJmdbmdm+eYmszMlGQjrtOJQeVLXoj4L/Ag==} @@ -1330,6 +1462,27 @@ packages: resolution: {integrity: sha512-1+WqvgNMhmlAambTvT3KPtCl/Ibr68VldY2XY40SL1CE0ZXiakFR/cbTspaF5HsnpDMvcYYoJHfl4980NBjGag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@fastify/ajv-compiler@4.0.5': + resolution: {integrity: sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==} + + '@fastify/cors@10.1.0': + resolution: {integrity: sha512-MZyBCBJtII60CU9Xme/iE4aEy8G7QpzGR8zkdXZkDFt7ElEMachbE61tfhAG/bvSaULlqlf0huMT12T7iqEmdQ==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.0.3': + resolution: {integrity: sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==} + + '@fastify/forwarded@3.0.1': + resolution: {integrity: sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + '@humanfs/core@0.19.1': resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==} engines: {node: '>=18.18.0'} @@ -1487,6 +1640,9 @@ packages: cpu: [x64] os: [win32] + '@ioredis/commands@1.5.0': + resolution: {integrity: sha512-eUgLqrMf8nJkZxT24JvVRrQya1vZkQh8BBeYNwGDqa5I0VUi8ACx7uFvAaLxintokpTenkK6DASvo/bvNbBGow==} + '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -1573,6 +1729,9 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1732,6 +1891,9 @@ packages: cpu: [x64] os: [win32] + '@sinclair/typebox@0.34.48': + resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} @@ -1747,9 +1909,15 @@ packages: '@swc/helpers@0.5.15': resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==} + '@types/bun@1.3.8': + resolution: {integrity: sha512-3LvWJ2q5GerAXYxO2mffLTqOzEu5qnhEAlh48Vnu8WQfnmSwbgagjGZV6BoHKJztENYEDn6QmVd949W4uESRJA==} + '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} + '@types/crypto-js@4.2.2': + resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} @@ -1765,6 +1933,9 @@ packages: '@types/node@22.15.3': resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==} + '@types/pg@8.16.0': + resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -1861,6 +2032,9 @@ packages: '@vitest/utils@3.2.4': resolution: {integrity: sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==} + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1871,9 +2045,20 @@ packages: engines: {node: '>=0.4.0'} hasBin: true + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@8.17.1: + resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} + ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -1882,6 +2067,10 @@ packages: resolution: {integrity: sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==} engines: {node: '>=12'} + ansi-styles@3.2.1: + resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} + engines: {node: '>=4'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} @@ -1935,10 +2124,17 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + available-typed-arrays@1.0.7: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} + avvio@9.1.0: + resolution: {integrity: sha512-fYASnYi600CsH/j9EQov7lECAniYiBFiiAtBNuZYLA2leLe9qOvZzqYHFjtIj6gD2VMoMLP14834LFWvr4IfDw==} + balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1982,6 +2178,17 @@ packages: buffer@6.0.3: resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bun-types@1.3.8: + resolution: {integrity: sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q==} + + c12@3.3.3: + resolution: {integrity: sha512-750hTRvgBy5kcMNPdh95Qo+XUBeGo8C7nsKSmedDmaQI+E0r82DwHeM6vBewDe4rGFbnxoa4V9pw+sPh5+Iz8Q==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2013,6 +2220,10 @@ packages: resolution: {integrity: sha512-48af6xm9gQK8rhIcOxWwdGzIervm8BVTin+yRp9HEvU20BtVZ2lBywlIJBzwaDtvo0FvjeL7QdCADoUoqIbV3A==} engines: {node: '>=18'} + chalk@2.4.2: + resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} + engines: {node: '>=4'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2021,6 +2232,16 @@ packages: resolution: {integrity: sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==} engines: {node: '>= 16'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + citty@0.1.6: + resolution: {integrity: sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==} + + citty@0.2.0: + resolution: {integrity: sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==} + classnames@2.5.1: resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} @@ -2043,10 +2264,20 @@ packages: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} + cluster-key-slot@1.1.2: + resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==} + engines: {node: '>=0.10.0'} + + color-convert@1.9.3: + resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} + color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} + color-name@1.1.3: + resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} + color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} @@ -2056,9 +2287,29 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + confbox@0.2.2: + resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + copy-to-clipboard@3.3.3: resolution: {integrity: sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==} + cosmiconfig@9.0.0: + resolution: {integrity: sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==} + engines: {node: '>=14'} + peerDependencies: + typescript: '>=4.9.5' + peerDependenciesMeta: + typescript: + optional: true + cross-fetch@3.2.0: resolution: {integrity: sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==} @@ -2066,6 +2317,9 @@ packages: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} + crypto-js@4.2.0: + resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==} + csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} @@ -2114,18 +2368,48 @@ packages: resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} engines: {node: '>= 0.4'} + defu@6.1.4: + resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==} + + denque@2.1.0: + resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} + engines: {node: '>=0.10'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + detect-libc@2.1.2: resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} + diff@3.5.1: + resolution: {integrity: sha512-Z3u54A8qGyqFOSr2pk0ijYs8mOE9Qz8kTvtKeBI+upoG9j04Sq+oI7W8zAJiQybDcESET8/uIdHzs0p3k4fZlw==} + engines: {node: '>=0.3.1'} + doctrine@2.1.0: resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} engines: {node: '>=0.10.0'} + dotenv-expand@12.0.3: + resolution: {integrity: sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==} + engines: {node: '>=12'} + dotenv@16.0.3: resolution: {integrity: sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==} engines: {node: '>=12'} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.2.3: + resolution: {integrity: sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==} + engines: {node: '>=12'} + dpdm@3.14.0: resolution: {integrity: sha512-YJzsFSyEtj88q5eTELg3UWU7TVZkG1dpbF4JDQ3t1b07xuzXmdoGeSz9TKOke1mUuOpWlk4q+pBh+aHzD6GBTg==} hasBin: true @@ -2150,6 +2434,13 @@ packages: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + es-abstract@1.24.0: resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==} engines: {node: '>= 0.4'} @@ -2194,6 +2485,10 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-string-regexp@1.0.5: + resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} + engines: {node: '>=0.8.0'} + escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} @@ -2275,10 +2570,19 @@ packages: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + exsolve@1.0.8: + resolution: {integrity: sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==} + fast-check@3.23.2: resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} engines: {node: '>=8.0.0'} + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2293,9 +2597,24 @@ packages: fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-json-stringify@6.2.0: + resolution: {integrity: sha512-Eaf/KNIDwHkzfyeQFNfLXJnQ7cl1XQI3+zRqmPlvtkMigbXnAcasTrvJQmquBSxKfFGeRA6PFog8t+hFmpDoWw==} + fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.0: + resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} + + fastify-plugin@5.1.0: + resolution: {integrity: sha512-FAIDA8eovSt5qcDgcBvDuX/v0Cjz0ohGhENZ/wpc3y+oZCY2afZ9Baqql3g/lC+OHRnciQol4ww7tuthOb9idw==} + + fastify@5.7.4: + resolution: {integrity: sha512-e6l5NsRdaEP8rdD8VR0ErJASeyaRbzXYpmkrpr2SuvuMq6Si3lvsaVy5C+7gLanEkvjpMDzBXWE5HPeb/hgTxA==} + fastq@1.19.1: resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==} @@ -2316,6 +2635,10 @@ packages: resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} engines: {node: '>=8'} + find-my-way@9.4.0: + resolution: {integrity: sha512-5Ye4vHsypZRYtS01ob/iwHzGRUDELlsoCftI/OZFhcLs1M0tkGPcXldE80TAZC5yYuJMBPJQQ43UHlqbJWiX2w==} + engines: {node: '>=20'} + find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -2339,6 +2662,9 @@ packages: resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} engines: {node: '>=14.14'} + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -2373,6 +2699,14 @@ packages: get-tsconfig@4.10.1: resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + giget@2.0.0: + resolution: {integrity: sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==} + hasBin: true + + git-diff@2.0.6: + resolution: {integrity: sha512-/Iu4prUrydE3Pb3lCBMbcSNIf81tgGt0W1ZwknnyF62t3tHmtiJTRj0f+1ZIhp3+Rh0ktz1pJVoa7ZXUCskivA==} + engines: {node: '>= 4.8.0'} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2385,6 +2719,10 @@ packages: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} hasBin: true + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Glob versions prior to v9 are no longer supported + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -2411,6 +2749,10 @@ packages: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} + has-flag@3.0.0: + resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} + engines: {node: '>=4'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} @@ -2453,6 +2795,10 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inherits@2.0.4: resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} @@ -2460,10 +2806,25 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} + interpret@1.4.0: + resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==} + engines: {node: '>= 0.10'} + + ioredis@5.9.2: + resolution: {integrity: sha512-tAAg/72/VxOUW7RQSX1pIxJVucYKcjFjfvj60L57jrZpYCHC3XN0WCQ3sNYL4Gmvv+7GPvTAjc+KSdeNuE8oWQ==} + engines: {node: '>=12.22.0'} + + ipaddr.js@2.3.0: + resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + engines: {node: '>= 10'} + is-array-buffer@3.0.5: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -2590,6 +2951,10 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + jiti@2.6.1: + resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + hasBin: true + jotai@2.15.1: resolution: {integrity: sha512-yHT1HAZ3ba2Q8wgaUQ+xfBzEtcS8ie687I8XVCBinfg4bNniyqLIN+utPXWKQE93LMF5fPbQSVRZqgpcN5yd6Q==} engines: {node: '>=12.20.0'} @@ -2624,9 +2989,18 @@ packages: json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -2643,14 +3017,82 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + kysely-codegen@0.19.0: + resolution: {integrity: sha512-ZpdQQnpfY0kh45CA6yPA9vdFsBE+b06Fx7QVcbL5rX//yjbA0yYGZGhnH7GTd4P4BY/HIv5uAfuOD83JVZf95w==} + engines: {node: '>=20.0.0'} + hasBin: true + peerDependencies: + '@libsql/kysely-libsql': '>=0.3.0 <0.5.0' + '@tediousjs/connection-string': '>=0.5.0 <0.6.0' + better-sqlite3: '>=7.6.2 <13.0.0' + kysely: '>=0.27.0 <1.0.0' + kysely-bun-sqlite: '>=0.3.2 <1.0.0' + kysely-bun-worker: '>=1.2.0 <2.0.0' + mysql2: '>=2.3.3 <4.0.0' + pg: '>=8.8.0 <9.0.0' + tarn: '>=3.0.0 <4.0.0' + tedious: '>=18.0.0 <20.0.0' + peerDependenciesMeta: + '@libsql/kysely-libsql': + optional: true + '@tediousjs/connection-string': + optional: true + better-sqlite3: + optional: true + kysely-bun-sqlite: + optional: true + kysely-bun-worker: + optional: true + mysql2: + optional: true + pg: + optional: true + tarn: + optional: true + tedious: + optional: true + + kysely-ctl@0.19.0: + resolution: {integrity: sha512-89hzOd1cy/H063jB2E9wYHq+uKYpaHv6Mb5RiNFpRZL6BYCah9ncsdl3x5b52eirxry4UyWSmGNN3sFv+gK+ig==} + engines: {node: '>=20'} + hasBin: true + peerDependencies: + kysely: '>=0.18.1 <0.29.0' + kysely-neon: ^2 + kysely-postgres-js: ^2 || ^3 + kysely-prisma-postgres: ^0.1 + peerDependenciesMeta: + kysely-neon: + optional: true + kysely-postgres-js: + optional: true + kysely-prisma-postgres: + optional: true + + kysely@0.28.11: + resolution: {integrity: sha512-zpGIFg0HuoC893rIjYX1BETkVWdDnzTzF5e0kWXJFg5lE0k1/LfNWBejrcnOFu8Q2Rfq/hTDTU7XLUM8QOrpzg==} + engines: {node: '>=20.0.0'} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} engines: {node: '>=10'} + lodash.defaults@4.2.0: + resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + + lodash.isarguments@3.1.0: + resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} @@ -2661,6 +3103,10 @@ packages: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} engines: {node: '>=10'} + loglevel@1.9.2: + resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} + engines: {node: '>= 0.6.0'} + loose-envify@1.4.0: resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true @@ -2697,10 +3143,16 @@ packages: resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.2: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + mnemonist@0.40.0: + resolution: {integrity: sha512-kdd8AFNig2AD5Rkih7EPCXhu/iMvwevQFX/uEiGhZyPZi7fHqOoF4V4kHLpCfysxXMgQ4B52kdPMCwARshKvEg==} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -2733,6 +3185,9 @@ packages: sass: optional: true + node-fetch-native@1.6.7: + resolution: {integrity: sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==} + node-fetch@2.7.0: resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} engines: {node: 4.x || >=6.0.0} @@ -2746,6 +3201,11 @@ packages: resolution: {integrity: sha512-l2NNj07e9afPnhAhvgVrCD/oy2Ai1yfLpuo3EpiO1jFTsB4sFz6oIfAfSZyQzVpkZQ9xS8ZS5g1jCBgq4Hwo0g==} engines: {node: '>=12.19'} + nypm@0.6.4: + resolution: {integrity: sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==} + engines: {node: '>=18'} + hasBin: true + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2774,6 +3234,22 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obliterator@2.0.5: + resolution: {integrity: sha512-42CPE9AhahZRsMNslczq0ctAEtqk8Eka26QofnqC346BZdHDySk3LWka23LI7ULIw11NmltpiLagIq8gBozxTw==} + + ofetch@1.5.1: + resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==} + + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} engines: {node: '>=6'} @@ -2798,6 +3274,10 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + p-timeout@7.0.1: + resolution: {integrity: sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==} + engines: {node: '>=20'} + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -2805,10 +3285,18 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + path-key@3.1.1: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} @@ -2827,6 +3315,43 @@ packages: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + + pg-cloudflare@1.3.0: + resolution: {integrity: sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==} + + pg-connection-string@2.11.0: + resolution: {integrity: sha512-kecgoJwhOpxYU21rZjULrmrBJ698U2RxXofKVzOn5UDj61BPj/qMb7diYUR1nLScCDbrztQFl1TaQZT0t1EtzQ==} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-pool@3.11.0: + resolution: {integrity: sha512-MJYfvHwtGp870aeusDh+hg9apvOe2zmpZJpyt+BMtzUWlVqbhFmMK6bOBXLBUPd7iRtIF9fZplDc7KrPN3PN7w==} + peerDependencies: + pg: '>=8.0' + + pg-protocol@1.11.0: + resolution: {integrity: sha512-pfsxk2M9M3BuGgDOfuy37VNRRX3jmKgMjcvAcWqNDpZSf4cUmv8HSOl5ViRQFsfARFn0KuUQTgLxVMbNq5NW3g==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + pg@8.18.0: + resolution: {integrity: sha512-xqrUDL1b9MbkydY/s+VZ6v+xiMUmOUk7SS9d/1kpyQxoJ6U9AO1oIJyUWVZojbfe5Cc/oluutcgFG4L9RDP1iQ==} + engines: {node: '>= 16.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + + pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2838,6 +3363,23 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.0: + resolution: {integrity: sha512-0GNPNzHXBKw6U/InGe79A3Crzyk9bcSyObF9/Gfo9DLEf5qj5RF50RSjsu0W1rZ6ZqRGdzDFCRBQvi9/rSGPtA==} + hasBin: true + + pkg-types@2.3.0: + resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + + pluralize@8.0.0: + resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==} + engines: {node: '>=4'} + possible-typed-array-names@1.1.0: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} @@ -2850,10 +3392,32 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.0.0: + resolution: {integrity: sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==} + prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} @@ -2867,6 +3431,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + rc-cascader@3.34.0: resolution: {integrity: sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==} peerDependencies: @@ -3095,6 +3662,9 @@ packages: react: '>=16.9.0' react-dom: '>=16.9.0' + rc9@2.1.2: + resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} + react-dom@19.2.3: resolution: {integrity: sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==} peerDependencies: @@ -3114,6 +3684,26 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + rechoir@0.6.2: + resolution: {integrity: sha512-HFM8rkZ+i3zrV+4LQjwQ0W+ez98pApMGM3HUrN04j3CqzPOzl9nmP15Y8YXNm8QHGv/eacOVEjqhmWpkRV0NAw==} + engines: {node: '>= 0.10'} + + redis-errors@1.2.0: + resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==} + engines: {node: '>=4'} + + redis-parser@3.0.0: + resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==} + engines: {node: '>=4'} + reflect.getprototypeof@1.0.10: resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} engines: {node: '>= 0.4'} @@ -3125,10 +3715,17 @@ packages: remeda@2.28.0: resolution: {integrity: sha512-943nIauNk4xBh/2DNcsZCc3DRpKncNXnNjtWlqfhM72FvD4bKuOXWSfuY0+k7yf8/Ob3+QzXtxLeobLoMn/INA==} + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + require-directory@2.1.1: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -3139,6 +3736,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.11: + resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} + engines: {node: '>= 0.4'} + hasBin: true + resolve@2.0.0-next.5: resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==} hasBin: true @@ -3147,10 +3749,17 @@ packages: resolution: {integrity: sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==} engines: {node: '>=8'} + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + reusify@1.1.0: resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + rollup@4.46.3: resolution: {integrity: sha512-RZn2XTjXb8t5g13f5YclGoilU/kwT696DIkY3sywjdZidNSi3+vseaQov7D7BZXVJCPv3pDWUN69C78GGbXsKw==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -3174,12 +3783,22 @@ packages: resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} engines: {node: '>= 0.4'} + safe-regex2@5.0.0: + resolution: {integrity: sha512-YwJwe5a51WlK7KbOJREPdjNrpViQBI3p4T50lfwPuDhZnE3XGVTlGvi+aolc5+RvxDD6bnUmjVsU9n1eboLUYw==} + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -3194,6 +3813,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + set-function-length@1.2.2: resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} engines: {node: '>= 0.4'} @@ -3221,6 +3843,15 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shelljs.exec@1.1.8: + resolution: {integrity: sha512-vFILCw+lzUtiwBAHV8/Ex8JsFjelFMdhONIsgKNLgTzeRckp2AOYRQtHJE/9LhNvdMmE27AGtzWx0+DHpwIwSw==} + engines: {node: '>= 4.0.0'} + + shelljs@0.8.5: + resolution: {integrity: sha512-TiwcRcrkhHvbrZbnRcFYMLl30Dfov3HKqzp5tO5b4pt6G/SezKcYhmDg15zXVBswHmctSAQKznqNW2LO5tTDow==} + engines: {node: '>=4'} + hasBin: true + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -3255,13 +3886,23 @@ packages: resolution: {integrity: sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==} engines: {node: '>=10.0.0'} + sonic-boom@4.2.0: + resolution: {integrity: sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww==} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + standard-as-callback@2.1.0: + resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + std-env@3.9.0: resolution: {integrity: sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==} @@ -3333,6 +3974,10 @@ packages: stylis@4.3.6: resolution: {integrity: sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==} + supports-color@5.5.0: + resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} + engines: {node: '>=4'} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -3341,6 +3986,10 @@ packages: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} engines: {node: '>= 0.4'} + thread-stream@4.0.0: + resolution: {integrity: sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==} + engines: {node: '>=20'} + throttle-debounce@5.0.2: resolution: {integrity: sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==} engines: {node: '>=12.22'} @@ -3351,6 +4000,10 @@ packages: tinyexec@0.3.2: resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyglobby@0.2.14: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} @@ -3371,6 +4024,10 @@ packages: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} + toad-cache@3.7.0: + resolution: {integrity: sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==} + engines: {node: '>=12'} + toggle-selection@1.0.6: resolution: {integrity: sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==} @@ -3387,6 +4044,16 @@ packages: resolution: {integrity: sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==} engines: {node: '>=14.0.0'} + tsconfck@3.1.6: + resolution: {integrity: sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==} + engines: {node: ^18 || >=20} + hasBin: true + peerDependencies: + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -3470,6 +4137,9 @@ packages: engines: {node: '>=14.17'} hasBin: true + ufo@1.6.3: + resolution: {integrity: sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -3607,6 +4277,9 @@ packages: resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} engines: {node: '>=12'} + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@7.5.10: resolution: {integrity: sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==} engines: {node: '>=8.3.0'} @@ -3635,6 +4308,10 @@ packages: resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3651,6 +4328,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + snapshots: '@ant-design/colors@7.2.1': @@ -3710,6 +4390,14 @@ snapshots: resize-observer-polyfill: 1.5.1 throttle-debounce: 5.0.2 + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/helper-validator-identifier@7.28.5': {} + '@babel/runtime@7.28.4': {} '@biomejs/biome@2.1.4': @@ -3779,6 +4467,8 @@ snapshots: '@emotion/unitless@0.7.5': {} + '@emurgo/cardano-message-signing-nodejs@1.1.0': {} + '@emurgo/cardano-serialization-lib-browser@13.2.1': {} '@emurgo/cardano-serialization-lib-nodejs@13.2.1': {} @@ -3861,9 +4551,9 @@ snapshots: '@esbuild/win32-x64@0.25.8': optional: true - '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0)': + '@eslint-community/eslint-utils@4.7.0(eslint@9.31.0(jiti@2.6.1))': dependencies: - eslint: 9.31.0 + eslint: 9.31.0(jiti@2.6.1) eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.1': {} @@ -3905,6 +4595,34 @@ snapshots: '@eslint/core': 0.15.1 levn: 0.4.1 + '@fastify/ajv-compiler@4.0.5': + dependencies: + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + + '@fastify/cors@10.1.0': + dependencies: + fastify-plugin: 5.1.0 + mnemonist: 0.40.0 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.0.3': + dependencies: + fast-json-stringify: 6.2.0 + + '@fastify/forwarded@3.0.1': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.1 + ipaddr.js: 2.3.0 + '@humanfs/core@0.19.1': {} '@humanfs/node@0.16.6': @@ -4015,6 +4733,8 @@ snapshots: '@img/sharp-win32-x64@0.34.5': optional: true + '@ioredis/commands@1.5.0': {} + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4076,6 +4796,8 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 + '@pinojs/redact@0.4.0': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -4207,6 +4929,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.46.3': optional: true + '@sinclair/typebox@0.34.48': {} + '@socket.io/component-emitter@3.1.2': {} '@stricahq/cbors@1.0.2': @@ -4233,10 +4957,16 @@ snapshots: dependencies: tslib: 2.8.1 + '@types/bun@1.3.8': + dependencies: + bun-types: 1.3.8 + '@types/chai@5.2.2': dependencies: '@types/deep-eql': 4.0.2 + '@types/crypto-js@4.2.2': {} + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} @@ -4249,6 +4979,12 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/pg@8.16.0': + dependencies: + '@types/node': 22.15.3 + pg-protocol: 1.11.0 + pg-types: 2.2.0 + '@types/react-dom@19.2.3(@types/react@19.2.7)': dependencies: '@types/react': 19.2.7 @@ -4257,15 +4993,15 @@ snapshots: dependencies: csstype: 3.2.3 - '@typescript-eslint/eslint-plugin@8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0)(typescript@5.8.2))(eslint@9.31.0)(typescript@5.8.2)': + '@typescript-eslint/eslint-plugin@8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2))(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2)': dependencies: '@eslint-community/regexpp': 4.12.1 - '@typescript-eslint/parser': 8.37.0(eslint@9.31.0)(typescript@5.8.2) + '@typescript-eslint/parser': 8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2) '@typescript-eslint/scope-manager': 8.37.0 - '@typescript-eslint/type-utils': 8.37.0(eslint@9.31.0)(typescript@5.8.2) - '@typescript-eslint/utils': 8.37.0(eslint@9.31.0)(typescript@5.8.2) + '@typescript-eslint/type-utils': 8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2) + '@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2) '@typescript-eslint/visitor-keys': 8.37.0 - eslint: 9.31.0 + eslint: 9.31.0(jiti@2.6.1) graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -4274,14 +5010,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.37.0(eslint@9.31.0)(typescript@5.8.2)': + '@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2)': dependencies: '@typescript-eslint/scope-manager': 8.37.0 '@typescript-eslint/types': 8.37.0 '@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.2) '@typescript-eslint/visitor-keys': 8.37.0 debug: 4.4.1 - eslint: 9.31.0 + eslint: 9.31.0(jiti@2.6.1) typescript: 5.8.2 transitivePeerDependencies: - supports-color @@ -4304,13 +5040,13 @@ snapshots: dependencies: typescript: 5.8.2 - '@typescript-eslint/type-utils@8.37.0(eslint@9.31.0)(typescript@5.8.2)': + '@typescript-eslint/type-utils@8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2)': dependencies: '@typescript-eslint/types': 8.37.0 '@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.2) - '@typescript-eslint/utils': 8.37.0(eslint@9.31.0)(typescript@5.8.2) + '@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2) debug: 4.4.1 - eslint: 9.31.0 + eslint: 9.31.0(jiti@2.6.1) ts-api-utils: 2.1.0(typescript@5.8.2) typescript: 5.8.2 transitivePeerDependencies: @@ -4334,13 +5070,13 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.37.0(eslint@9.31.0)(typescript@5.8.2)': + '@typescript-eslint/utils@8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2)': dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.6.1)) '@typescript-eslint/scope-manager': 8.37.0 '@typescript-eslint/types': 8.37.0 '@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.2) - eslint: 9.31.0 + eslint: 9.31.0(jiti@2.6.1) typescript: 5.8.2 transitivePeerDependencies: - supports-color @@ -4358,13 +5094,13 @@ snapshots: chai: 5.3.1 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.1.3(@types/node@22.15.3)(tsx@4.20.3))': + '@vitest/mocker@3.2.4(vite@7.1.3(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.17 optionalDependencies: - vite: 7.1.3(@types/node@22.15.3)(tsx@4.20.3) + vite: 7.1.3(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) '@vitest/pretty-format@3.2.4': dependencies: @@ -4392,12 +5128,18 @@ snapshots: loupe: 3.2.0 tinyrainbow: 2.0.0 + abstract-logging@2.0.1: {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 acorn@8.15.0: {} + ajv-formats@3.0.1(ajv@8.17.1): + optionalDependencies: + ajv: 8.17.1 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -4405,10 +5147,21 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ajv@8.17.1: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.0 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + ansi-regex@5.0.1: {} ansi-regex@6.1.0: {} + ansi-styles@3.2.1: + dependencies: + color-convert: 1.9.3 + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 @@ -4536,10 +5289,17 @@ snapshots: async-function@1.0.0: {} + atomic-sleep@1.0.0: {} + available-typed-arrays@1.0.7: dependencies: possible-typed-array-names: 1.1.0 + avvio@9.1.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.19.1 + balanced-match@1.0.2: {} base-x@4.0.1: {} @@ -4589,6 +5349,25 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + bun-types@1.3.8: + dependencies: + '@types/node': 22.15.3 + + c12@3.3.3: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.2 + defu: 6.1.4 + dotenv: 17.2.3 + exsolve: 1.0.8 + giget: 2.0.0 + jiti: 2.6.1 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.0 + rc9: 2.1.2 + cac@6.7.14: {} call-bind-apply-helpers@1.0.2: @@ -4624,6 +5403,12 @@ snapshots: loupe: 3.2.0 pathval: 2.0.1 + chalk@2.4.2: + dependencies: + ansi-styles: 3.2.1 + escape-string-regexp: 1.0.5 + supports-color: 5.5.0 + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -4631,6 +5416,16 @@ snapshots: check-error@2.1.1: {} + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + citty@0.1.6: + dependencies: + consola: 3.4.2 + + citty@0.2.0: {} + classnames@2.5.1: {} cli-cursor@3.1.0: @@ -4649,20 +5444,43 @@ snapshots: clone@1.0.4: {} + cluster-key-slot@1.1.2: {} + + color-convert@1.9.3: + dependencies: + color-name: 1.1.3 + color-convert@2.0.1: dependencies: color-name: 1.1.4 + color-name@1.1.3: {} + color-name@1.1.4: {} compute-scroll-into-view@3.1.1: {} concat-map@0.0.1: {} + confbox@0.2.2: {} + + consola@3.4.2: {} + + cookie@1.1.1: {} + copy-to-clipboard@3.3.3: dependencies: toggle-selection: 1.0.6 + cosmiconfig@9.0.0(typescript@5.8.2): + dependencies: + env-paths: 2.2.1 + import-fresh: 3.3.1 + js-yaml: 4.1.0 + parse-json: 5.2.0 + optionalDependencies: + typescript: 5.8.2 + cross-fetch@3.2.0: dependencies: node-fetch: 2.7.0 @@ -4675,6 +5493,8 @@ snapshots: shebang-command: 2.0.0 which: 2.0.2 + crypto-js@4.2.0: {} + csstype@3.1.3: {} csstype@3.2.3: {} @@ -4723,15 +5543,33 @@ snapshots: has-property-descriptors: 1.0.2 object-keys: 1.1.1 + defu@6.1.4: {} + + denque@2.1.0: {} + + dequal@2.0.3: {} + + destr@2.0.5: {} + detect-libc@2.1.2: optional: true + diff@3.5.1: {} + doctrine@2.1.0: dependencies: esutils: 2.0.3 + dotenv-expand@12.0.3: + dependencies: + dotenv: 16.6.1 + dotenv@16.0.3: {} + dotenv@16.6.1: {} + + dotenv@17.2.3: {} + dpdm@3.14.0: dependencies: chalk: 4.1.2 @@ -4768,6 +5606,12 @@ snapshots: engine.io-parser@5.2.3: {} + env-paths@2.2.1: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + es-abstract@1.24.0: dependencies: array-buffer-byte-length: 1.0.2 @@ -4902,19 +5746,21 @@ snapshots: escalade@3.2.0: {} + escape-string-regexp@1.0.5: {} + escape-string-regexp@4.0.0: {} - eslint-config-prettier@10.1.1(eslint@9.31.0): + eslint-config-prettier@10.1.1(eslint@9.31.0(jiti@2.6.1)): dependencies: - eslint: 9.31.0 + eslint: 9.31.0(jiti@2.6.1) eslint-plugin-only-warn@1.1.0: {} - eslint-plugin-react-hooks@5.2.0(eslint@9.31.0): + eslint-plugin-react-hooks@5.2.0(eslint@9.31.0(jiti@2.6.1)): dependencies: - eslint: 9.31.0 + eslint: 9.31.0(jiti@2.6.1) - eslint-plugin-react@7.37.5(eslint@9.31.0): + eslint-plugin-react@7.37.5(eslint@9.31.0(jiti@2.6.1)): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -4922,7 +5768,7 @@ snapshots: array.prototype.tosorted: 1.1.4 doctrine: 2.1.0 es-iterator-helpers: 1.2.1 - eslint: 9.31.0 + eslint: 9.31.0(jiti@2.6.1) estraverse: 5.3.0 hasown: 2.0.2 jsx-ast-utils: 3.3.5 @@ -4936,10 +5782,10 @@ snapshots: string.prototype.matchall: 4.0.12 string.prototype.repeat: 1.0.0 - eslint-plugin-turbo@2.5.0(eslint@9.31.0)(turbo@2.5.5): + eslint-plugin-turbo@2.5.0(eslint@9.31.0(jiti@2.6.1))(turbo@2.5.5): dependencies: dotenv: 16.0.3 - eslint: 9.31.0 + eslint: 9.31.0(jiti@2.6.1) turbo: 2.5.5 eslint-scope@8.4.0: @@ -4951,9 +5797,9 @@ snapshots: eslint-visitor-keys@4.2.1: {} - eslint@9.31.0: + eslint@9.31.0(jiti@2.6.1): dependencies: - '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0) + '@eslint-community/eslint-utils': 4.7.0(eslint@9.31.0(jiti@2.6.1)) '@eslint-community/regexpp': 4.12.1 '@eslint/config-array': 0.21.0 '@eslint/config-helpers': 0.3.0 @@ -4988,6 +5834,8 @@ snapshots: minimatch: 3.1.2 natural-compare: 1.4.0 optionator: 0.9.4 + optionalDependencies: + jiti: 2.6.1 transitivePeerDependencies: - supports-color @@ -5015,10 +5863,16 @@ snapshots: expect-type@1.2.2: {} + exponential-backoff@3.1.3: {} + + exsolve@1.0.8: {} + fast-check@3.23.2: dependencies: pure-rand: 6.1.0 + fast-decode-uri-component@1.0.1: {} + fast-deep-equal@3.1.3: {} fast-glob@3.3.1: @@ -5039,8 +5893,43 @@ snapshots: fast-json-stable-stringify@2.1.0: {} + fast-json-stringify@6.2.0: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.17.1 + ajv-formats: 3.0.1(ajv@8.17.1) + fast-uri: 3.1.0 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + fast-levenshtein@2.0.6: {} + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.0: {} + + fastify-plugin@5.1.0: {} + + fastify@5.7.4: + dependencies: + '@fastify/ajv-compiler': 4.0.5 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.0.3 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.1.0 + fast-json-stringify: 6.2.0 + find-my-way: 9.4.0 + light-my-request: 6.6.0 + pino: 10.3.0 + process-warning: 5.0.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.7.3 + toad-cache: 3.7.0 + fastq@1.19.1: dependencies: reusify: 1.1.0 @@ -5057,6 +5946,12 @@ snapshots: dependencies: to-regex-range: 5.0.1 + find-my-way@9.4.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.0.0 + find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -5084,6 +5979,8 @@ snapshots: jsonfile: 6.1.0 universalify: 2.0.1 + fs.realpath@1.0.0: {} + fsevents@2.3.3: optional: true @@ -5129,7 +6026,23 @@ snapshots: get-tsconfig@4.10.1: dependencies: resolve-pkg-maps: 1.0.0 - optional: true + + giget@2.0.0: + dependencies: + citty: 0.1.6 + consola: 3.4.2 + defu: 6.1.4 + node-fetch-native: 1.6.7 + nypm: 0.6.4 + pathe: 2.0.3 + + git-diff@2.0.6: + dependencies: + chalk: 2.4.2 + diff: 3.5.1 + loglevel: 1.9.2 + shelljs: 0.8.5 + shelljs.exec: 1.1.8 glob-parent@5.1.2: dependencies: @@ -5148,6 +6061,15 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + globals@14.0.0: {} globals@16.3.0: {} @@ -5165,6 +6087,8 @@ snapshots: has-bigints@1.1.0: {} + has-flag@3.0.0: {} + has-flag@4.0.0: {} has-property-descriptors@1.0.2: @@ -5198,6 +6122,11 @@ snapshots: imurmurhash@0.1.4: {} + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + inherits@2.0.4: {} internal-slot@1.1.0: @@ -5206,12 +6135,32 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 + interpret@1.4.0: {} + + ioredis@5.9.2: + dependencies: + '@ioredis/commands': 1.5.0 + cluster-key-slot: 1.1.2 + debug: 4.4.1 + denque: 2.1.0 + lodash.defaults: 4.2.0 + lodash.isarguments: 3.1.0 + redis-errors: 1.2.0 + redis-parser: 3.0.0 + standard-as-callback: 2.1.0 + transitivePeerDependencies: + - supports-color + + ipaddr.js@2.3.0: {} + is-array-buffer@3.0.5: dependencies: call-bind: 1.0.8 call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-arrayish@0.2.1: {} + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -5342,6 +6291,8 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 + jiti@2.6.1: {} + jotai@2.15.1(@types/react@19.2.7)(react@19.2.3): optionalDependencies: '@types/react': 19.2.7 @@ -5361,8 +6312,16 @@ snapshots: json-buffer@3.0.1: {} + json-parse-even-better-errors@2.3.1: {} + + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + json-schema-traverse@0.4.1: {} + json-schema-traverse@1.0.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} json2mq@0.2.0: @@ -5386,15 +6345,64 @@ snapshots: dependencies: json-buffer: 3.0.1 + kysely-codegen@0.19.0(kysely@0.28.11)(pg@8.18.0)(typescript@5.8.2): + dependencies: + chalk: 4.1.2 + cosmiconfig: 9.0.0(typescript@5.8.2) + dotenv: 17.2.3 + dotenv-expand: 12.0.3 + git-diff: 2.0.6 + kysely: 0.28.11 + micromatch: 4.0.8 + minimist: 1.2.8 + pluralize: 8.0.0 + zod: 4.3.6 + optionalDependencies: + pg: 8.18.0 + transitivePeerDependencies: + - typescript + + kysely-ctl@0.19.0(kysely@0.28.11)(typescript@5.8.2): + dependencies: + c12: 3.3.3 + citty: 0.1.6 + confbox: 0.2.2 + consola: 3.4.2 + jiti: 2.6.1 + kysely: 0.28.11 + nypm: 0.6.4 + ofetch: 1.5.1 + pathe: 2.0.3 + pkg-types: 2.3.0 + std-env: 3.9.0 + tsconfck: 3.1.6(typescript@5.8.2) + transitivePeerDependencies: + - magicast + - typescript + + kysely@0.28.11: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + + lines-and-columns@1.2.4: {} + locate-path@6.0.0: dependencies: p-locate: 5.0.0 + lodash.defaults@4.2.0: {} + + lodash.isarguments@3.1.0: {} + lodash.merge@4.6.2: {} lodash@4.17.21: {} @@ -5404,6 +6412,8 @@ snapshots: chalk: 4.1.2 is-unicode-supported: 0.1.0 + loglevel@1.9.2: {} + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 @@ -5435,8 +6445,14 @@ snapshots: dependencies: brace-expansion: 2.0.2 + minimist@1.2.8: {} + minipass@7.1.2: {} + mnemonist@0.40.0: + dependencies: + obliterator: 2.0.5 + ms@2.1.3: {} nanoid@3.3.11: {} @@ -5466,12 +6482,20 @@ snapshots: - '@babel/core' - babel-plugin-macros + node-fetch-native@1.6.7: {} + node-fetch@2.7.0: dependencies: whatwg-url: 5.0.0 nofilter@3.1.0: {} + nypm@0.6.4: + dependencies: + citty: 0.2.0 + pathe: 2.0.3 + tinyexec: 1.0.2 + object-assign@4.1.1: {} object-inspect@1.13.4: {} @@ -5508,6 +6532,22 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obliterator@2.0.5: {} + + ofetch@1.5.1: + dependencies: + destr: 2.0.5 + node-fetch-native: 1.6.7 + ufo: 1.6.3 + + ohash@2.0.11: {} + + on-exit-leak-free@2.1.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + onetime@5.1.2: dependencies: mimic-fn: 2.1.0 @@ -5547,14 +6587,25 @@ snapshots: dependencies: p-limit: 3.1.0 + p-timeout@7.0.1: {} + package-json-from-dist@1.0.1: {} parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + path-exists@4.0.0: {} + path-is-absolute@1.0.1: {} + path-key@3.1.1: {} path-parse@1.0.7: {} @@ -5568,12 +6619,77 @@ snapshots: pathval@2.0.1: {} + perfect-debounce@2.1.0: {} + + pg-cloudflare@1.3.0: + optional: true + + pg-connection-string@2.11.0: {} + + pg-int8@1.0.1: {} + + pg-pool@3.11.0(pg@8.18.0): + dependencies: + pg: 8.18.0 + + pg-protocol@1.11.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + pg@8.18.0: + dependencies: + pg-connection-string: 2.11.0 + pg-pool: 3.11.0(pg@8.18.0) + pg-protocol: 1.11.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.3.0 + + pgpass@1.0.5: + dependencies: + split2: 4.2.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} picomatch@4.0.3: {} + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.0: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.0.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.0 + thread-stream: 4.0.0 + + pkg-types@2.3.0: + dependencies: + confbox: 0.2.2 + exsolve: 1.0.8 + pathe: 2.0.3 + + pluralize@8.0.0: {} + possible-typed-array-names@1.1.0: {} postcss@8.4.31: @@ -5588,8 +6704,22 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + prelude-ls@1.2.1: {} + process-warning@4.0.1: {} + + process-warning@5.0.0: {} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 @@ -5602,6 +6732,8 @@ snapshots: queue-microtask@1.2.3: {} + quick-format-unescaped@4.0.4: {} + rc-cascader@3.34.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3): dependencies: '@babel/runtime': 7.28.4 @@ -5921,6 +7053,11 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) + rc9@2.1.2: + dependencies: + defu: 6.1.4 + destr: 2.0.5 + react-dom@19.2.3(react@19.2.3): dependencies: react: 19.2.3 @@ -5938,6 +7075,20 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readdirp@5.0.0: {} + + real-require@0.2.0: {} + + rechoir@0.6.2: + dependencies: + resolve: 1.22.11 + + redis-errors@1.2.0: {} + + redis-parser@3.0.0: + dependencies: + redis-errors: 1.2.0 + reflect.getprototypeof@1.0.10: dependencies: call-bind: 1.0.8 @@ -5962,14 +7113,23 @@ snapshots: dependencies: type-fest: 4.41.0 + remeda@2.33.4: {} + require-directory@2.1.1: {} + require-from-string@2.0.2: {} + resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} - resolve-pkg-maps@1.0.0: - optional: true + resolve-pkg-maps@1.0.0: {} + + resolve@1.22.11: + dependencies: + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 resolve@2.0.0-next.5: dependencies: @@ -5982,8 +7142,12 @@ snapshots: onetime: 5.1.2 signal-exit: 3.0.7 + ret@0.5.0: {} + reusify@1.1.0: {} + rfdc@1.4.1: {} + rollup@4.46.3: dependencies: '@types/estree': 1.0.8 @@ -6035,18 +7199,27 @@ snapshots: es-errors: 1.3.0 is-regex: 1.2.1 + safe-regex2@5.0.0: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + scheduler@0.27.0: {} scroll-into-view-if-needed@3.1.0: dependencies: compute-scroll-into-view: 3.1.1 + secure-json-parse@4.1.0: {} + semver@6.3.1: {} semver@7.7.2: {} - semver@7.7.3: - optional: true + semver@7.7.3: {} + + set-cookie-parser@2.7.2: {} set-function-length@1.2.2: dependencies: @@ -6112,6 +7285,14 @@ snapshots: shebang-regex@3.0.0: {} + shelljs.exec@1.1.8: {} + + shelljs@0.8.5: + dependencies: + glob: 7.2.3 + interpret: 1.4.0 + rechoir: 0.6.2 + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 @@ -6164,10 +7345,18 @@ snapshots: transitivePeerDependencies: - supports-color + sonic-boom@4.2.0: + dependencies: + atomic-sleep: 1.0.0 + source-map-js@1.2.1: {} + split2@4.2.0: {} + stackback@0.0.2: {} + standard-as-callback@2.1.0: {} + std-env@3.9.0: {} stop-iteration-iterator@1.1.0: @@ -6258,18 +7447,28 @@ snapshots: stylis@4.3.6: {} + supports-color@5.5.0: + dependencies: + has-flag: 3.0.0 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 supports-preserve-symlinks-flag@1.0.0: {} + thread-stream@4.0.0: + dependencies: + real-require: 0.2.0 + throttle-debounce@5.0.2: {} tinybench@2.9.0: {} tinyexec@0.3.2: {} + tinyexec@1.0.2: {} + tinyglobby@0.2.14: dependencies: fdir: 6.5.0(picomatch@4.0.3) @@ -6285,6 +7484,8 @@ snapshots: dependencies: is-number: 7.0.0 + toad-cache@3.7.0: {} + toggle-selection@1.0.6: {} tr46@0.0.3: {} @@ -6295,6 +7496,10 @@ snapshots: ts-custom-error@3.3.1: {} + tsconfck@3.1.6(typescript@5.8.2): + optionalDependencies: + typescript: 5.8.2 + tslib@2.8.1: {} tsx@4.20.3: @@ -6303,7 +7508,6 @@ snapshots: get-tsconfig: 4.10.1 optionalDependencies: fsevents: 2.3.3 - optional: true turbo-darwin-64@2.5.5: optional: true @@ -6371,13 +7575,13 @@ snapshots: possible-typed-array-names: 1.1.0 reflect.getprototypeof: 1.0.10 - typescript-eslint@8.37.0(eslint@9.31.0)(typescript@5.8.2): + typescript-eslint@8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2): dependencies: - '@typescript-eslint/eslint-plugin': 8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0)(typescript@5.8.2))(eslint@9.31.0)(typescript@5.8.2) - '@typescript-eslint/parser': 8.37.0(eslint@9.31.0)(typescript@5.8.2) + '@typescript-eslint/eslint-plugin': 8.37.0(@typescript-eslint/parser@8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2))(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2) + '@typescript-eslint/parser': 8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2) '@typescript-eslint/typescript-estree': 8.37.0(typescript@5.8.2) - '@typescript-eslint/utils': 8.37.0(eslint@9.31.0)(typescript@5.8.2) - eslint: 9.31.0 + '@typescript-eslint/utils': 8.37.0(eslint@9.31.0(jiti@2.6.1))(typescript@5.8.2) + eslint: 9.31.0(jiti@2.6.1) typescript: 5.8.2 transitivePeerDependencies: - supports-color @@ -6386,6 +7590,8 @@ snapshots: typescript@5.8.3: {} + ufo@1.6.3: {} + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -6403,13 +7609,13 @@ snapshots: util-deprecate@1.0.2: {} - vite-node@3.2.4(@types/node@22.15.3)(tsx@4.20.3): + vite-node@3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.1.3(@types/node@22.15.3)(tsx@4.20.3) + vite: 7.1.3(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) transitivePeerDependencies: - '@types/node' - jiti @@ -6424,7 +7630,7 @@ snapshots: - tsx - yaml - vite@7.1.3(@types/node@22.15.3)(tsx@4.20.3): + vite@7.1.3(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3): dependencies: esbuild: 0.25.8 fdir: 6.5.0(picomatch@4.0.3) @@ -6435,13 +7641,14 @@ snapshots: optionalDependencies: '@types/node': 22.15.3 fsevents: 2.3.3 + jiti: 2.6.1 tsx: 4.20.3 - vitest@3.2.4(@types/node@22.15.3)(tsx@4.20.3): + vitest@3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.1.3(@types/node@22.15.3)(tsx@4.20.3)) + '@vitest/mocker': 3.2.4(vite@7.1.3(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -6459,8 +7666,8 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.1.3(@types/node@22.15.3)(tsx@4.20.3) - vite-node: 3.2.4(@types/node@22.15.3)(tsx@4.20.3) + vite: 7.1.3(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) + vite-node: 3.2.4(@types/node@22.15.3)(jiti@2.6.1)(tsx@4.20.3) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.15.3 @@ -6553,12 +7760,16 @@ snapshots: string-width: 5.1.2 strip-ansi: 7.1.0 + wrappy@1.0.2: {} + ws@7.5.10: {} ws@8.18.3: {} xmlhttprequest-ssl@2.1.2: {} + xtend@4.0.2: {} + y18n@5.0.8: {} yargs-parser@21.1.1: {} @@ -6574,3 +7785,5 @@ snapshots: yargs-parser: 21.1.1 yocto-queue@0.1.0: {} + + zod@4.3.6: {} diff --git a/turbo.json b/turbo.json index ff10a66..70801e2 100644 --- a/turbo.json +++ b/turbo.json @@ -7,51 +7,149 @@ "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, - "@repo/ledger-core#build": { - "dependsOn": ["@repo/ledger-utils#build"], + "@minswap/felis-ledger-utils#build": { + "dependsOn": ["@minswap/felis-uplc-node#build", "@minswap/felis-uplc-web#build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**"] }, - "@repo/tx-builder#build": { - "dependsOn": ["@repo/ledger-core#build", "@repo/cip#build"], + "@minswap/felis-ledger-core#build": { + "dependsOn": ["@minswap/felis-ledger-utils#build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**"] }, - "@repo/cip#build": { - "dependsOn": ["@repo/ledger-core#build"], + "@minswap/felis-cip#build": { + "dependsOn": ["@minswap/felis-ledger-core#build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**"] }, - "@repo/minswap-lending-market#build": { - "dependsOn": ["@repo/tx-builder#build", "@repo/minswap-build-tx#build"], + "@minswap/felis-tx-builder#build": { + "dependsOn": ["@minswap/felis-ledger-core#build", "@minswap/felis-cip#build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**"] }, - "@repo/minswap-build-tx#build": { - "dependsOn": ["@repo/tx-builder#build"], + "@minswap/felis-dex-v1#build": { + "dependsOn": ["@minswap/felis-ledger-core#build", "@minswap/felis-ledger-utils#build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**"] }, - "@repo/minswap-dex-v1#build": { - "dependsOn": ["@repo/ledger-core#build", "@repo/ledger-utils#build"], + "@minswap/felis-dex-v2#build": { + "dependsOn": ["@minswap/felis-ledger-core#build", "@minswap/felis-ledger-utils#build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**"] }, - "@repo/minswap-dex-v2#build": { - "dependsOn": ["@repo/ledger-core#build", "@repo/ledger-utils#build"], + "@minswap/felis-provider#build": { + "dependsOn": ["@minswap/felis-ledger-core#build", "@minswap/felis-ledger-utils#build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**"] }, - "@repo/minswap-stableswap#build": { - "dependsOn": ["@repo/minswap-dex-v2#build", "@repo/minswap-build-tx#build", "@repo/tx-builder#build"], + "@minswap/felis-splash#build": { + "dependsOn": ["@minswap/felis-ledger-core#build", "@minswap/felis-ledger-utils#build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**"] }, - "@repo/syncer#build": { - "dependsOn": ["@repo/minswap-dex-v1#build", "@repo/minswap-dex-v2#build", "@repo/minswap-stableswap#build", "@repo/tx-builder#build"], + "@minswap/felis-sundaeswap-v1#build": { + "dependsOn": ["@minswap/felis-ledger-core#build", "@minswap/felis-ledger-utils#build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], "outputs": ["dist/**"] }, + "@minswap/felis-sundaeswap-v3#build": { + "dependsOn": ["@minswap/felis-ledger-core#build", "@minswap/felis-ledger-utils#build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "@minswap/felis-wingriders-v1#build": { + "dependsOn": ["@minswap/felis-ledger-core#build", "@minswap/felis-ledger-utils#build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "@minswap/felis-wingriders-v2#build": { + "dependsOn": ["@minswap/felis-ledger-core#build", "@minswap/felis-ledger-utils#build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "@minswap/felis-build-tx#build": { + "dependsOn": ["@minswap/felis-tx-builder#build", "@minswap/felis-dex-v2#build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "@minswap/felis-stableswap#build": { + "dependsOn": ["@minswap/felis-dex-v2#build", "@minswap/felis-build-tx#build", "@minswap/felis-tx-builder#build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "@minswap/felis-lending-market#build": { + "dependsOn": ["@minswap/felis-tx-builder#build", "@minswap/felis-build-tx#build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "@minswap/felis-syncer#build": { + "dependsOn": ["@minswap/felis-dex-v1#build", "@minswap/felis-dex-v2#build", "@minswap/felis-stableswap#build", "@minswap/felis-tx-builder#build"], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "@minswap/felis#build": { + "dependsOn": [ + "@minswap/felis-ledger-utils#build", + "@minswap/felis-ledger-core#build", + "@minswap/felis-cip#build", + "@minswap/felis-tx-builder#build", + "@minswap/felis-provider#build", + "@minswap/felis-dex-v1#build", + "@minswap/felis-dex-v2#build", + "@minswap/felis-build-tx#build", + "@minswap/felis-stableswap#build", + "@minswap/felis-lending-market#build", + "@minswap/felis-splash#build", + "@minswap/felis-sundaeswap-v1#build", + "@minswap/felis-sundaeswap-v3#build", + "@minswap/felis-wingriders-v1#build", + "@minswap/felis-wingriders-v2#build", + "@minswap/felis-syncer#build" + ], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "long-short-backend#build": { + "dependsOn": [ + "@minswap/felis-build-tx#build", + "@minswap/felis-cip#build", + "@minswap/felis-dex-v1#build", + "@minswap/felis-dex-v2#build", + "@minswap/felis-ledger-core#build", + "@minswap/felis-ledger-utils#build", + "@minswap/felis-tx-builder#build", + "@minswap/felis-lending-market#build" + ], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "@apps/example#build": { + "dependsOn": [ + "@minswap/felis-ledger-core#build", + "@minswap/felis-ledger-utils#build", + "@minswap/felis-dex-v2#build", + "@minswap/felis-sundaeswap-v1#build", + "@minswap/felis-sundaeswap-v3#build", + "@minswap/felis-syncer#build", + "@minswap/felis-provider#build", + "@minswap/felis-tx-builder#build" + ], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": ["dist/**"] + }, + "web#build": { + "dependsOn": [ + "@minswap/felis-ledger-core#build", + "@minswap/felis-ledger-utils#build", + "@minswap/felis-tx-builder#build", + "@minswap/felis-cip#build", + "@minswap/felis-lending-market#build", + "@minswap/felis-dex-v2#build", + "@minswap/felis-build-tx#build" + ], + "inputs": ["$TURBO_DEFAULT$", ".env*"], + "outputs": [".next/**", "!.next/cache/**"] + }, "lint": { "dependsOn": ["^lint"] },