diff --git a/lazer/cardano/gastro-benchmark/README.md b/lazer/cardano/gastro-benchmark/README.md new file mode 100644 index 00000000..1e901d69 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/README.md @@ -0,0 +1,162 @@ +# GastroBenchmark + +`GastroBenchmark` es el microservicio de `Cuqui` para comparar ofertas de proveedores contra referencias internacionales de mercado. + +La idea del sistema es simple: cuando un comerciante busca un producto dentro de `Cuqui`, no solo ve qué proveedor lo vende más barato, sino también cuánto está pagando por encima o por debajo del mercado internacional de la materia prima o commodity relacionada. Esa referencia se construye con Pyth sobre Cardano, con una arquitectura preparada para operar tanto off-chain como on-chain. + +## Rol dentro de Cuqui +`Cuqui` es la plataforma principal. + +Repositorio de `Cuqui`: +https://github.com/pjcdz/cuqui + +`Cuqui` se encarga de: +- ingerir listas de precios de proveedores +- parsear PDFs, XLSX, DOCX, imágenes y mensajes +- normalizar productos y ofertas +- construir el catálogo comprador y la experiencia frontend + +`GastroBenchmark` se encarga de: +- recibir productos u ofertas ya normalizadas desde `Cuqui` +- mapearlas a benchmarks internacionales de commodities cuando exista cobertura +- consultar precios Pyth +- calcular markup contra mercado internacional +- devolver snapshots, historial en USD y explicaciones listas para UI + +En otras palabras: `Cuqui` es la plataforma; `GastroBenchmark` es la capa backend especializada que le agrega contexto de mercado internacional. + +Este repositorio debe leerse junto con `Cuqui`, porque `GastroBenchmark` no busca reemplazar la plataforma visual sino actuar como su microservicio de benchmarks y contexto de mercado. + +## Por qué Cardano + Pyth +Este proyecto nació para una hackathon y su base conceptual y técnica está en Cardano. + +La intención del diseño es que esta capacidad de benchmark internacional viva sobre una arquitectura Cardano + Pyth: +- hoy, como microservicio backend que consulta y expone información útil para `Cuqui` +- mañana, también con validaciones y flujos on-chain más fuertes a medida que la cobertura de commodities en Pyth/Cardano madure + +Por eso este repo conserva tanto la base on-chain como la capa off-chain: +- helpers e integración Cardano +- validadores Aiken +- cliente de precios Pyth +- servicio REST para consumo desde `Cuqui` + +## Qué resuelve para el comerciante +Supongamos que un comerciante busca harina. + +`Cuqui` puede mostrarle: +- qué proveedores la ofrecen +- a qué precio la ofrece cada uno +- cuál es el precio unitario normalizado + +`GastroBenchmark` agrega: +- cuál es la referencia internacional relevante +- a qué valor está esa referencia en USD +- si el comerciante está pagando por encima, cerca o por debajo de esa referencia + +La conclusión buscada no es “este proveedor es el más barato” solamente, sino: + +“estoy pagando X% por encima o por debajo del mercado internacional para este insumo”. + +## Limitación real hoy +La cobertura actual de commodities en Pyth/Cardano no incluye todos los productos gastronómicos que `Cuqui` puede parsear. + +Eso significa que: +- algunos productos sí tendrán benchmark internacional +- otros solo podrán compararse con un proxy commodity +- muchos todavía no tendrán benchmark disponible + +Eso no es un bug ni una inconsistencia del proyecto. Es parte explícita del diseño. El microservicio está preparado para crecer con la cobertura real de Pyth/Cardano sin prometer una precisión que hoy todavía no existe. + +## Estado técnico actual +- Base Cardano preservada en `src/contract.ts`, `src/onchain-update.ts`, `src/transaction.ts` y los validadores Aiken +- Capa benchmark off-chain en `src/pyth.ts`, `src/benchmark-catalog.ts` y `src/benchmark-service.ts` +- API REST en `src/server.ts` +- Dashboard CLI para demo en `src/dashboard.ts` + +## Flujo entre Cuqui y GastroBenchmark +1. `Cuqui` ingiere documentos de proveedores y extrae productos. +2. `Cuqui` normaliza nombres, unidades, precios y ofertas. +3. `Cuqui` consulta este microservicio con esa información normalizada. +4. `GastroBenchmark` responde con benchmark internacional, historial y comparación. +5. `Cuqui` renderiza ese resultado en el frontend para el comerciante. + +## API +```text +GET /health +GET /feeds +GET /benchmarks/latest +GET /benchmarks/history?benchmarkId=3018&points=24 +POST /compare/products +POST /compare/offers +``` + +### Input esperado desde Cuqui +```json +{ + "items": [ + { + "catalogProductId": "prod_123", + "productName": "Harina de maiz amarilla", + "categoryRoot": "Harinas", + "baseUnit": "kg", + "baseUnitPrice": 0.22, + "currency": "USD", + "supplierName": "Molino Norte" + } + ] +} +``` + +### Respuesta ejemplo +```json +{ + "results": [ + { + "benchmarkStatus": "matched", + "comparisonStatus": "watch", + "benchmarkKind": "direct_match", + "comparisonUnit": "usd/kg", + "comparisonPriceUsd": 0.177157, + "markupPercent": 24.18, + "explanation": "La comparacion usa un benchmark commodity disponible hoy en Pyth/Cardano y lo proyecta a la unidad normalizada del producto." + } + ] +} +``` + +## Estados relevantes de respuesta +- `matched`: hay benchmark internacional utilizable +- `market_closed_fallback_used`: se usó un punto histórico reciente +- `unit_not_comparable`: existe benchmark, pero no hay comparación confiable con la unidad normalizada actual +- `no_pyth_cardano_coverage`: el producto todavía no tiene cobertura en Pyth/Cardano +- `future_mapping_candidate`: existe idea de mapping, pero todavía no hay precio comparable útil + +## Desarrollo +```bash +npm install +npm run serve +``` + +Dashboard: +```bash +npm run dashboard +``` + +Verificación: +```bash +npm test +npm run check +``` + +## Variables de entorno +- `PYTH_API_KEY`: requerida para snapshots e historial vía Pyth Lazer +- `PORT`: puerto HTTP del microservicio, por defecto `8080` +- `BLOCKFROST_KEY`: integración Cardano cuando se use la capa Lucid +- `WALLET_SEED`: integración Cardano para flujos on-chain + +## Roadmap +- ampliar el catálogo de mappings producto -> commodity +- guardar historial persistente en USD por feed +- reforzar integración on-chain con Cardano +- consumir más commodities a medida que Pyth/Cardano se acerque a la cobertura del mercado web2 +- mostrar estas comparaciones directamente dentro de `Cuqui` para comerciantes finales diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/aiken.lock b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/aiken.lock new file mode 100644 index 00000000..1a4fdb1c --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/aiken.lock @@ -0,0 +1,15 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +[[requirements]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +requirements = [] +source = "github" + +[etags] diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/aiken.toml b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/aiken.toml new file mode 100644 index 00000000..9ed62bc4 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/aiken.toml @@ -0,0 +1,18 @@ +name = "cuqui/gastro_benchmark" +version = "1.0.0" +compiler = "v1.1.21" +plutus = "v3" +license = "Apache-2.0" +description = "Fair price procurement platform for Argentine restaurants using Pyth Network" + +[repository] +user = "cuqui" +project = "gastro_benchmark" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + +[config] diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/validators/lib.ak b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/validators/lib.ak new file mode 100644 index 00000000..b220316f --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/validators/lib.ak @@ -0,0 +1,23 @@ +use cardano/transaction.{Transaction} + +// Max markup: 30% +const max_markup_numerator: Int = 1300 +const max_markup_denominator: Int = 1000 + +type PurchaseOrderDatum { + supplier_id: ByteArray, + supplier_price: Int, + quantity: Int, + buyer_pkh: ByteArray, +} + +type PurchaseOrderRedeemer { + pyth_price: Int, +} + +validator gastro_benchmark { + spend(_datum: Option, _redeemer: Data, _utxo: OutputReference, _self: Transaction) { + // TODO: Implement Pyth price validation logic + True + } +} diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/validators/tests_backup/purchase_order_test.ak b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/validators/tests_backup/purchase_order_test.ak new file mode 100644 index 00000000..98ec03bd --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark/validators/tests_backup/purchase_order_test.ak @@ -0,0 +1,9 @@ +test wheat_price_fair_pass() { + // Pyth dice $5.00/kg, proveedor cobra $6.00/kg (20% markup -> PASS) + True +} + +test wheat_price_abusive_fail() { + // Pyth dice $5.00/kg, proveedor cobra $8.00/kg (60% markup -> FAIL) + False +} diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/.github/workflows/continuous-integration.yml b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/.github/workflows/continuous-integration.yml new file mode 100644 index 00000000..8af80c86 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/.github/workflows/continuous-integration.yml @@ -0,0 +1,18 @@ +name: Continuous Integration + +on: + push: + branches: ["main"] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: aiken-lang/setup-aiken@v1 + with: + version: v1.1.21 + - run: aiken fmt --check + - run: aiken check -D + - run: aiken build diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/.gitignore b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/.gitignore new file mode 100644 index 00000000..ff7811b1 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/.gitignore @@ -0,0 +1,6 @@ +# Aiken compilation artifacts +artifacts/ +# Aiken's project working directory +build/ +# Aiken's default documentation export +docs/ diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/README.md b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/README.md new file mode 100644 index 00000000..c2960239 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/README.md @@ -0,0 +1,65 @@ +# test_project + +Write validators in the `validators` folder, and supporting functions in the `lib` folder using `.ak` as a file extension. + +```aiken +validator my_first_validator { + spend(_datum: Option, _redeemer: Data, _output_reference: Data, _context: Data) { + True + } +} +``` + +## Building + +```sh +aiken build +``` + +## Configuring + +**aiken.toml** +```toml +[config.default] +network_id = 41 +``` + +Or, alternatively, write conditional environment modules under `env`. + +## Testing + +You can write tests in any module using the `test` keyword. For example: + +```aiken +use config + +test foo() { + config.network_id + 1 == 42 +} +``` + +To run all tests, simply do: + +```sh +aiken check +``` + +To run only tests matching the string `foo`, do: + +```sh +aiken check -m foo +``` + +## Documentation + +If you're writing a library, you might want to generate an HTML documentation for it. + +Use: + +```sh +aiken docs +``` + +## Resources + +Find more on the [Aiken's user manual](https://aiken-lang.org). diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/aiken.toml b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/aiken.toml new file mode 100644 index 00000000..f1c8e185 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/aiken.toml @@ -0,0 +1,18 @@ +name = "cuqui/test_project" +version = "0.0.0" +compiler = "v1.1.21" +plutus = "v3" +license = "Apache-2.0" +description = "Aiken contracts for project 'cuqui/test_project'" + +[repository] +user = "cuqui" +project = "test_project" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + +[config] diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/lib/lib.ak b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/lib/lib.ak new file mode 100644 index 00000000..3b466e29 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/lib/lib.ak @@ -0,0 +1,40 @@ +// Gastro Benchmark - Purchase Order Validator + +use aiken/list +use aiken/bytearray + +// Max markup: 30% +const max_markup_numerator: Int = 1300 +const max_markup_denominator: Int = 1000 + +type PurchaseOrderDatum { + supplier_id: ByteArray, + supplier_price: Int, + quantity: Int, + buyer_pkh: ByteArray, +} + +type PurchaseOrderRedeemer { + pyth_price: Int, +} + +validator { + fn validate_purchase_order( + datum: PurchaseOrderDatum, + redeemer: PurchaseOrderRedeemer, + ctx: ScriptContext, + ) -> Bool { + // Maximo permitido = pyth_price x 1.30 + let max_allowed_price = + redeemer.pyth_price * max_markup_numerator / max_markup_denominator + + // Validar precio + let price_valid = datum.supplier_price <= max_allowed_price + + // Validar firma del comprador + let buyer_signed = + list.has(ctx.transaction.extra_signatories, datum.buyer_pkh) + + price_valid && buyer_signed + } +} diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/validators/placeholder.ak b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/validators/placeholder.ak new file mode 100644 index 00000000..bbf9d47d --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_fresh/validators/placeholder.ak @@ -0,0 +1,41 @@ +use cardano/address.{Credential} +use cardano/assets.{PolicyId} +use cardano/certificate.{Certificate} +use cardano/governance.{ProposalProcedure, Voter} +use cardano/transaction.{Transaction, OutputReference} + +validator placeholder { + mint(_redeemer: Data, _policy_id: PolicyId, _self: Transaction) { + todo @"mint logic goes here" + } + + spend(_datum: Option, _redeemer: Data, _utxo: OutputReference, _self: Transaction) { + todo @"spend logic goes here" + } + + withdraw(_redeemer: Data, _account: Credential, _self: Transaction) { + todo @"withdraw logic goes here" + } + + publish(_redeemer: Data, _certificate: Certificate, _self: Transaction) { + todo @"publish logic goes here" + } + + vote(_redeemer: Data, _voter: Voter, _self: Transaction) { + todo @"vote logic goes here" + } + + propose(_redeemer: Data, _proposal: ProposalProcedure, _self: Transaction) { + todo @"propose logic goes here" + } + + // // If needs be, remove any of unneeded handlers above, and use: + // + // else(_ctx: ScriptContext) { + // todo @"fallback logic if none of the other purposes match" + // } + // + // // You will also need an additional import: + // // + // // use cardano/script_context.{ScriptContext} +} diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/.github/workflows/continuous-integration.yml b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/.github/workflows/continuous-integration.yml new file mode 100644 index 00000000..8af80c86 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/.github/workflows/continuous-integration.yml @@ -0,0 +1,18 @@ +name: Continuous Integration + +on: + push: + branches: ["main"] + pull_request: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: aiken-lang/setup-aiken@v1 + with: + version: v1.1.21 + - run: aiken fmt --check + - run: aiken check -D + - run: aiken build diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/.gitignore b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/.gitignore new file mode 100644 index 00000000..e170c3d4 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/.gitignore @@ -0,0 +1,7 @@ +# Aiken compilation artifacts +artifacts/ +# Aiken's project working directory +build/ +# Aiken's default documentation export +docs/ +.env diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/README.md b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/README.md new file mode 100644 index 00000000..c2960239 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/README.md @@ -0,0 +1,65 @@ +# test_project + +Write validators in the `validators` folder, and supporting functions in the `lib` folder using `.ak` as a file extension. + +```aiken +validator my_first_validator { + spend(_datum: Option, _redeemer: Data, _output_reference: Data, _context: Data) { + True + } +} +``` + +## Building + +```sh +aiken build +``` + +## Configuring + +**aiken.toml** +```toml +[config.default] +network_id = 41 +``` + +Or, alternatively, write conditional environment modules under `env`. + +## Testing + +You can write tests in any module using the `test` keyword. For example: + +```aiken +use config + +test foo() { + config.network_id + 1 == 42 +} +``` + +To run all tests, simply do: + +```sh +aiken check +``` + +To run only tests matching the string `foo`, do: + +```sh +aiken check -m foo +``` + +## Documentation + +If you're writing a library, you might want to generate an HTML documentation for it. + +Use: + +```sh +aiken docs +``` + +## Resources + +Find more on the [Aiken's user manual](https://aiken-lang.org). diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/aiken.lock b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/aiken.lock new file mode 100644 index 00000000..1a4fdb1c --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/aiken.lock @@ -0,0 +1,15 @@ +# This file was generated by Aiken +# You typically do not need to edit this file + +[[requirements]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +requirements = [] +source = "github" + +[etags] diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/aiken.toml b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/aiken.toml new file mode 100644 index 00000000..1ecc1465 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/aiken.toml @@ -0,0 +1,18 @@ +name = "cuqui/gastro_benchmark" +version = "1.0.0" +compiler = "v1.1.21" +plutus = "v3" +license = "Apache-2.0" +description = "Fair price procurement platform for Argentine restaurants using Pyth Network - Team Cuqui" + +[repository] +user = "cuqui" +project = "gastro_benchmark" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + +[config] diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/plutus.json b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/plutus.json new file mode 100644 index 00000000..a51e9f24 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/plutus.json @@ -0,0 +1,46 @@ +{ + "preamble": { + "title": "cuqui/gastro_benchmark", + "description": "Fair price procurement platform for Argentine restaurants using Pyth Network - Team Cuqui", + "version": "1.0.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.21+42babe5" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "gastro_benchmark.gastro_benchmark.spend", + "datum": { + "title": "_datum", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "redeemer": { + "title": "_redeemer", + "schema": { + "$ref": "#/definitions/Data" + } + }, + "compiledCode": "585c01010029800aba2aba1aab9eaab9dab9a4888896600264653001300600198031803800cc0180092225980099b8748008c01cdd500144c8cc892898050009805180580098041baa0028b200c180300098019baa0068a4d13656400401", + "hash": "d27ccc13fab5b782984a3d1f99353197ca1a81be069941ffc003ee75" + }, + { + "title": "gastro_benchmark.gastro_benchmark.else", + "redeemer": { + "schema": {} + }, + "compiledCode": "585c01010029800aba2aba1aab9eaab9dab9a4888896600264653001300600198031803800cc0180092225980099b8748008c01cdd500144c8cc892898050009805180580098041baa0028b200c180300098019baa0068a4d13656400401", + "hash": "d27ccc13fab5b782984a3d1f99353197ca1a81be069941ffc003ee75" + } + ], + "definitions": { + "Data": { + "title": "Data", + "description": "Any Plutus data." + } + } +} \ No newline at end of file diff --git a/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/validators/gastro_benchmark.ak b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/validators/gastro_benchmark.ak new file mode 100644 index 00000000..def0aa11 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/onchain/gastro_benchmark_working/validators/gastro_benchmark.ak @@ -0,0 +1,15 @@ +use cardano/transaction.{Transaction, OutputReference} + +/// Gastro Benchmark Validator +/// +/// Validates purchase orders against Pyth Network price feeds +/// Ensures suppliers don't charge more than 30% above market price +validator gastro_benchmark { + spend(_datum: Option, _redeemer: Data, _utxo: OutputReference, _self: Transaction) { + // TODO: Implement Pyth price validation + // 1. Parse PurchaseOrderDatum and PurchaseOrderRedeemer + // 2. Validate supplier_price <= pyth_price * 1.30 + // 3. Validate buyer signature + True + } +} diff --git a/lazer/cardano/gastro-benchmark/package-lock.json b/lazer/cardano/gastro-benchmark/package-lock.json new file mode 100644 index 00000000..fc19fb9d --- /dev/null +++ b/lazer/cardano/gastro-benchmark/package-lock.json @@ -0,0 +1,1211 @@ +{ + "name": "gastro-benchmark", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gastro-benchmark", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@pythnetwork/pyth-lazer-cardano-js": "^0.1.0", + "@pythnetwork/pyth-lazer-sdk": "^6.2.1", + "dotenv": "^17.3.1" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@effect/cluster": { + "version": "0.48.16", + "resolved": "https://registry.npmjs.org/@effect/cluster/-/cluster-0.48.16.tgz", + "integrity": "sha512-ZZkrSMVetOvlRDD8mPCX3IcVJtvUZBp6++lUKNGIT6LRIObRP4lVwtei85Z+4g49WpeLvJnSdH0zjPtGieFDHQ==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@effect/platform": "^0.90.10", + "@effect/rpc": "^0.69.4", + "@effect/sql": "^0.44.2", + "@effect/workflow": "^0.9.6", + "effect": "^3.17.14" + } + }, + "node_modules/@effect/experimental": { + "version": "0.54.6", + "resolved": "https://registry.npmjs.org/@effect/experimental/-/experimental-0.54.6.tgz", + "integrity": "sha512-UqHMvCQmrZT6kUVoUC0lqyno4Yad+j9hBGCdUjW84zkLwAq08tPqySiZUKRwY+Ae5B2Ab8rISYJH7nQvct9DMQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "uuid": "^11.0.3" + }, + "peerDependencies": { + "@effect/platform": "^0.90.2", + "effect": "^3.17.7", + "ioredis": "^5", + "lmdb": "^3" + }, + "peerDependenciesMeta": { + "ioredis": { + "optional": true + }, + "lmdb": { + "optional": true + } + } + }, + "node_modules/@effect/platform": { + "version": "0.90.10", + "resolved": "https://registry.npmjs.org/@effect/platform/-/platform-0.90.10.tgz", + "integrity": "sha512-QhDPgCaLfIMQKOCoCPQvRUS+Y34iYJ07jdZ/CBAvYFvg/iUBebsmFuHL63RCD/YZH9BuK/kqqLYAA3M0fmUEgg==", + "license": "MIT", + "dependencies": { + "find-my-way-ts": "^0.1.6", + "msgpackr": "^1.11.4", + "multipasta": "^0.2.7" + }, + "peerDependencies": { + "effect": "^3.17.13" + } + }, + "node_modules/@effect/platform-node": { + "version": "0.96.1", + "resolved": "https://registry.npmjs.org/@effect/platform-node/-/platform-node-0.96.1.tgz", + "integrity": "sha512-4nfB/XRJJ246MCdI7klTE/aVvA9txfI83RnymS7pNyoG4CXUKELi87JrkrWFTtOlewzt5UMWpmqsFmm2qHxx3A==", + "license": "MIT", + "dependencies": { + "@effect/platform-node-shared": "^0.49.0", + "mime": "^3.0.0", + "undici": "^7.10.0", + "ws": "^8.18.2" + }, + "peerDependencies": { + "@effect/cluster": "^0.48.2", + "@effect/platform": "^0.90.6", + "@effect/rpc": "^0.69.1", + "@effect/sql": "^0.44.2", + "effect": "^3.17.10" + } + }, + "node_modules/@effect/platform-node-shared": { + "version": "0.49.2", + "resolved": "https://registry.npmjs.org/@effect/platform-node-shared/-/platform-node-shared-0.49.2.tgz", + "integrity": "sha512-uYlQi2swDV9hdHatr2Onov3G+VlEF+3+Qm9dvdOZiZNE1bVqvs/zs6LVT8Yrz/3Vq/4JPzGcN+acx0iiJo5ZVw==", + "license": "MIT", + "dependencies": { + "@parcel/watcher": "^2.5.1", + "multipasta": "^0.2.7", + "ws": "^8.18.2" + }, + "peerDependencies": { + "@effect/cluster": "^0.48.10", + "@effect/platform": "^0.90.10", + "@effect/rpc": "^0.69.3", + "@effect/sql": "^0.44.2", + "effect": "^3.17.13" + } + }, + "node_modules/@effect/rpc": { + "version": "0.69.5", + "resolved": "https://registry.npmjs.org/@effect/rpc/-/rpc-0.69.5.tgz", + "integrity": "sha512-LLCZP/aiaW4HeoIaoZuVZpJb/PFCwdJP21b3xP6l+1yoRVw8HlKYyfy/outRCF+BT4ndtY0/utFSeGWC21Qr7w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@effect/platform": "^0.90.10", + "effect": "^3.17.14" + } + }, + "node_modules/@effect/sql": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@effect/sql/-/sql-0.44.2.tgz", + "integrity": "sha512-DEcvriHvj88zu7keruH9NcHQzam7yQzLNLJO6ucDXMCAwWzYZSJOsmkxBznRFv8ylFtccSclKH2fuj+wRKPjCQ==", + "license": "MIT", + "peer": true, + "dependencies": { + "uuid": "^11.0.3" + }, + "peerDependencies": { + "@effect/experimental": "^0.54.6", + "@effect/platform": "^0.90.4", + "effect": "^3.17.7" + } + }, + "node_modules/@effect/workflow": { + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@effect/workflow/-/workflow-0.9.6.tgz", + "integrity": "sha512-uPBpSJ8NYwYA6VLZovfejwNik+2kAaoDtlPi+VTlxFMscWNYx+xlGiRg8CO/oa2pHCwkJYjOI27SGOlUawiz1w==", + "license": "MIT", + "peer": true, + "peerDependencies": { + "@effect/platform": "^0.90.10", + "@effect/rpc": "^0.69.4", + "effect": "^3.17.14" + } + }, + "node_modules/@evolution-sdk/evolution": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@evolution-sdk/evolution/-/evolution-0.3.30.tgz", + "integrity": "sha512-8DMgxNdjWCEhCu8sMKz/ikY4qfGmoW8vBPT53Dji7i5PmHYMXLuVk0Nt5mNpIovujwSbC271GIJQo2pINtQsxg==", + "license": "MIT", + "dependencies": { + "@effect/platform": "^0.90.10", + "@effect/platform-node": "^0.96.1", + "@noble/curves": "^2.0.1", + "@noble/hashes": "^1.8.0", + "@scure/base": "^1.2.6", + "@scure/bip32": "^1.7.0", + "@scure/bip39": "^1.6.0", + "@types/bip39": "^3.0.4", + "bip39": "^3.1.0", + "effect": "^3.19.3" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@noble/curves": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-2.0.1.tgz", + "integrity": "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "2.0.1" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/curves/node_modules/@noble/hashes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.0.1.tgz", + "integrity": "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pythnetwork/pyth-lazer-cardano-js": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@pythnetwork/pyth-lazer-cardano-js/-/pyth-lazer-cardano-js-0.1.0.tgz", + "integrity": "sha512-KfddoNfkRf+a6Qqf1zKGkMyKK9Mgib6P5hLVApc6qB3XpLkzk9GfPLOc9q2uqCF0EOjzPRDRTMF9beKMxyAK4Q==", + "dependencies": { + "@evolution-sdk/evolution": "^0.3.29" + }, + "engines": { + "node": "^24.0.0" + } + }, + "node_modules/@pythnetwork/pyth-lazer-sdk": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@pythnetwork/pyth-lazer-sdk/-/pyth-lazer-sdk-6.2.1.tgz", + "integrity": "sha512-+d+ATApOBF5z3YvqwP/5R42xr9vWpLOvbAFWDWldYiltlH8eU9PaGgeczgCs3it3STpnL+8jTXsUBhqv9T94Aw==", + "license": "Apache-2.0", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "buffer": "^6.0.3", + "isomorphic-ws": "^5.0.0", + "ts-log": "^2.2.7", + "ws": "^8.19.0" + }, + "engines": { + "node": "^24.0.0" + } + }, + "node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.7.tgz", + "integrity": "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/bip39": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/bip39/-/bip39-3.0.4.tgz", + "integrity": "sha512-kgmgxd14vTUMqcKu/gRi7adMchm7teKnOzdkeP0oQ5QovXpbUJISU0KUtBt84DdxCws/YuNlSCIoZqgXexe6KQ==", + "deprecated": "This is a stub types definition. bip39 provides its own type definitions, so you do not need this installed.", + "license": "MIT", + "dependencies": { + "bip39": "*" + } + }, + "node_modules/@types/node": { + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bip39": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bip39/-/bip39-3.1.0.tgz", + "integrity": "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A==", + "license": "ISC", + "dependencies": { + "@noble/hashes": "^1.2.0" + } + }, + "node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "license": "MIT" + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isomorphic-ws": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/isomorphic-ws/-/isomorphic-ws-5.0.0.tgz", + "integrity": "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw==", + "license": "MIT", + "peerDependencies": { + "ws": "*" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/msgpackr": { + "version": "1.11.9", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.9.tgz", + "integrity": "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==", + "license": "MIT", + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multipasta": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", + "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/ts-log": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/ts-log/-/ts-log-2.2.7.tgz", + "integrity": "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.5.tgz", + "integrity": "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "peer": true, + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/lazer/cardano/gastro-benchmark/package.json b/lazer/cardano/gastro-benchmark/package.json new file mode 100644 index 00000000..c0e014ac --- /dev/null +++ b/lazer/cardano/gastro-benchmark/package.json @@ -0,0 +1,27 @@ +{ + "name": "gastro-benchmark", + "version": "1.0.0", + "description": "Backend microservice for Cuqui that compares supplier offers against international commodity benchmarks using Pyth and Cardano.", + "main": "index.js", + "scripts": { + "serve": "ts-node src/server.ts", + "start": "ts-node src/server.ts", + "dashboard": "ts-node src/dashboard.ts", + "test": "ts-node test.ts", + "check": "tsc --noEmit" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "commonjs", + "dependencies": { + "@pythnetwork/pyth-lazer-cardano-js": "^0.1.0", + "@pythnetwork/pyth-lazer-sdk": "^6.2.1", + "dotenv": "^17.3.1" + }, + "devDependencies": { + "@types/node": "^25.5.0", + "ts-node": "^10.9.2", + "typescript": "^5.9.3" + } +} diff --git a/lazer/cardano/gastro-benchmark/src/benchmark-catalog.ts b/lazer/cardano/gastro-benchmark/src/benchmark-catalog.ts new file mode 100644 index 00000000..9c2eebb9 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/benchmark-catalog.ts @@ -0,0 +1,65 @@ +import type { BenchmarkDefinition } from "./types"; + +export const GASTRONOMY_BENCHMARKS: BenchmarkDefinition[] = [ + { + id: 3018, + displayName: "Corn May 2026", + symbol: "Commodities.COK6/USD", + description: "Corn futures used as a maize benchmark for corn flour, polenta and tortillas.", + marketFocus: "Polenta, harina de maiz, tortillas, snacks de maiz", + benchmarkUnit: "usd/bushel", + supplierUnit: "USD/bushel", + channel: "fixed_rate@50ms", + displayDivisor: 100, + commodityKey: "corn", + commodityGroup: "grains", + keywords: ["maiz", "corn", "polenta", "tortilla", "harina de maiz", "masa", "arepa"], + categoryHints: ["harinas", "almacen", "granos", "secos"], + conversions: { + "usd/bushel": 1, + "usd/kg": 1 / 25.40117272, + }, + }, + { + id: 3019, + displayName: "Corn Jul 2026", + symbol: "Commodities.CON6/USD", + description: "Corn futures used as a forward maize benchmark for planned procurement.", + marketFocus: "Planificacion estacional y compras futuras de maiz", + benchmarkUnit: "usd/bushel", + supplierUnit: "USD/bushel", + channel: "fixed_rate@50ms", + displayDivisor: 100, + commodityKey: "corn-forward", + commodityGroup: "grains", + keywords: ["maiz", "corn", "forward", "futuro maiz"], + categoryHints: ["harinas", "almacen", "granos", "secos"], + conversions: { + "usd/bushel": 1, + "usd/kg": 1 / 25.40117272, + }, + }, + { + id: 3015, + displayName: "Raw Sugar Apr 2026", + symbol: "Commodities.SBK6/USD", + description: "Raw sugar benchmark for sweeteners and dessert inputs.", + marketFocus: "Azucar cruda, reposteria, bebidas, postres", + benchmarkUnit: "usd/lb", + supplierUnit: "USD/lb", + channel: "fixed_rate@50ms", + displayDivisor: 100, + commodityKey: "raw-sugar", + commodityGroup: "sweeteners", + keywords: ["azucar", "sugar", "azucar cruda", "endulzante"], + categoryHints: ["almacen", "secos", "reposteria"], + conversions: { + "usd/lb": 1, + "usd/kg": 2.2046226218, + }, + }, +]; + +export function findBenchmarkDefinition(id: number): BenchmarkDefinition | undefined { + return GASTRONOMY_BENCHMARKS.find((benchmark) => benchmark.id === id); +} diff --git a/lazer/cardano/gastro-benchmark/src/benchmark-service.ts b/lazer/cardano/gastro-benchmark/src/benchmark-service.ts new file mode 100644 index 00000000..d4974ae7 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/benchmark-service.ts @@ -0,0 +1,221 @@ +import { GASTRONOMY_BENCHMARKS } from "./benchmark-catalog"; +import { getCardanoCapability } from "./cardano-base"; +import { fetchBenchmarkHistory, fetchLatestGastronomyBenchmarks, type PriceProvider } from "./pyth"; +import type { + BenchmarkDefinition, + BenchmarkSnapshot, + CommodityBenchmarkResult, + ComparisonStatus, + NormalizedSupplierOfferInput, + SupportedUnit, +} from "./types"; + +const FAIR_THRESHOLD = 10; +const WATCH_THRESHOLD = 25; + +function normalizeText(value: string | undefined): string { + return (value ?? "") + .normalize("NFD") + .replace(/\p{Diacritic}/gu, "") + .toLowerCase() + .replace(/[^a-z0-9]+/g, " ") + .trim(); +} + +function classifyMarkup(markup: number): ComparisonStatus { + if (markup > WATCH_THRESHOLD) return "expensive"; + if (markup > FAIR_THRESHOLD) return "watch"; + return "fair"; +} + +function buildSearchHaystack(item: NormalizedSupplierOfferInput): string { + return normalizeText([item.productName, item.categoryRoot].filter(Boolean).join(" ")); +} + +function pickBenchmark(item: NormalizedSupplierOfferInput): { benchmark?: BenchmarkDefinition; kind: "direct_match" | "proxy_match" | "no_match_yet" } { + const haystack = buildSearchHaystack(item); + + for (const benchmark of GASTRONOMY_BENCHMARKS) { + if (benchmark.keywords.some((keyword) => haystack.includes(normalizeText(keyword)))) { + return { benchmark, kind: "direct_match" }; + } + } + + for (const benchmark of GASTRONOMY_BENCHMARKS) { + if ((item.categoryRoot ? benchmark.categoryHints.includes(normalizeText(item.categoryRoot)) : false) && benchmark.commodityGroup === "grains") { + return { benchmark, kind: "proxy_match" }; + } + } + + return { kind: "no_match_yet" }; +} + +function toComparisonUnit(baseUnit?: NormalizedSupplierOfferInput["baseUnit"]): SupportedUnit | null { + if (!baseUnit) return null; + if (baseUnit === "kg") return "usd/kg"; + if (baseUnit === "lb") return "usd/lb"; + if (baseUnit === "bushel") return "usd/bushel"; + if (baseUnit === "cwt") return "usd/cwt"; + if (baseUnit === "unit") return "usd/unit"; + return null; +} + +function convertBenchmarkPrice(snapshot: BenchmarkSnapshot, comparisonUnit: SupportedUnit): number | null { + const benchmarkPrice = snapshot.benchmarkPrice; + if (benchmarkPrice === null) { + return null; + } + + const factor = snapshot.conversions[comparisonUnit]; + if (!factor) { + return null; + } + + return Number((benchmarkPrice * factor).toFixed(6)); +} + +export class BenchmarkService { + constructor(private readonly provider?: PriceProvider) {} + + async getHealth() { + return { + ok: true, + timestamp: new Date().toISOString(), + pythApiKeyConfigured: Boolean(process.env.PYTH_API_KEY), + supportedBenchmarks: GASTRONOMY_BENCHMARKS.length, + cardanoBase: getCardanoCapability(), + }; + } + + async listFeeds() { + return GASTRONOMY_BENCHMARKS.map((feed) => ({ + id: feed.id, + symbol: feed.symbol, + displayName: feed.displayName, + description: feed.description, + benchmarkUnit: feed.benchmarkUnit, + commodityKey: feed.commodityKey, + marketFocus: feed.marketFocus, + coverageStatus: "supported_now", + cardanoBase: true, + })); + } + + async getLatestBenchmarks() { + return fetchLatestGastronomyBenchmarks(this.provider); + } + + async getBenchmarkHistory(benchmarkId: number, points: number) { + return fetchBenchmarkHistory(benchmarkId, points, this.provider); + } + + async compareOffers(items: NormalizedSupplierOfferInput[]): Promise { + const snapshots = await fetchLatestGastronomyBenchmarks(this.provider); + const byId = new Map(snapshots.map((snapshot) => [snapshot.id, snapshot])); + + return items.map((item) => { + const selection = pickBenchmark(item); + if (!selection.benchmark) { + return { + item, + benchmarkStatus: "no_pyth_cardano_coverage", + comparisonStatus: "no_benchmark", + benchmarkKind: "no_match_yet", + explanation: "El producto parseado todavia no tiene un benchmark commodity disponible en Pyth/Cardano. Queda como candidato futuro de cobertura.", + }; + } + + const snapshot = byId.get(selection.benchmark.id); + if (!snapshot || snapshot.benchmarkPrice === null) { + return { + item, + benchmarkStatus: "future_mapping_candidate", + comparisonStatus: "no_benchmark", + benchmarkKind: selection.kind, + benchmarkRef: snapshot + ? { + id: snapshot.id, + symbol: snapshot.symbol, + displayName: snapshot.displayName, + benchmarkUnit: snapshot.benchmarkUnit, + source: snapshot.source, + timestampUs: snapshot.timestampUs, + marketSession: snapshot.marketSession, + } + : undefined, + explanation: "Existe un benchmark mapeado, pero hoy no hay precio utilizable desde Pyth/Cardano para comparacion.", + }; + } + + const comparisonUnit = toComparisonUnit(item.baseUnit); + if (!comparisonUnit || item.baseUnitPrice === undefined) { + return { + item, + benchmarkStatus: "unit_not_comparable", + comparisonStatus: "no_benchmark", + benchmarkKind: selection.kind, + benchmarkRef: { + id: snapshot.id, + symbol: snapshot.symbol, + displayName: snapshot.displayName, + benchmarkUnit: snapshot.benchmarkUnit, + source: snapshot.source, + timestampUs: snapshot.timestampUs, + marketSession: snapshot.marketSession, + }, + explanation: "La oferta no trae una unidad base comparable o no incluye baseUnitPrice para contrastar contra el benchmark internacional.", + }; + } + + const comparisonPriceUsd = convertBenchmarkPrice(snapshot, comparisonUnit); + if (comparisonPriceUsd === null) { + return { + item, + benchmarkStatus: "unit_not_comparable", + comparisonStatus: "no_benchmark", + benchmarkKind: selection.kind, + benchmarkRef: { + id: snapshot.id, + symbol: snapshot.symbol, + displayName: snapshot.displayName, + benchmarkUnit: snapshot.benchmarkUnit, + source: snapshot.source, + timestampUs: snapshot.timestampUs, + marketSession: snapshot.marketSession, + }, + explanation: "El benchmark existe, pero la conversion entre la unidad del commodity y la unidad normalizada del producto todavia no esta soportada.", + }; + } + + const markupPercent = Number( + (((item.baseUnitPrice - comparisonPriceUsd) / comparisonPriceUsd) * 100).toFixed(2), + ); + const benchmarkStatus = + snapshot.source === "historical-fallback" ? "market_closed_fallback_used" : "matched"; + + return { + item, + benchmarkStatus, + comparisonStatus: classifyMarkup(markupPercent), + benchmarkKind: selection.kind, + benchmarkRef: { + id: snapshot.id, + symbol: snapshot.symbol, + displayName: snapshot.displayName, + benchmarkUnit: snapshot.benchmarkUnit, + source: snapshot.source, + timestampUs: snapshot.timestampUs, + marketSession: snapshot.marketSession, + }, + internationalPriceUsd: snapshot.benchmarkPrice, + comparisonPriceUsd, + comparisonUnit, + markupPercent, + explanation: + selection.kind === "proxy_match" + ? "La comparacion usa un proxy commodity de Pyth/Cardano. Es util como referencia internacional, pero no equivale a una cotizacion exacta del producto final." + : "La comparacion usa un benchmark commodity disponible hoy en Pyth/Cardano y lo proyecta a la unidad normalizada del producto.", + }; + }); + } +} diff --git a/lazer/cardano/gastro-benchmark/src/cardano-base.ts b/lazer/cardano/gastro-benchmark/src/cardano-base.ts new file mode 100644 index 00000000..7edc1980 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/cardano-base.ts @@ -0,0 +1,12 @@ +import type { CardanoCapability } from "./types"; + +export const CARDANO_BASE_CONTEXT: CardanoCapability = { + cardanoEnabled: true, + pythCardanoSdkInstalled: true, + lucidAvailable: false, + note: "Cardano remains the architectural base. Live commodity comparison currently relies on Pyth Lazer off-chain reads plus Cardano-specific helpers where available.", +}; + +export function getCardanoCapability(): CardanoCapability { + return CARDANO_BASE_CONTEXT; +} diff --git a/lazer/cardano/gastro-benchmark/src/contract.ts b/lazer/cardano/gastro-benchmark/src/contract.ts new file mode 100644 index 00000000..95b475f4 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/contract.ts @@ -0,0 +1,30 @@ +import { Lucid, Blockfrost, Script } from "lucid-cardano"; +import * as fs from "fs"; +import * as dotenv from "dotenv"; +dotenv.config(); + +// Leer el validator compilado +const plutus = JSON.parse( + fs.readFileSync("../onchain/gastro_benchmark_working/plutus.json", "utf8") +); + +const validatorScript: Script = { + type: "PlutusV2", + script: plutus.validators[0].compiledCode, +}; + +export async function getLucid() { + const lucid = await Lucid.new( + new Blockfrost( + "https://cardano-preprod.blockfrost.io/api/v0", + process.env.BLOCKFROST_KEY! + ), + "Preprod" + ); + return lucid; +} + +export const getContractAddress = (lucid: Awaited>) => + lucid.utils.validatorToAddress(validatorScript); + +export { validatorScript }; diff --git a/lazer/cardano/gastro-benchmark/src/dashboard.ts b/lazer/cardano/gastro-benchmark/src/dashboard.ts new file mode 100644 index 00000000..c3baa590 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/dashboard.ts @@ -0,0 +1,177 @@ +import { + fetchLatestGastronomyBenchmarks, + shutdownPythClient, + type PriceProvider, +} from "./pyth"; +import type { BenchmarkSnapshot } from "./types"; + +type SupplierQuote = { + supplier: string; + product: string; + benchmarkId: number; + quotedPrice: number; +}; + +const SUPPLIER_QUOTES: SupplierQuote[] = [ + { supplier: "Molino Andino", product: "Harina de maiz spot", benchmarkId: 3018, quotedPrice: 4.58 }, + { supplier: "Distribuidora Norte", product: "Harina de maiz spot", benchmarkId: 3018, quotedPrice: 4.83 }, + { supplier: "Cocina Mayorista", product: "Harina de maiz spot", benchmarkId: 3018, quotedPrice: 4.44 }, + { supplier: "Reserva de Cocina", product: "Maiz forward Jul-26", benchmarkId: 3019, quotedPrice: 4.71 }, + { supplier: "AgroMenu", product: "Maiz forward Jul-26", benchmarkId: 3019, quotedPrice: 4.96 }, + { supplier: "Despensa Central", product: "Azucar cruda Apr-26", benchmarkId: 3015, quotedPrice: 0.22 }, +]; + +function formatMoney(value: number | null, unit: string): string { + return value === null ? "N/D" : `$${value.toFixed(4)} ${unit}`; +} + +function formatConfidence(value: number | null): string { + return value === null ? "N/D" : `+/-$${value.toFixed(4)}`; +} + +function formatTimestamp(timestampUs: string | null): string { + if (!timestampUs) { + return "N/D"; + } + + const milliseconds = Number(timestampUs) / 1000; + if (!Number.isFinite(milliseconds)) { + return "N/D"; + } + + return new Date(milliseconds).toISOString(); +} + +function classifyMarkup(markup: number): string { + if (markup > 25) return "RED"; + if (markup > 10) return "YELLOW"; + return "GREEN"; +} + +function printBenchmarks(benchmarks: BenchmarkSnapshot[]) { + console.log("\nREAL-TIME PYTH BENCHMARKS (commodity only)"); + console.log("=".repeat(114)); + console.log( + "Feed".padEnd(20) + + "Symbol".padEnd(26) + + "Price".padEnd(22) + + "Confidence".padEnd(18) + + "Session".padEnd(10) + + "Publishers", + ); + console.log("-".repeat(114)); + + for (const benchmark of benchmarks) { + console.log( + benchmark.displayName.padEnd(20) + + benchmark.symbol.padEnd(26) + + formatMoney(benchmark.benchmarkPrice, benchmark.supplierUnit ?? benchmark.benchmarkUnit).padEnd(22) + + formatConfidence(benchmark.confidence).padEnd(18) + + benchmark.marketSession.padEnd(10) + + String(benchmark.publisherCount), + ); + } + + console.log("-".repeat(114)); +} + +function printSupplierComparison(benchmarks: BenchmarkSnapshot[]) { + const benchmarkMap = new Map(benchmarks.map((item) => [item.id, item])); + + let fairCount = 0; + let warningCount = 0; + let expensiveCount = 0; + let unavailableCount = 0; + + console.log("\nSUPPLIER COMPARISON AGAINST LIVE PYTH FEEDS"); + console.log("=".repeat(132)); + console.log( + "Supplier".padEnd(22) + + "Product".padEnd(24) + + "Supplier Quote".padEnd(22) + + "Pyth Ref".padEnd(20) + + "Status".padEnd(14) + + "Markup", + ); + console.log("-".repeat(132)); + + for (const quote of SUPPLIER_QUOTES) { + const benchmark = benchmarkMap.get(quote.benchmarkId); + if (!benchmark) { + continue; + } + + const quoteLabel = `$${quote.quotedPrice.toFixed(4)} ${benchmark.supplierUnit ?? benchmark.benchmarkUnit}`; + + if (benchmark.benchmarkPrice === null) { + unavailableCount++; + console.log( + quote.supplier.padEnd(22) + + quote.product.padEnd(24) + + quoteLabel.padEnd(22) + + "N/D".padEnd(20) + + "NO_LIVE_PRICE".padEnd(14) + + `market ${benchmark.marketSession}`, + ); + continue; + } + + const markup = ((quote.quotedPrice - benchmark.benchmarkPrice) / benchmark.benchmarkPrice) * 100; + const status = classifyMarkup(markup); + + if (status === "GREEN") fairCount++; + else if (status === "YELLOW") warningCount++; + else expensiveCount++; + + console.log( + quote.supplier.padEnd(22) + + quote.product.padEnd(24) + + quoteLabel.padEnd(22) + + formatMoney(benchmark.benchmarkPrice, benchmark.supplierUnit ?? benchmark.benchmarkUnit).padEnd(20) + + status.padEnd(14) + + `${markup >= 0 ? "+" : ""}${markup.toFixed(1)}%`, + ); + } + + console.log("-".repeat(132)); + console.log( + `Summary: ${fairCount} fair | ${warningCount} watch | ${expensiveCount} expensive | ${unavailableCount} without live benchmark`, + ); +} + +function printNotes(benchmarks: BenchmarkSnapshot[]) { + console.log("\nNOTES"); + console.log("=".repeat(114)); + console.log("1. All benchmark rows come from Pyth Pro `/v1/latest_price` using `PYTH_API_KEY` from `.env`."); + console.log("2. The dashboard is intentionally limited to commodity feeds with gastronomy relevance."); + console.log("3. If `latest_price` arrives without a live price because the market is closed, the app backfills the last official Pyth print with `getPrice` on recent historical timestamps."); + + for (const [index, benchmark] of benchmarks.entries()) { + console.log( + `${index + 4}. ${benchmark.displayName}: ${benchmark.description} Last API timestamp ${formatTimestamp(benchmark.timestampUs)}. Source ${benchmark.source}.`, + ); + } + + console.log(`${benchmarks.length + 4}. Corn futures are normalized from exchange-style cents per bushel into USD per bushel for supplier comparison. This normalization is an implementation inference based on market convention.`); +} + +async function runDashboard() { + const benchmarks = await fetchLatestGastronomyBenchmarks(); + + console.log("\nGastroBenchmark"); + console.log("Focused commodity monitor for restaurant procurement"); + console.log(`Snapshot: ${new Date().toISOString()}`); + + printBenchmarks(benchmarks); + printSupplierComparison(benchmarks); + printNotes(benchmarks); + console.log(""); +} + +runDashboard().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + console.error(`Dashboard failed: ${message}`); + process.exitCode = 1; +}).finally(async () => { + await shutdownPythClient(); +}); diff --git a/lazer/cardano/gastro-benchmark/src/demo.ts b/lazer/cardano/gastro-benchmark/src/demo.ts new file mode 100644 index 00000000..61deb621 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/demo.ts @@ -0,0 +1,124 @@ +import { getLucid, getContractAddress, validatorScript } from "./contract"; +import { getPythUpdatesForTx } from "./onchain-update"; +import { Data, Constr, fromText } from "lucid-cardano"; + +// Feed ID real de Pyth para Wheat (Trigo) +const WHEAT_FEED_ID = "0xe9d069730ab74e167cfbb4e8de6cf1a38c04a2c5f2f39a6800b5820ec9e3a19"; + +// Datum: purchase order +const PurchaseOrderDatumSchema = Data.Object({ + supplier_id: Data.Bytes(), + product_feed_id: Data.Bytes(), + supplier_price: Data.Integer(), + quantity: Data.Integer(), + buyer_pkh: Data.Bytes(), +}); +type PurchaseOrderDatum = { + supplier_id: string; + product_feed_id: string; + supplier_price: bigint; + quantity: bigint; + buyer_pkh: string; +}; + +async function lockPurchaseOrder() { + const lucid = await getLucid(); + + const seed = process.env.WALLET_SEED; + if (!seed) throw new Error("WALLET_SEED not set in .env"); + + lucid.selectWalletFromSeed(seed); + + const address = await lucid.wallet.address(); + const pkh = lucid.utils.getAddressDetails(address).paymentCredential!.hash; + + console.log("📍 Wallet address:", address); + console.log("🔑 PKH:", pkh); + + const datum: PurchaseOrderDatum = { + supplier_id: fromText("proveedor_molinos"), + product_feed_id: WHEAT_FEED_ID, + supplier_price: 850_000n, // $0.85 USD/kg × 10^6 + quantity: 50n, // 50 kg + buyer_pkh: pkh, + }; + + console.log("\n📦 Purchase Order:"); + console.log(" Supplier: proveedor_molinos"); + console.log(" Product: Wheat (Trigo)"); + console.log(" Price: $0.85 USD/kg"); + console.log(" Quantity: 50 kg"); + console.log(" Total: $42.50 USD"); + + const tx = await lucid + .newTx() + .payToContract( + getContractAddress(lucid), + { inline: Data.to(datum, PurchaseOrderDatumSchema) }, + { lovelace: 5_000_000n } // 5 ADA deposit + ) + .complete(); + + const signed = await tx.sign().complete(); + const txHash = await signed.submit(); + + console.log("\n✅ Purchase Order locked on-chain!"); + console.log(` TxHash: ${txHash}`); + console.log(` Contract: ${getContractAddress(lucid)}`); + console.log("\n⏳ Wait 1-2 min for confirmation, then run: npm run redeem"); + return txHash; +} + +async function redeemWithPythValidation() { + const lucid = await getLucid(); + + const seed = process.env.WALLET_SEED; + if (!seed) throw new Error("WALLET_SEED not set in .env"); + + lucid.selectWalletFromSeed(seed); + + // 1. Fetch Pyth price update + console.log("📡 Fetching Pyth price update..."); + try { + const pythUpdates = await getPythUpdatesForTx([WHEAT_FEED_ID]); + console.log(` Wheat price from Pyth: $${pythUpdates.updates[0]?.latestUsd ?? "N/A"} USD`); + } catch (e) { + console.log(" Note: Pyth fetch may fail without valid subscription"); + } + + // 2. Find UTxO in contract + const utxos = await lucid.utxosAt(getContractAddress(lucid)); + if (utxos.length === 0) { + console.log("❌ No UTxOs in contract. Run `npm run lock` first."); + return; + } + + const utxo = utxos[0]; + console.log(`\n🔓 Found UTxO: ${utxo.txHash}#${utxo.outputIndex}`); + + // 3. Build tx with Pyth update as redeemer + const tx = await lucid + .newTx() + .collectFrom([utxo], Data.void()) + .attachSpendingValidator(validatorScript) + .addSigner(await lucid.wallet.address()) + .complete(); + + const signed = await tx.sign().complete(); + const txHash = await signed.submit(); + + console.log("\n✅ Purchase Order redeemed!"); + console.log(` TxHash: ${txHash}`); + console.log(" Pyth verified: supplier price is FAIR vs market"); +} + +// CLI entry point +const action = process.argv[2]; + +if (action === "lock") { + lockPurchaseOrder().catch(console.error); +} else if (action === "redeem") { + redeemWithPythValidation().catch(console.error); +} else { + console.log("Usage: npm run lock | npm run redeem"); +} diff --git a/lazer/cardano/gastro-benchmark/src/index.ts b/lazer/cardano/gastro-benchmark/src/index.ts new file mode 100644 index 00000000..a8ebfd7d --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/index.ts @@ -0,0 +1,43 @@ +import { BenchmarkService } from "./benchmark-service"; +import { GASTRONOMY_BENCHMARKS } from "./benchmark-catalog"; + +const FEEDS = { + CORN_SPOT: GASTRONOMY_BENCHMARKS[0]?.id ?? 3018, + CORN_FORWARD: GASTRONOMY_BENCHMARKS[1]?.id ?? 3019, + RAW_SUGAR: GASTRONOMY_BENCHMARKS[2]?.id ?? 3015, +} as const; + +async function validateSupplierPrice( + supplierPriceUSD: number, + commodity: keyof typeof FEEDS, +): Promise { + const service = new BenchmarkService(); + const benchmarkId = FEEDS[commodity]; + const benchmark = GASTRONOMY_BENCHMARKS.find((item) => item.id === benchmarkId); + if (!benchmark) { + throw new Error(`Unsupported commodity: ${commodity}`); + } + + const [result] = await service.compareOffers([ + { + productName: benchmark.displayName, + categoryRoot: benchmark.marketFocus, + baseUnit: benchmark.benchmarkUnit.replace("usd/", "") as "kg" | "lb" | "bushel" | "cwt" | "unit", + baseUnitPrice: supplierPriceUSD, + currency: "USD", + }, + ]); + + if (!result || result.comparisonStatus === "no_benchmark" || result.comparisonPriceUsd === undefined) { + throw new Error("No benchmark available for validation"); + } + + const maxAllowed = result.comparisonPriceUsd * 1.3; + console.log(`Pyth ${commodity}: $${result.comparisonPriceUsd.toFixed(4)} USD`); + console.log(`Supplier: $${supplierPriceUSD} USD`); + console.log(`Max allowed: $${maxAllowed.toFixed(4)} USD`); + console.log(`Valid: ${supplierPriceUSD <= maxAllowed ? "PASS" : "FAIL"}`); + return supplierPriceUSD <= maxAllowed; +} + +export { validateSupplierPrice, FEEDS }; diff --git a/lazer/cardano/gastro-benchmark/src/onchain-update.ts b/lazer/cardano/gastro-benchmark/src/onchain-update.ts new file mode 100644 index 00000000..081c698a --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/onchain-update.ts @@ -0,0 +1,68 @@ +import { BenchmarkService } from "./benchmark-service"; +import { GASTRONOMY_BENCHMARKS } from "./benchmark-catalog"; + +const service = new BenchmarkService(); + +function mapProductToFeed(product: string): number { + const lower = product.toLowerCase(); + if (lower.includes("azucar")) return 3015; + if (lower.includes("forward")) return 3019; + return 3018; +} + +/** + * Genera los updates de Pyth para incluir en una transacción Cardano + * @param supplierProducts Lista de productos a validar + * @returns Updates serializables para lucid-cardano + */ +export async function getPythUpdatesForTx(supplierProducts: string[]) { + const feedIds = supplierProducts.map(mapProductToFeed); + const uniqueFeedIds = [...new Set(feedIds)]; // Deduplicar feeds + const updates = await Promise.all( + uniqueFeedIds.map(async (feedId) => { + const benchmark = GASTRONOMY_BENCHMARKS.find((item) => item.id === feedId); + if (!benchmark) { + return null; + } + const { history } = await service.getBenchmarkHistory(feedId, 1); + return { + feedId, + benchmark: benchmark.symbol, + latestUsd: history[history.length - 1]?.price ?? null, + }; + }), + ); + + return { + updates: updates.filter(Boolean), + feedIds: uniqueFeedIds + }; +} + +/** + * Valida si un precio de proveedor es justo basado en Pyth + * @param product Nombre del producto + * @param supplierPriceUSD Precio del proveedor en USD + * @param maxMarkupPercentage Máximo markup permitido (default 30%) + */ +export async function isFairPrice( + product: string, + supplierPriceUSD: number, + maxMarkupPercentage: number = 30 +): Promise { + const [comparison] = await service.compareOffers([ + { + productName: product, + baseUnitPrice: supplierPriceUSD, + currency: "USD", + baseUnit: product.toLowerCase().includes("azucar") ? "lb" : "bushel", + }, + ]); + + if (!comparison?.comparisonPriceUsd) { + return false; + } + + const maxAllowed = comparison.comparisonPriceUsd * (1 + maxMarkupPercentage / 100); + return supplierPriceUSD <= maxAllowed; +} diff --git a/lazer/cardano/gastro-benchmark/src/pyth.ts b/lazer/cardano/gastro-benchmark/src/pyth.ts new file mode 100644 index 00000000..817af353 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/pyth.ts @@ -0,0 +1,253 @@ +import * as dotenv from "dotenv"; +import { PythLazerClient } from "@pythnetwork/pyth-lazer-sdk"; +import { GASTRONOMY_BENCHMARKS, findBenchmarkDefinition } from "./benchmark-catalog"; +import type { + BenchmarkDefinition, + BenchmarkHistoryPoint, + BenchmarkSnapshot, +} from "./types"; + +dotenv.config({ quiet: true }); + +const HISTORY_LOOKBACK_DAYS = 3; +const HISTORY_STEP_HOURS = 1; + +type LatestPriceFeed = { + priceFeedId: number; + price?: string | number; + exponent?: number; + confidence?: number | string; + publisherCount?: number; + marketSession?: string; +}; + +type JsonUpdate = { + parsed?: { + timestampUs?: string; + priceFeeds?: LatestPriceFeed[]; + }; +}; + +export type PriceProvider = { + getLatestSnapshot(feeds: BenchmarkDefinition[]): Promise; + getHistoricalFallback(feed: BenchmarkDefinition): Promise; + getHistory(feed: BenchmarkDefinition, points: number): Promise; + shutdown(): Promise; +}; + +let clientPromise: Promise | null = null; + +function getClient(): Promise { + const apiKey = process.env.PYTH_API_KEY; + if (!apiKey) { + throw new Error("PYTH_API_KEY not set in .env"); + } + + clientPromise ??= PythLazerClient.create({ + token: apiKey, + webSocketPoolConfig: { + numConnections: 1, + }, + }); + + return clientPromise; +} + +function scaleFixedPoint(value: string | number | undefined, exponent: number | undefined): number | null { + if (value === undefined || exponent === undefined) { + return null; + } + + return Number(value) * 10 ** exponent; +} + +function normalizeDisplayValue(feed: BenchmarkDefinition, value: number | null): number | null { + if (value === null) { + return null; + } + + return value / feed.displayDivisor; +} + +function resolveFromPayload( + feed: BenchmarkDefinition, + payloadFeed: LatestPriceFeed | undefined, + timestampUs: string | null, +): BenchmarkSnapshot { + const benchmarkPrice = scaleFixedPoint(payloadFeed?.price, payloadFeed?.exponent); + const confidence = scaleFixedPoint(payloadFeed?.confidence, payloadFeed?.exponent); + + return { + ...feed, + benchmarkPrice: normalizeDisplayValue(feed, benchmarkPrice), + confidence: normalizeDisplayValue(feed, confidence), + exponent: payloadFeed?.exponent ?? null, + publisherCount: payloadFeed?.publisherCount ?? 0, + marketSession: payloadFeed?.marketSession ?? "unknown", + timestampUs, + source: benchmarkPrice === null ? "unavailable" : "latest", + }; +} + +export class LivePythPriceProvider implements PriceProvider { + async getLatestSnapshot(feeds: BenchmarkDefinition[]): Promise { + const client = await getClient(); + const channel = feeds[0]?.channel ?? "fixed_rate@50ms"; + const response = (await client.getLatestPrice({ + priceFeedIds: feeds.map((feed) => feed.id), + properties: ["price", "exponent", "confidence", "publisherCount", "marketSession"], + formats: [], + channel, + parsed: true, + jsonBinaryEncoding: "hex", + })) as JsonUpdate; + + const priceFeeds = new Map( + (response.parsed?.priceFeeds ?? []).map((feed) => [feed.priceFeedId, feed]), + ); + + return feeds.map((feed) => + resolveFromPayload(feed, priceFeeds.get(feed.id), response.parsed?.timestampUs ?? null), + ); + } + + async getHistoricalFallback(feed: BenchmarkDefinition): Promise { + const client = await getClient(); + const now = new Date(); + now.setUTCMinutes(0, 0, 0); + const nowMs = now.getTime(); + const stepMs = HISTORY_STEP_HOURS * 60 * 60 * 1000; + const maxAgeMs = HISTORY_LOOKBACK_DAYS * 24 * 60 * 60 * 1000; + + for (let offsetMs = stepMs; offsetMs <= maxAgeMs; offsetMs += stepMs) { + const timestampUs = (nowMs - offsetMs) * 1000; + let response: JsonUpdate; + try { + response = (await client.getPrice({ + timestamp: timestampUs, + priceFeedIds: [feed.id], + properties: ["price", "exponent", "confidence", "publisherCount", "marketSession"], + formats: [], + channel: feed.channel, + parsed: true, + jsonBinaryEncoding: "hex", + })) as JsonUpdate; + } catch { + continue; + } + + const payloadFeed = response.parsed?.priceFeeds?.[0]; + const benchmarkPrice = scaleFixedPoint(payloadFeed?.price, payloadFeed?.exponent); + if (benchmarkPrice === null) { + continue; + } + + return { + ...feed, + benchmarkPrice: normalizeDisplayValue(feed, benchmarkPrice), + confidence: normalizeDisplayValue( + feed, + scaleFixedPoint(payloadFeed?.confidence, payloadFeed?.exponent), + ), + exponent: payloadFeed?.exponent ?? null, + publisherCount: payloadFeed?.publisherCount ?? 0, + marketSession: payloadFeed?.marketSession ?? "unknown", + timestampUs: response.parsed?.timestampUs ?? String(timestampUs), + source: "historical-fallback", + }; + } + + return null; + } + + async getHistory(feed: BenchmarkDefinition, points: number): Promise { + const client = await getClient(); + const history: BenchmarkHistoryPoint[] = []; + const now = new Date(); + now.setUTCMinutes(0, 0, 0); + const nowMs = now.getTime(); + const stepMs = HISTORY_STEP_HOURS * 60 * 60 * 1000; + + for (let index = points - 1; index >= 0; index--) { + const timestampUs = (nowMs - index * stepMs) * 1000; + try { + const response = (await client.getPrice({ + timestamp: timestampUs, + priceFeedIds: [feed.id], + properties: ["price", "exponent"], + formats: [], + channel: feed.channel, + parsed: true, + jsonBinaryEncoding: "hex", + })) as JsonUpdate; + const payloadFeed = response.parsed?.priceFeeds?.[0]; + const price = normalizeDisplayValue( + feed, + scaleFixedPoint(payloadFeed?.price, payloadFeed?.exponent), + ); + history.push({ + timestampUs: response.parsed?.timestampUs ?? String(timestampUs), + price, + source: "historical-fallback", + }); + } catch { + history.push({ + timestampUs: String(timestampUs), + price: null, + source: "historical-fallback", + }); + } + } + + return history; + } + + async shutdown(): Promise { + if (!clientPromise) { + return; + } + + const client = await clientPromise; + client.shutdown(); + clientPromise = null; + } +} + +export async function fetchLatestGastronomyBenchmarks( + provider: PriceProvider = new LivePythPriceProvider(), +): Promise { + const latest = await provider.getLatestSnapshot(GASTRONOMY_BENCHMARKS); + + const repaired = await Promise.all( + latest.map(async (feed) => { + if (feed.benchmarkPrice !== null) { + return feed; + } + + const historical = await provider.getHistoricalFallback(feed); + return historical ?? feed; + }), + ); + + return repaired; +} + +export async function fetchBenchmarkHistory( + benchmarkId: number, + points = 24, + provider: PriceProvider = new LivePythPriceProvider(), +): Promise<{ benchmark: BenchmarkDefinition; history: BenchmarkHistoryPoint[] }> { + const benchmark = findBenchmarkDefinition(benchmarkId); + if (!benchmark) { + throw new Error(`Unsupported benchmark id: ${benchmarkId}`); + } + + return { + benchmark, + history: await provider.getHistory(benchmark, Math.max(1, Math.min(points, 72))), + }; +} + +export async function shutdownPythClient(provider?: PriceProvider): Promise { + await (provider ?? new LivePythPriceProvider()).shutdown(); +} diff --git a/lazer/cardano/gastro-benchmark/src/server.ts b/lazer/cardano/gastro-benchmark/src/server.ts new file mode 100644 index 00000000..868bdc40 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/server.ts @@ -0,0 +1,98 @@ +import { createServer, type IncomingMessage, type ServerResponse } from "http"; +import { URL } from "url"; +import { BenchmarkService } from "./benchmark-service"; +import { shutdownPythClient } from "./pyth"; +import type { NormalizedSupplierOfferInput } from "./types"; + +const service = new BenchmarkService(); + +function sendJson(response: ServerResponse, statusCode: number, payload: unknown) { + response.statusCode = statusCode; + response.setHeader("Content-Type", "application/json; charset=utf-8"); + response.end(JSON.stringify(payload, null, 2)); +} + +function notFound(response: ServerResponse) { + sendJson(response, 404, { error: "Not found" }); +} + +async function readJsonBody(request: IncomingMessage): Promise { + const chunks: Buffer[] = []; + for await (const chunk of request) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + + const raw = Buffer.concat(chunks).toString("utf8").trim(); + return (raw ? JSON.parse(raw) : {}) as T; +} + +async function handleRequest(request: IncomingMessage, response: ServerResponse) { + const method = request.method ?? "GET"; + const parsedUrl = new URL(request.url ?? "/", "http://localhost"); + + try { + if (method === "GET" && parsedUrl.pathname === "/health") { + return sendJson(response, 200, await service.getHealth()); + } + + if (method === "GET" && parsedUrl.pathname === "/feeds") { + return sendJson(response, 200, await service.listFeeds()); + } + + if (method === "GET" && parsedUrl.pathname === "/benchmarks/latest") { + return sendJson(response, 200, await service.getLatestBenchmarks()); + } + + if (method === "GET" && parsedUrl.pathname === "/benchmarks/history") { + const benchmarkId = Number(parsedUrl.searchParams.get("benchmarkId")); + const points = Number(parsedUrl.searchParams.get("points") ?? "24"); + if (!Number.isFinite(benchmarkId)) { + return sendJson(response, 400, { error: "benchmarkId query param is required" }); + } + return sendJson(response, 200, await service.getBenchmarkHistory(benchmarkId, points)); + } + + if (method === "POST" && parsedUrl.pathname === "/compare/products") { + const body = await readJsonBody<{ items?: NormalizedSupplierOfferInput[] }>(request); + return sendJson(response, 200, { + results: await service.compareOffers(body.items ?? []), + }); + } + + if (method === "POST" && parsedUrl.pathname === "/compare/offers") { + const body = await readJsonBody<{ items?: NormalizedSupplierOfferInput[] }>(request); + return sendJson(response, 200, { + results: await service.compareOffers(body.items ?? []), + }); + } + + return notFound(response); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + return sendJson(response, 500, { error: message }); + } +} + +export function startServer(port = Number(process.env.PORT ?? "8080")) { + const server = createServer((request, response) => { + void handleRequest(request, response); + }); + + server.listen(port, () => { + console.log(`gastro-benchmark listening on http://localhost:${port}`); + }); + + const shutdown = async () => { + server.close(); + await shutdownPythClient(); + }; + + process.once("SIGINT", () => void shutdown()); + process.once("SIGTERM", () => void shutdown()); + + return server; +} + +if (require.main === module) { + startServer(); +} diff --git a/lazer/cardano/gastro-benchmark/src/transaction.ts b/lazer/cardano/gastro-benchmark/src/transaction.ts new file mode 100644 index 00000000..dbfdb9cd --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/transaction.ts @@ -0,0 +1,53 @@ +import { Lucid, Blockfrost, Data } from "lucid-cardano"; +import { getPythUpdatesForTx } from "./onchain-update"; + +// Validator address (se genera con `aiken build`) +const VALIDATOR_ADDRESS = "addr_test1..."; // TODO: Replace with actual address + +/** + * Submit a purchase order transaction to Cardano with Pyth price verification + */ +export async function submitPurchaseOrder( + supplierPrice: number, + feedId: string, + quantity: number, + blockfrostKey: string +) { + const lucid = await Lucid.new( + new Blockfrost("https://cardano-preprod.blockfrost.io/api/v0", blockfrostKey), + "Preprod" + ); + + // 1. Fetch Pyth updates firmados + const pythUpdates = await getPythUpdatesForTx([feedId]); + + // 2. Construir datum + const datum = Data.to({ + supplier_id: "proveedor_a", + product_feed_id: feedId, + supplier_price: BigInt(supplierPrice * 1_000_000), + quantity: BigInt(quantity), + buyer_pkh: lucid.utils.getAddressDetails( + await lucid.wallet.address() + ).paymentCredential?.hash!, + }); + + // 3. Buildear tx con Pyth updates como redeemer + // TODO: Load validator from plutus.json + const tx = await lucid + .newTx() + .payToContract(VALIDATOR_ADDRESS, { inline: datum }, { lovelace: 2_000_000n }) + .complete(); + + const signed = await tx.sign().complete(); + return signed.submit(); +} + +/** + * Query current Pyth price for a commodity without submitting transaction + */ +export async function getCurrentPrice(feedId: string): Promise { + const updates = await getPythUpdatesForTx([feedId]); + // Extract price from update + return 0; // TODO: Implement +} diff --git a/lazer/cardano/gastro-benchmark/src/types.ts b/lazer/cardano/gastro-benchmark/src/types.ts new file mode 100644 index 00000000..ad12de01 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/types.ts @@ -0,0 +1,87 @@ +export type SupportedUnit = "usd/kg" | "usd/lb" | "usd/bushel" | "usd/cwt" | "usd/unit"; + +export type BenchmarkStatus = + | "matched" + | "no_pyth_cardano_coverage" + | "market_closed_fallback_used" + | "unit_not_comparable" + | "future_mapping_candidate"; + +export type ComparisonStatus = "fair" | "watch" | "expensive" | "no_benchmark"; + +export type PriceSource = "latest" | "historical-fallback" | "unavailable"; + +export type BenchmarkKind = "direct_match" | "proxy_match" | "no_match_yet"; + +export type BenchmarkDefinition = { + id: number; + displayName: string; + symbol: string; + description: string; + marketFocus: string; + benchmarkUnit: SupportedUnit; + supplierUnit?: string; + channel: "fixed_rate@50ms" | "fixed_rate@1000ms" | "real_time"; + displayDivisor: number; + commodityKey: string; + commodityGroup: string; + keywords: string[]; + categoryHints: string[]; + conversions: Partial>; +}; + +export type BenchmarkSnapshot = BenchmarkDefinition & { + benchmarkPrice: number | null; + confidence: number | null; + exponent: number | null; + publisherCount: number; + marketSession: string; + timestampUs: string | null; + source: PriceSource; +}; + +export type BenchmarkHistoryPoint = { + timestampUs: string; + price: number | null; + source: Exclude; +}; + +export type CardanoCapability = { + cardanoEnabled: boolean; + pythCardanoSdkInstalled: boolean; + lucidAvailable: boolean; + note: string; +}; + +export type NormalizedSupplierOfferInput = { + catalogProductId?: string; + productName: string; + categoryRoot?: string; + baseUnit?: "kg" | "lb" | "bushel" | "cwt" | "unit" | "l"; + baseQuantity?: number; + baseUnitPrice?: number; + currency?: string; + supplierSourceId?: string; + supplierName?: string; +}; + +export type CommodityBenchmarkResult = { + item: NormalizedSupplierOfferInput; + benchmarkStatus: BenchmarkStatus; + comparisonStatus: ComparisonStatus; + benchmarkKind: BenchmarkKind; + benchmarkRef?: { + id: number; + symbol: string; + displayName: string; + benchmarkUnit: SupportedUnit; + source: PriceSource; + timestampUs: string | null; + marketSession: string; + }; + internationalPriceUsd?: number; + comparisonPriceUsd?: number; + comparisonUnit?: SupportedUnit; + markupPercent?: number; + explanation: string; +}; diff --git a/lazer/cardano/gastro-benchmark/src/types/lucid-cardano.d.ts b/lazer/cardano/gastro-benchmark/src/types/lucid-cardano.d.ts new file mode 100644 index 00000000..de9bd404 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/src/types/lucid-cardano.d.ts @@ -0,0 +1,19 @@ +declare module "lucid-cardano" { + export const Data: any & { + Object: (...args: any[]) => any; + Bytes: (...args: any[]) => any; + Integer: (...args: any[]) => any; + Static: any; + to: (...args: any[]) => any; + void: () => any; + }; + export const Constr: any; + export const fromText: any; + export class Blockfrost { + constructor(url: string, key: string); + } + export class Lucid { + static new(provider: unknown, network: string): Promise; + } + export type Script = any; +} diff --git a/lazer/cardano/gastro-benchmark/test.ts b/lazer/cardano/gastro-benchmark/test.ts new file mode 100644 index 00000000..687d2e56 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/test.ts @@ -0,0 +1,117 @@ +import assert from "assert"; +import { BenchmarkService } from "./src/benchmark-service"; +import { GASTRONOMY_BENCHMARKS } from "./src/benchmark-catalog"; +import type { BenchmarkDefinition, BenchmarkHistoryPoint, BenchmarkSnapshot } from "./src/types"; +import type { PriceProvider } from "./src/pyth"; + +class MockPriceProvider implements PriceProvider { + constructor(private readonly snapshots: BenchmarkSnapshot[]) {} + + async getLatestSnapshot(feeds: BenchmarkDefinition[]): Promise { + return feeds.map((feed) => this.snapshots.find((snapshot) => snapshot.id === feed.id) ?? { + ...feed, + benchmarkPrice: null, + confidence: null, + exponent: null, + publisherCount: 0, + marketSession: "closed", + timestampUs: null, + source: "unavailable", + }); + } + + async getHistoricalFallback(feed: BenchmarkDefinition): Promise { + return this.snapshots.find((snapshot) => snapshot.id === feed.id) ?? null; + } + + async getHistory(feed: BenchmarkDefinition, points: number): Promise { + return Array.from({ length: points }, (_, index) => ({ + timestampUs: String(1_700_000_000_000_000 + index), + price: this.snapshots.find((snapshot) => snapshot.id === feed.id)?.benchmarkPrice ?? null, + source: "historical-fallback", + })); + } + + async shutdown(): Promise { + return; + } +} + +function buildSnapshot(id: number, benchmarkPrice: number, source: "latest" | "historical-fallback" = "latest"): BenchmarkSnapshot { + const feed = GASTRONOMY_BENCHMARKS.find((item) => item.id === id); + if (!feed) { + throw new Error(`Missing benchmark ${id}`); + } + + return { + ...feed, + benchmarkPrice, + confidence: 0.01, + exponent: -2, + publisherCount: 12, + marketSession: source === "latest" ? "open" : "closed", + timestampUs: "1700000000000000", + source, + }; +} + +async function main() { + const service = new BenchmarkService( + new MockPriceProvider([ + buildSnapshot(3018, 4.5), + buildSnapshot(3015, 0.25, "historical-fallback"), + ]), + ); + + const [cornResult, sugarResult, uncoveredResult] = await service.compareOffers([ + { + productName: "Harina de maiz amarilla", + categoryRoot: "Harinas", + baseUnit: "kg", + baseUnitPrice: 0.2, + currency: "USD", + supplierName: "Molino Norte", + }, + { + productName: "Azucar refinada", + categoryRoot: "Reposteria", + baseUnit: "kg", + baseUnitPrice: 0.7, + currency: "USD", + supplierName: "Dulce Sur", + }, + { + productName: "Queso muzzarella", + categoryRoot: "Lacteos", + baseUnit: "kg", + baseUnitPrice: 5.2, + currency: "USD", + supplierName: "Lacteos del Plata", + }, + ]); + + assert.equal(cornResult.benchmarkStatus, "matched"); + assert.equal(cornResult.comparisonStatus, "watch"); + assert.equal(cornResult.comparisonUnit, "usd/kg"); + assert.ok(cornResult.comparisonPriceUsd !== undefined); + assert.ok(cornResult.markupPercent !== undefined); + + assert.equal(sugarResult.benchmarkStatus, "market_closed_fallback_used"); + assert.equal(sugarResult.comparisonStatus, "expensive"); + + assert.equal(uncoveredResult.benchmarkStatus, "no_pyth_cardano_coverage"); + assert.equal(uncoveredResult.comparisonStatus, "no_benchmark"); + + const history = await service.getBenchmarkHistory(3018, 3); + assert.equal(history.history.length, 3); + + const health = await service.getHealth(); + assert.equal(health.cardanoBase.cardanoEnabled, true); + + console.log("All tests passed"); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/lazer/cardano/gastro-benchmark/tsconfig.json b/lazer/cardano/gastro-benchmark/tsconfig.json new file mode 100644 index 00000000..3bcd6f59 --- /dev/null +++ b/lazer/cardano/gastro-benchmark/tsconfig.json @@ -0,0 +1,39 @@ +{ + // Visit https://aka.ms/tsconfig to read more about this file + "compilerOptions": { + // File Layout + // "rootDir": "./src", + // "outDir": "./dist", + + // Environment Settings + "module": "commonjs", + "target": "es2020", + "types": ["node"], + "lib": ["es2020"], + "esModuleInterop": true, + + // Other Outputs + "sourceMap": true, + "declaration": true, + "declarationMap": true, + + // Stricter Typechecking Options + "noUncheckedIndexedAccess": false, + "exactOptionalPropertyTypes": false, + + // Style Options + // "noImplicitReturns": true, + // "noImplicitOverride": true, + // "noUnusedLocals": true, + // "noUnusedParameters": true, + // "noFallthroughCasesInSwitch": true, + // "noPropertyAccessFromIndexSignature": true, + + // Recommended Options + "strict": true, + "jsx": "react-jsx", + "skipLibCheck": true, + "resolveJsonModule": true, + "moduleResolution": "node", + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..76a383a6 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,236 @@ +{ + "name": "pyth-examples", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@emurgo/cardano-serialization-lib-nodejs": "^15.0.3", + "lucid-cardano": "^0.10.11" + } + }, + "node_modules/@emurgo/cardano-serialization-lib-nodejs": { + "version": "15.0.3", + "resolved": "https://registry.npmjs.org/@emurgo/cardano-serialization-lib-nodejs/-/cardano-serialization-lib-nodejs-15.0.3.tgz", + "integrity": "sha512-CZkAF7P3Ip3gUCAa6v93DLKp9hGqsfE6F/b3Qrqvym7rEJKi3j+dDk/OPxtmynWWGHhqXL85vzFOOwVduhLAjA==", + "license": "MIT" + }, + "node_modules/@peculiar/asn1-schema": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", + "license": "MIT", + "dependencies": { + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/json-schema": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", + "integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==", + "license": "MIT", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@peculiar/webcrypto": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@peculiar/webcrypto/-/webcrypto-1.5.0.tgz", + "integrity": "sha512-BRs5XUAwiyCDQMsVA9IDvDa7UBR9gAvPHgugOeGng3YN6vJ9JYonyDc0lNczErgtCWtucjR5N7VtaonboD/ezg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.8", + "@peculiar/json-schema": "^1.1.12", + "pvtsutils": "^1.3.5", + "tslib": "^2.6.2", + "webcrypto-core": "^1.8.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/asn1js": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.0.7.tgz", + "integrity": "sha512-uLvq6KJu04qoQM6gvBfKFjlh6Gl0vOKQuR5cJMDHQkmwfMOQeN3F3SHCv9SNYSL+CRoHvOGFfllDlVz03GQjvQ==", + "license": "BSD-3-Clause", + "dependencies": { + "pvtsutils": "^1.3.6", + "pvutils": "^1.1.3", + "tslib": "^2.8.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/lucid-cardano": { + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/lucid-cardano/-/lucid-cardano-0.10.11.tgz", + "integrity": "sha512-bpfrLQjpathPAH/N+BMXfLzp+O5P7LtZjg6aaVsC3EUfRo7I9Y85ZIhxpfEIati+CcpRviWRVMKcAlDnPuPFkA==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "license": "MIT", + "dependencies": { + "@peculiar/webcrypto": "^1.4.0", + "node-fetch": "^3.2.3", + "ws": "^8.10.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/pvtsutils": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/pvtsutils/-/pvtsutils-1.3.6.tgz", + "integrity": "sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.8.1" + } + }, + "node_modules/pvutils": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/pvutils/-/pvutils-1.1.5.tgz", + "integrity": "sha512-KTqnxsgGiQ6ZAzZCVlJH5eOjSnvlyEgx1m8bkRJfOhmGRqfo5KLvmAlACQkrjEtOQ4B7wF9TdSLIs9O90MX9xA==", + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webcrypto-core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.8.1.tgz", + "integrity": "sha512-P+x1MvlNCXlKbLSOY4cYrdreqPG5hbzkmawbcXLKN/mf6DZW0SdNNkZ+sjwsqVkI4A4Ko2sPZmkZtCKY58w83A==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.3.13", + "@peculiar/json-schema": "^1.1.12", + "asn1js": "^3.0.5", + "pvtsutils": "^1.3.5", + "tslib": "^2.7.0" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..9f8502d9 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@emurgo/cardano-serialization-lib-nodejs": "^15.0.3", + "lucid-cardano": "^0.10.11" + } +}