diff --git a/lazer/cardano/OSI/README.md b/lazer/cardano/OSI/README.md new file mode 100644 index 00000000..dc65ffb8 --- /dev/null +++ b/lazer/cardano/OSI/README.md @@ -0,0 +1,123 @@ +# OSI: Oracle-Settled Invoice + +Oracle-Settled Invoice is a Cardano escrow that locks one asset and settles a liability indexed to another using Pyth prices. + +This project is a practical blueprint for building oracle-driven settlement flows on Cardano with Pyth. It combines Aiken on-chain validators with a typeScript off-chain client, so you can go from contract logic to real transaction execution with minimal setup. + +## Sections + +- [Introduction](../OSI/README.md#introduction) +- [Project structure](../OSI/README.md#project-structure) +- [Setup](../OSI/README.md#setup) +- [Running instructions](../OSI/README.md#running-instructions) + - [CLI](../OSI/README.md#1-run-the-cli) + - [Web page](../OSI/README.md#2-run-the-webpage) + +## Introduction + +This project addresses a common payments challenge: obligations are often defined in one currency, while treasury and disbursement happen in another. In OSI (Oracle-Settled Invoice), the payable amount is denominated in a quote asset (USDT in this PoC), but settlement is executed in ADA using real-time oracle conversion. + +For this proof-of-concept, we use the USDT/USD and ADA/USD feeds. At settlement time, the transaction consumes signed Pyth price updates and computes the ADA amount required to satisfy each USDT-denominated payout. This enables deterministic and transparent cross-currency settlement directly on-chain. + +The `off-chain` component in OSI/off-chain/e2e.ts uses Pyth’s SDK to fetch signed price updates and Evolution SDK to construct the transaction. The `on-chain` validator in OSI/on-chain/validators/osi.ak, using Pyth’s Aiken library, verifies those updates at execution time (including authenticity and timing) before allowing payout. This ensures the ADA delivered matches the expected USDT value according to trusted oracle data, reducing exchange-rate dispute risk between payer and payee. + +The same pattern can support many real use cases, such as: + +- payroll where compensation is defined in USD but paid in ADA; +- vendor invoices denominated in fiat and settled in crypto; +- scheduled future payments indexed to a CNT pegged to an RWA, while settlement occurs in another on-chain asset. + +## Project structure + +```bash +lazer/cardano/OSI/ +├── on-chain/ +├── off-chain/ +├── web-ui/ +└── README.md +``` + +- `on-chain`: smart contract implementation +- `off-chain`: transaction building and connection with Pyth +- `web-ui`: dApp connection with browser wallet (Eternl recommended) +- `README.md`: project explanation and running instructions + +## Setup + +### 1. Build contracts + +```bash +cd on-chain +aiken build +``` + +### 2. Set environment variables + +Create a `.env` file in the `off-chain` directory with the following variables: + +- `LAZER_TOKEN` - The token that provides access to the Pyth API. +- `NETWORK` - Cardano network (recommended `preprod`) +- `PROVIDER_TYPE` - Cardano provider (recommended Blockfrost) +- `BLOCKFROST_BASE_URL` - The base URL for the Blockfrost API. For preprod, this is `https://cardano-preprod.blockfrost.io/api/v0`. +- `BLOCKFROST_PROJECT_ID` - Your Blockfrost project ID for the preprod network. +- `WALLET_MNEMONIC` - The mnemonic phrase for the wallet that will be used to interact with the contracts. This wallet should have some ADA on the preprod network. +- `PYTH_POLICY_ID` - The policy ID of the Pyth on the Cardano preprod network. + +### 3. Install off-chain deps + +```bash +cd off-chain +bun install +``` + +## Running instructions + +There are two ways to interact with the dApp: + +### 1. Run the CLI + +The CLI allows the creation and payout of the escrows. + +#### 1.1 Start the escrow + +Creates an UTxO allocating minAda and including the list of payees in the datum + +```bash +bun run create-validator-utxo +``` + +![Create escrow](../OSI/img/create_escrow.png) + +#### 1.2 Fund the escrow + +Adds the funds to pay out later + +```bash +bun run add-fund-validator-utxo +``` + +![Fund escrow](../OSI/img/add_funds.png) + +#### 1.3 Pay out + +Pays out the expected amount of ADA to the payees specified in the datum + +```bash +bun run spend-validator-utxo +``` + +![Pay out](../OSI/img/pay_out.png) + +### 2. [Experimental] Run the webpage + +Lift a webpage that will be available on `localhost:3000`. + +```bash +cd web-ui +bun install +bun run dev +``` + +![Web](../OSI/img/web.png) + +The web implementation can be accessed on [this branch](https://github.com/lolaaimar/pyth-examples/tree/integration). \ No newline at end of file diff --git a/lazer/cardano/OSI/img/add_funds.png b/lazer/cardano/OSI/img/add_funds.png new file mode 100644 index 00000000..7f653e04 Binary files /dev/null and b/lazer/cardano/OSI/img/add_funds.png differ diff --git a/lazer/cardano/OSI/img/create_escrow.png b/lazer/cardano/OSI/img/create_escrow.png new file mode 100644 index 00000000..e6f6c045 Binary files /dev/null and b/lazer/cardano/OSI/img/create_escrow.png differ diff --git a/lazer/cardano/OSI/img/pay_out.png b/lazer/cardano/OSI/img/pay_out.png new file mode 100644 index 00000000..70cf19cd Binary files /dev/null and b/lazer/cardano/OSI/img/pay_out.png differ diff --git a/lazer/cardano/OSI/img/web.png b/lazer/cardano/OSI/img/web.png new file mode 100644 index 00000000..d6d712d4 Binary files /dev/null and b/lazer/cardano/OSI/img/web.png differ diff --git a/lazer/cardano/OSI/off-chain/.gitignore b/lazer/cardano/OSI/off-chain/.gitignore new file mode 100644 index 00000000..a14702c4 --- /dev/null +++ b/lazer/cardano/OSI/off-chain/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/lazer/cardano/OSI/off-chain/README.md b/lazer/cardano/OSI/off-chain/README.md new file mode 100644 index 00000000..7117596f --- /dev/null +++ b/lazer/cardano/OSI/off-chain/README.md @@ -0,0 +1,50 @@ +# off-chain + +To install dependencies: + +```bash +bun install +``` + +Minimal e2e commands: + +```bash +bun run create-validator-utxo +bun run add-fund-validator-utxo [outRef] +bun run spend-validator-utxo +bun run evaluate-ogmios +bun run fetch-ada-usdt-update +``` + +You can also pass an explicit out-ref as `#`. + +### Fund Operation + +Add funds to an existing OSI UTxO while preserving the deadline and payees: + +```bash +# Add 1000000 lovelace to the first validator UTxO +bun run add-fund-validator-utxo 1000000 + +# Add 2000000 lovelace to a specific UTxO +bun run add-fund-validator-utxo 2000000 # +``` + +The Fund operation: +- Takes an existing OSI UTxO from the script address +- Adds additional ADA (lovelace) from your wallet +- Returns the combined funds to the same script address with the original datum unchanged +- Preserves the deadline and payees for later Payout operations + +Required environment variables: + +- `NETWORK` = `preprod`, `preview`, or `mainnet` +- `WALLET_MNEMONIC` +- `PYTH_POLICY_ID` +- `LAZER_TOKEN` +- `VALIDATOR_LOVELACE` (defaults to `5000000`) +- provider config: + - `PROVIDER_TYPE=blockfrost` with `BLOCKFROST_BASE_URL` and `BLOCKFROST_PROJECT_ID` + - or `PROVIDER_TYPE=kupmios` with `KUPO_URL` and `OGMIOS_URL` + - or `PROVIDER_TYPE=maestro` with `MAESTRO_BASE_URL` and `MAESTRO_API_KEY` + - or `PROVIDER_TYPE=koios` with `KOIOS_BASE_URL` and optional `KOIOS_TOKEN` diff --git a/lazer/cardano/OSI/off-chain/bun.lock b/lazer/cardano/OSI/off-chain/bun.lock new file mode 100644 index 00000000..994bb32e --- /dev/null +++ b/lazer/cardano/OSI/off-chain/bun.lock @@ -0,0 +1,162 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "off-chain", + "dependencies": { + "@evolution-sdk/evolution": "^0.3.30", + "@pythnetwork/pyth-lazer-cardano-js": "^0.1.0", + "@pythnetwork/pyth-lazer-sdk": "^6.2.1", + "dotenv": "^17.3.1", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@effect/cluster": ["@effect/cluster@0.48.16", "", { "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" } }, "sha512-ZZkrSMVetOvlRDD8mPCX3IcVJtvUZBp6++lUKNGIT6LRIObRP4lVwtei85Z+4g49WpeLvJnSdH0zjPtGieFDHQ=="], + + "@effect/experimental": ["@effect/experimental@0.54.6", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/platform": "^0.90.2", "effect": "^3.17.7", "ioredis": "^5", "lmdb": "^3" }, "optionalPeers": ["ioredis", "lmdb"] }, "sha512-UqHMvCQmrZT6kUVoUC0lqyno4Yad+j9hBGCdUjW84zkLwAq08tPqySiZUKRwY+Ae5B2Ab8rISYJH7nQvct9DMQ=="], + + "@effect/platform": ["@effect/platform@0.90.10", "", { "dependencies": { "find-my-way-ts": "^0.1.6", "msgpackr": "^1.11.4", "multipasta": "^0.2.7" }, "peerDependencies": { "effect": "^3.17.13" } }, "sha512-QhDPgCaLfIMQKOCoCPQvRUS+Y34iYJ07jdZ/CBAvYFvg/iUBebsmFuHL63RCD/YZH9BuK/kqqLYAA3M0fmUEgg=="], + + "@effect/platform-node": ["@effect/platform-node@0.96.1", "", { "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" } }, "sha512-4nfB/XRJJ246MCdI7klTE/aVvA9txfI83RnymS7pNyoG4CXUKELi87JrkrWFTtOlewzt5UMWpmqsFmm2qHxx3A=="], + + "@effect/platform-node-shared": ["@effect/platform-node-shared@0.49.2", "", { "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" } }, "sha512-uYlQi2swDV9hdHatr2Onov3G+VlEF+3+Qm9dvdOZiZNE1bVqvs/zs6LVT8Yrz/3Vq/4JPzGcN+acx0iiJo5ZVw=="], + + "@effect/rpc": ["@effect/rpc@0.69.5", "", { "peerDependencies": { "@effect/platform": "^0.90.10", "effect": "^3.17.14" } }, "sha512-LLCZP/aiaW4HeoIaoZuVZpJb/PFCwdJP21b3xP6l+1yoRVw8HlKYyfy/outRCF+BT4ndtY0/utFSeGWC21Qr7w=="], + + "@effect/sql": ["@effect/sql@0.44.2", "", { "dependencies": { "uuid": "^11.0.3" }, "peerDependencies": { "@effect/experimental": "^0.54.6", "@effect/platform": "^0.90.4", "effect": "^3.17.7" } }, "sha512-DEcvriHvj88zu7keruH9NcHQzam7yQzLNLJO6ucDXMCAwWzYZSJOsmkxBznRFv8ylFtccSclKH2fuj+wRKPjCQ=="], + + "@effect/workflow": ["@effect/workflow@0.9.6", "", { "peerDependencies": { "@effect/platform": "^0.90.10", "@effect/rpc": "^0.69.4", "effect": "^3.17.14" } }, "sha512-uPBpSJ8NYwYA6VLZovfejwNik+2kAaoDtlPi+VTlxFMscWNYx+xlGiRg8CO/oa2pHCwkJYjOI27SGOlUawiz1w=="], + + "@evolution-sdk/evolution": ["@evolution-sdk/evolution@0.3.30", "", { "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" } }, "sha512-8DMgxNdjWCEhCu8sMKz/ikY4qfGmoW8vBPT53Dji7i5PmHYMXLuVk0Nt5mNpIovujwSbC271GIJQo2pINtQsxg=="], + + "@isaacs/ttlcache": ["@isaacs/ttlcache@1.4.1", "", {}, "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA=="], + + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], + + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.3", "", { "os": "linux", "cpu": "arm" }, "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw=="], + + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg=="], + + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.3", "", { "os": "linux", "cpu": "x64" }, "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg=="], + + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3", "", { "os": "win32", "cpu": "x64" }, "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ=="], + + "@noble/curves": ["@noble/curves@2.0.1", "", { "dependencies": { "@noble/hashes": "2.0.1" } }, "sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw=="], + + "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], + + "@parcel/watcher": ["@parcel/watcher@2.5.6", "", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "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" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], + + "@parcel/watcher-android-arm64": ["@parcel/watcher-android-arm64@2.5.6", "", { "os": "android", "cpu": "arm64" }, "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A=="], + + "@parcel/watcher-darwin-arm64": ["@parcel/watcher-darwin-arm64@2.5.6", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA=="], + + "@parcel/watcher-darwin-x64": ["@parcel/watcher-darwin-x64@2.5.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg=="], + + "@parcel/watcher-freebsd-x64": ["@parcel/watcher-freebsd-x64@2.5.6", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng=="], + + "@parcel/watcher-linux-arm-glibc": ["@parcel/watcher-linux-arm-glibc@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ=="], + + "@parcel/watcher-linux-arm-musl": ["@parcel/watcher-linux-arm-musl@2.5.6", "", { "os": "linux", "cpu": "arm" }, "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg=="], + + "@parcel/watcher-linux-arm64-glibc": ["@parcel/watcher-linux-arm64-glibc@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA=="], + + "@parcel/watcher-linux-arm64-musl": ["@parcel/watcher-linux-arm64-musl@2.5.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA=="], + + "@parcel/watcher-linux-x64-glibc": ["@parcel/watcher-linux-x64-glibc@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ=="], + + "@parcel/watcher-linux-x64-musl": ["@parcel/watcher-linux-x64-musl@2.5.6", "", { "os": "linux", "cpu": "x64" }, "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg=="], + + "@parcel/watcher-win32-arm64": ["@parcel/watcher-win32-arm64@2.5.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q=="], + + "@parcel/watcher-win32-ia32": ["@parcel/watcher-win32-ia32@2.5.6", "", { "os": "win32", "cpu": "ia32" }, "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g=="], + + "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], + + "@pythnetwork/pyth-lazer-cardano-js": ["@pythnetwork/pyth-lazer-cardano-js@0.1.0", "", { "dependencies": { "@evolution-sdk/evolution": "^0.3.29" } }, "sha512-KfddoNfkRf+a6Qqf1zKGkMyKK9Mgib6P5hLVApc6qB3XpLkzk9GfPLOc9q2uqCF0EOjzPRDRTMF9beKMxyAK4Q=="], + + "@pythnetwork/pyth-lazer-sdk": ["@pythnetwork/pyth-lazer-sdk@6.2.1", "", { "dependencies": { "@isaacs/ttlcache": "^1.4.1", "buffer": "^6.0.3", "isomorphic-ws": "^5.0.0", "ts-log": "^2.2.7", "ws": "^8.19.0" } }, "sha512-+d+ATApOBF5z3YvqwP/5R42xr9vWpLOvbAFWDWldYiltlH8eU9PaGgeczgCs3it3STpnL+8jTXsUBhqv9T94Aw=="], + + "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], + + "@scure/bip32": ["@scure/bip32@1.7.0", "", { "dependencies": { "@noble/curves": "~1.9.0", "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw=="], + + "@scure/bip39": ["@scure/bip39@1.6.0", "", { "dependencies": { "@noble/hashes": "~1.8.0", "@scure/base": "~1.2.5" } }, "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A=="], + + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@types/bip39": ["@types/bip39@3.0.4", "", { "dependencies": { "bip39": "*" } }, "sha512-kgmgxd14vTUMqcKu/gRi7adMchm7teKnOzdkeP0oQ5QovXpbUJISU0KUtBt84DdxCws/YuNlSCIoZqgXexe6KQ=="], + + "@types/bun": ["@types/bun@1.3.11", "", { "dependencies": { "bun-types": "1.3.11" } }, "sha512-5vPne5QvtpjGpsGYXiFyycfpDF2ECyPcTSsFBMa0fraoxiQyMJ3SmuQIGhzPg2WJuWxVBoxWJ2kClYTcw/4fAg=="], + + "@types/node": ["@types/node@25.5.0", "", { "dependencies": { "undici-types": "~7.18.0" } }, "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bip39": ["bip39@3.1.0", "", { "dependencies": { "@noble/hashes": "^1.2.0" } }, "sha512-c9kiwdk45Do5GL0vJMe7tS95VjCii65mYAH7DfWl3uW8AVzXKQVUm64i3hzVybBDMp9r7j9iNxR85+ul8MdN/A=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "bun-types": ["bun-types@1.3.11", "", { "dependencies": { "@types/node": "*" } }, "sha512-1KGPpoxQWl9f6wcZh57LvrPIInQMn2TQ7jsgxqpRzg+l0QPOFvJVH7HmvHo/AiPgwXy+/Thf6Ov3EdVn1vOabg=="], + + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + + "dotenv": ["dotenv@17.3.1", "", {}, "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA=="], + + "effect": ["effect@3.21.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ=="], + + "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + + "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + + "isomorphic-ws": ["isomorphic-ws@5.0.0", "", { "peerDependencies": { "ws": "*" } }, "sha512-muId7Zzn9ywDsyXgTIafTry2sV3nySZeUDe6YedVd1Hvuuep5AsIlqK+XefWpYTyJG5e503F2xIuT2lcU6rCSw=="], + + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="], + + "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "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" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], + + "multipasta": ["multipasta@0.2.7", "", {}, "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA=="], + + "node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "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" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + + "picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="], + + "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + + "ts-log": ["ts-log@2.2.7", "", {}, "sha512-320x5Ggei84AxzlXp91QkIGSw5wgaLT6GeAH0KsqDmRZdVWW2OiSeVvElVoatk3f7nicwXlElXsoFkARiGE2yg=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici": ["undici@7.24.5", "", {}, "sha512-3IWdCpjgxp15CbJnsi/Y9TCDE7HWVN19j1hmzVhoAkY/+CJx449tVxT5wZc1Gwg8J+P0LWvzlBzxYRnHJ+1i7Q=="], + + "undici-types": ["undici-types@7.18.2", "", {}, "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w=="], + + "uuid": ["uuid@11.1.0", "", { "bin": { "uuid": "dist/esm/bin/uuid" } }, "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A=="], + + "ws": ["ws@8.20.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + + "@noble/curves/@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + + "@scure/bip32/@noble/curves": ["@noble/curves@1.9.7", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw=="], + } +} diff --git a/lazer/cardano/OSI/off-chain/e2e.ts b/lazer/cardano/OSI/off-chain/e2e.ts new file mode 100644 index 00000000..2062dc0d --- /dev/null +++ b/lazer/cardano/OSI/off-chain/e2e.ts @@ -0,0 +1,465 @@ +import { Buffer } from "node:buffer"; +import { readFile } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +import dotenv from "dotenv"; +import { + Address, + Assets, + Cardano, + Data, + ScriptHash, + TransactionHash, + UPLC, + UTxO, + createClient, + type ProviderConfig, + type ProviderOnlyClient, + type SigningClient, +} from "@evolution-sdk/evolution"; +import { + PythLazerClient, + type JsonUpdate, + type PriceFeedProperty, +} from "@pythnetwork/pyth-lazer-sdk"; + +const OFF_CHAIN_DIR = dirname(fileURLToPath(import.meta.url)); +dotenv.config({ path: join(OFF_CHAIN_DIR, ".env") }); + +const PLUTUS_JSON_PATH = join(OFF_CHAIN_DIR, "../on-chain/plutus.json"); +const VALIDATOR_TITLE = "osi.oracle_settled_invoice.spend"; +const ADA_USD_FEED_ID = 16; +const USDT_USD_FEED_ID = 8; + +const DEFAULT_LAZER_PROPERTIES = [ + "price", + "exponent", + "feedUpdateTimestamp", +] as const satisfies readonly PriceFeedProperty[]; + +const DEFAULT_LAZER_WS_URLS = [ + "wss://pyth-lazer-0.dourolabs.app/v1/stream", + "wss://pyth-lazer-1.dourolabs.app/v1/stream", + "wss://pyth-lazer-2.dourolabs.app/v1/stream", +] as const; + +type NetworkName = "mainnet" | "preprod" | "preview"; + +type BlueprintValidator = { + title: string; + compiledCode: string; + hash?: string; + parameters?: unknown[]; +}; + +type Blueprint = { + validators: BlueprintValidator[]; +}; + +export type OsiRuntime = { + client: SigningClient; + providerClient: ProviderOnlyClient; + network: NetworkName; + feedId: number; + queryFeedIds: number[]; + pythPolicyId: string; + fundingLovelace: bigint; + validator: { + script: Cardano.PlutusV3.PlutusV3; + scriptHash: ScriptHash.ScriptHash; + address: Address.Address; + }; +}; + +export type PythSignedUpdate = { + signedUpdate: Buffer; + signedUpdateHex: string; + parsed: JsonUpdate["parsed"]; +}; + +export async function loadRuntimeFromEnv(): Promise { + const network = readNetwork(); + const pythPolicyId = readRequiredEnv("PYTH_POLICY_ID"); + const feedId = ADA_USD_FEED_ID; + const queryFeedIds = [ADA_USD_FEED_ID, USDT_USD_FEED_ID]; + const fundingLovelace = 500_000_000n; + const providerConfig = readProviderConfig(); + const client = createSigningClientFromEnv(network, providerConfig); + const providerClient = createProviderOnlyClient(network, providerConfig); + const validator = await loadParameterizedValidator({ + network, + pythPolicyId, + baseAssetId: BigInt(USDT_USD_FEED_ID), + }); + + return { + client, + providerClient, + network, + feedId, + queryFeedIds, + pythPolicyId, + fundingLovelace, + validator, + }; +} + +export async function loadFirstValidatorUtxo( + client: SigningClient, + address: Address.Address, +): Promise { + const utxos = await client.getUtxos(address); + const [utxo] = utxos; + + if (!utxo) { + throw new Error( + `No UTxOs found at validator address ${Address.toBech32(address)}`, + ); + } + + return utxo; +} + +export async function loadValidatorUtxoByTxHash( + client: SigningClient, + address: Address.Address, + transactionIdHex: string, + index?: bigint, +): Promise { + const utxos = await client.getUtxos(address); + const matches = utxos.filter((utxo) => { + if (TransactionHash.toHex(utxo.transactionId) !== transactionIdHex) { + return false; + } + + return index === undefined || utxo.index === index; + }); + + const [utxo] = matches; + + if (!utxo) { + const target = + index === undefined + ? transactionIdHex + : `${transactionIdHex}#${index.toString()}`; + + throw new Error( + `No validator UTxO found at ${Address.toBech32(address)} for ${target}`, + ); + } + + return utxo; +} + +export function parseCliOutRef(argument: string | undefined): { + transactionIdHex: string; + index?: bigint; +} | null { + if (!argument) { + return null; + } + + const [transactionIdHex, indexRaw] = argument.split("#"); + + if (!transactionIdHex || !/^[0-9a-fA-F]{64}$/.test(transactionIdHex)) { + throw new Error( + "Expected first CLI argument to be a 64-hex transaction hash or #", + ); + } + + if (indexRaw === undefined) { + return { + transactionIdHex: transactionIdHex.toLowerCase(), + }; + } + + if (!/^\d+$/.test(indexRaw)) { + throw new Error("UTxO index must be a non-negative integer"); + } + + return { + transactionIdHex: transactionIdHex.toLowerCase(), + index: BigInt(indexRaw), + }; +} + +export async function fetchLatestSignedUpdate( + token: string, + feedIds: readonly number[], +): Promise { + const client = await PythLazerClient.create({ + token, + webSocketPoolConfig: { + numConnections: DEFAULT_LAZER_WS_URLS.length, + urls: [...DEFAULT_LAZER_WS_URLS], + }, + }); + + try { + const latest = await client.getLatestPrice({ + channel: "fixed_rate@200ms", + priceFeedIds: [...feedIds], + properties: [...DEFAULT_LAZER_PROPERTIES], + formats: ["solana"], + jsonBinaryEncoding: "hex", + parsed: true, + }); + + const signedUpdateHex = latest.solana?.data; + + if (!signedUpdateHex) { + throw new Error("Pyth response did not include a signed solana update"); + } + + return { + signedUpdate: Buffer.from(signedUpdateHex, "hex"), + signedUpdateHex, + parsed: latest.parsed, + }; + } finally { + client.shutdown(); + } +} + +export function makeWithdrawRedeemer(update: Buffer): Data.Data { + return Data.list([update]); +} + +export function formatTxHash(txHash: TransactionHash.TransactionHash): string { + return TransactionHash.toHex(txHash); +} + +export function logDetailedError(error: unknown): void { + console.error("Detailed error:"); + console.dir(normalizeError(error), { depth: null, colors: true }); + + if (error instanceof Error && error.stack) { + console.error("\nStack:"); + console.error(error.stack); + } +} + +export function readLazerToken(): string { + return readRequiredEnv("LAZER_TOKEN"); +} + +export function readWalletMnemonic(): string { + return readRequiredEnv("WALLET_MNEMONIC"); +} + +export function makeLovelace(amount: bigint): Assets.Assets { + return Assets.fromLovelace(amount); +} + +async function loadParameterizedValidator({ + network, + pythPolicyId, + baseAssetId, +}: { + network: NetworkName; + pythPolicyId: string; + baseAssetId: bigint; +}) { + const blueprintRaw = await readFile(PLUTUS_JSON_PATH, "utf8"); + const blueprint = JSON.parse(blueprintRaw) as Blueprint; + const validator = blueprint.validators.find( + (candidate) => candidate.title === VALIDATOR_TITLE, + ); + + if (!validator) { + throw new Error(`Validator ${VALIDATOR_TITLE} not found in plutus.json`); + } + + const scriptBytesHex = + Array.isArray(validator.parameters) && validator.parameters.length > 0 + ? UPLC.applySingleCborEncoding( + UPLC.applyParamsToScript(validator.compiledCode, [ + Buffer.from(pythPolicyId, "hex"), + baseAssetId, + ]), + ) + : validator.compiledCode; + + const script = new Cardano.PlutusV3.PlutusV3({ + bytes: Buffer.from(scriptBytesHex, "hex"), + }); + const scriptHash = ScriptHash.fromScript(script); + const scriptHashHex = ScriptHash.toHex(scriptHash); + + if ( + (!Array.isArray(validator.parameters) || + validator.parameters.length === 0) && + validator.hash && + validator.hash !== scriptHashHex + ) { + throw new Error( + `Loaded validator hash ${scriptHashHex} does not match blueprint hash ${validator.hash}`, + ); + } + + const address = new Address.Address({ + networkId: networkToAddressNetworkId(network), + paymentCredential: scriptHash, + }); + + return { + script, + scriptHash, + address, + }; +} + +function createSigningClientFromEnv( + network: NetworkName, + provider: ProviderConfig, +): SigningClient { + return createClient({ + network, + provider, + wallet: { + type: "seed", + mnemonic: readWalletMnemonic(), + }, + }); +} + +function createProviderOnlyClient( + network: NetworkName, + provider: ProviderConfig, +): ProviderOnlyClient { + return createClient({ + network, + provider, + }); +} + +function readProviderConfig(): ProviderConfig { + const providerType = (process.env.PROVIDER_TYPE?.trim() || "blockfrost") as + | "blockfrost" + | "kupmios" + | "maestro" + | "koios"; + + switch (providerType) { + case "blockfrost": + return { + type: "blockfrost", + baseUrl: readRequiredEnv("BLOCKFROST_BASE_URL"), + projectId: readRequiredEnv("BLOCKFROST_PROJECT_ID"), + }; + case "kupmios": + return { + type: "kupmios", + kupoUrl: readRequiredEnv("KUPO_URL"), + ogmiosUrl: readRequiredEnv("OGMIOS_URL"), + }; + case "maestro": + return { + type: "maestro", + baseUrl: readRequiredEnv("MAESTRO_BASE_URL"), + apiKey: readRequiredEnv("MAESTRO_API_KEY"), + }; + case "koios": + return { + type: "koios", + baseUrl: readRequiredEnv("KOIOS_BASE_URL"), + token: optionalEnv("KOIOS_TOKEN"), + }; + default: + throw new Error( + `Unsupported PROVIDER_TYPE: ${providerType satisfies never}`, + ); + } +} + +function readNetwork(): NetworkName { + const network = (process.env.NETWORK?.trim() || "preprod") as NetworkName; + + if (network !== "mainnet" && network !== "preprod" && network !== "preview") { + throw new Error(`Unsupported NETWORK: ${network}`); + } + + return network; +} + +function networkToAddressNetworkId(network: NetworkName): number { + return network === "mainnet" ? 1 : 0; +} + +function readRequiredEnv(name: string): string { + const value = process.env[name]?.trim(); + + if (!value) { + throw new Error(`${name} is not set in environment`); + } + + return value; +} + +function optionalEnv(name: string): string | undefined { + const value = process.env[name]?.trim(); + return value === undefined || value === "" ? undefined : value; +} + +function readIntEnv(name: string, fallback: number): number { + const raw = process.env[name]?.trim(); + + if (!raw) { + return fallback; + } + + const value = Number(raw); + + if (!Number.isInteger(value) || value < 0) { + throw new Error(`${name} must be a non-negative integer`); + } + + return value; +} + +function normalizeError(error: unknown): unknown { + if (error instanceof Error) { + const errorRecord = error as unknown as Record; + + return { + name: error.name, + message: error.message, + ...errorRecord, + cause: normalizeNestedValue( + (error as unknown as { cause?: unknown }).cause, + ), + }; + } + + return normalizeNestedValue(error); +} + +function normalizeNestedValue(value: unknown): unknown { + if (value === null || value === undefined) { + return value; + } + + if (Array.isArray(value)) { + return value.map(normalizeNestedValue); + } + + if (typeof value === "bigint") { + return value.toString(); + } + + if (value instanceof Uint8Array) { + return Buffer.from(value).toString("hex"); + } + + if (typeof value === "object") { + return Object.fromEntries( + Object.entries(value as Record).map(([key, nested]) => [ + key, + normalizeNestedValue(nested), + ]), + ); + } + + return value; +} diff --git a/lazer/cardano/OSI/off-chain/osi.ts b/lazer/cardano/OSI/off-chain/osi.ts new file mode 100644 index 00000000..22bfb944 --- /dev/null +++ b/lazer/cardano/OSI/off-chain/osi.ts @@ -0,0 +1,137 @@ +import { + Address, + Bytes, + Credential, + Data, + Plutus, + TSchema, + type Data as PlutusData, +} from "@evolution-sdk/evolution"; + +export const OsiPaymentCredentialSchema = Plutus.Credential.PaymentCredential; + +export const OsiDatumSchema = TSchema.Struct( + { + deadline: TSchema.Integer, + payees: TSchema.Map(OsiPaymentCredentialSchema, TSchema.Integer), + }, + { index: 0 }, +); + +export const OsiRedeemerSchema = TSchema.Variant({ + Fund: {}, + Payout: {}, +}); + +export const OsiDatumCodec = Data.withSchema(OsiDatumSchema); +export const OsiRedeemerCodec = Data.withSchema(OsiRedeemerSchema); + +export type OsiPaymentCredential = typeof OsiPaymentCredentialSchema.Type; +export type OsiDatum = typeof OsiDatumSchema.Type; +export type OsiRedeemer = typeof OsiRedeemerSchema.Type; + +export type OsiExamplePayee = { + address: string; + paymentKeyHash: string; + quoteAmount: bigint; +}; + +export const EXAMPLE_PREPROD_PAYEES: readonly OsiExamplePayee[] = [ + { + address: "addr_test1vz7upneaquh4td8kycdyr8d4x9cdvv7y99j2kzun0rzxx9slapnsh", + paymentKeyHash: "bdc0cf3d072f55b4f6261a419db53170d633c42964ab0b9378c46316", + quoteAmount: 10_000_000n, + }, + { + address: "addr_test1vrnqcm69r8mmcgp000mp8scyum6kgqndhgmmzcqyw085ctqztm5sn", + paymentKeyHash: "e60c6f4519f7bc202f7bf613c304e6f564026dba37b1600473cf4c2c", + quoteAmount: 10_000_000n, + }, + { + address: "addr_test1vzx8f6qul48nddykx8zwqxzrhftyyfnfjv9atxhxtdl926cjud93m", + paymentKeyHash: "8c74e81cfd4f36b49631c4e01843ba56422669930bd59ae65b7e556b", + quoteAmount: 10_000_000n, + }, + { + address: "addr_test1vrf6sj0emxffff7w42ksy3pvtcymmq4rsvxzsfxs7erf9tc7mvj5y", + paymentKeyHash: "d3a849f9d99294a7ceaaad02442c5e09bd82a3830c2824d0f64692af", + quoteAmount: 10_000_000n, + }, + { + address: "addr_test1vrjqqz0dty8xv86hyw5ddwj3lkkzxk057hltkqgxhsg2dcgt8rk5s", + paymentKeyHash: "e40009ed590e661f5723a8d6ba51fdac2359f4f5febb0106bc10a6e1", + quoteAmount: 10_000_000n, + }, +] as const; + +export function makeVerificationKeyCredential( + verificationKeyHashHex: string, +): OsiPaymentCredential { + return { + VerificationKey: { + hash: Bytes.fromHex(verificationKeyHashHex), + }, + }; +} + +export function makeScriptCredential(scriptHashHex: string): OsiPaymentCredential { + return { + Script: { + hash: Bytes.fromHex(scriptHashHex), + }, + }; +} + +export function makeOsiDatumData(datum: OsiDatum): PlutusData.Data { + return OsiDatumCodec.toData(datum); +} + +export function decodeOsiDatumData(data: Data.Constr): OsiDatum { + return OsiDatumCodec.fromData(data); +} + +export function makeEmptyOsiDatum(deadline: bigint): OsiDatum { + return { + deadline, + payees: new Map(), + }; +} + +export function makeExamplePreprodOsiDatum(deadline: bigint): OsiDatum { + return { + deadline, + payees: new Map( + EXAMPLE_PREPROD_PAYEES.map((payee) => [ + makeVerificationKeyCredential(payee.paymentKeyHash), + payee.quoteAmount, + ]), + ), + }; +} + +export function makeFundRedeemerData(): PlutusData.Data { + return OsiRedeemerCodec.toData({ Fund: {} }); +} + +export function makePayoutRedeemerData(): PlutusData.Data { + return OsiRedeemerCodec.toData({ Payout: {} }); +} + +export function paymentCredentialToAddress( + paymentCredential: OsiPaymentCredential, + networkId: 0 | 1, +): Address.Address { + if ("VerificationKey" in paymentCredential) { + return new Address.Address({ + networkId, + paymentCredential: Credential.makeKeyHash( + paymentCredential.VerificationKey.hash, + ), + }); + } + + return new Address.Address({ + networkId, + paymentCredential: Credential.makeScriptHash(paymentCredential.Script.hash), + }); +} diff --git a/lazer/cardano/OSI/off-chain/package.json b/lazer/cardano/OSI/off-chain/package.json new file mode 100644 index 00000000..cc2461ef --- /dev/null +++ b/lazer/cardano/OSI/off-chain/package.json @@ -0,0 +1,25 @@ +{ + "name": "off-chain", + "type": "module", + "private": true, + "scripts": { + "add-fund-validator-utxo": "bun run scripts/add-fund-validator-utxo.ts", + "create-validator-utxo": "bun run scripts/create-validator-utxo.ts", + "evaluate-ogmios": "bun run scripts/evaluate-ogmios.ts", + "fetch-ada-usdt-update": "bun run scripts/fetch-ada-usdt-update.ts", + "spend-validator-utxo": "bun run scripts/spend-validator-utxo.ts", + "typecheck": "bun x tsc --noEmit" + }, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "@evolution-sdk/evolution": "^0.3.30", + "@pythnetwork/pyth-lazer-cardano-js": "^0.1.0", + "@pythnetwork/pyth-lazer-sdk": "^6.2.1", + "dotenv": "^17.3.1" + } +} diff --git a/lazer/cardano/OSI/off-chain/scripts/add-fund-validator-utxo.ts b/lazer/cardano/OSI/off-chain/scripts/add-fund-validator-utxo.ts new file mode 100644 index 00000000..dc5d3460 --- /dev/null +++ b/lazer/cardano/OSI/off-chain/scripts/add-fund-validator-utxo.ts @@ -0,0 +1,164 @@ +import { + Address, + Data, + InlineDatum, + UTxO, +} from "@evolution-sdk/evolution"; + +import { + formatTxHash, + loadFirstValidatorUtxo, + loadRuntimeFromEnv, + loadValidatorUtxoByTxHash, + makeLovelace, + parseCliOutRef, +} from "../e2e.ts"; +import { + decodeOsiDatumData, + makeFundRedeemerData, + makeOsiDatumData, +} from "../osi.ts"; + +const runtime = await loadRuntimeFromEnv(); + +const ANSI = { + reset: "\x1b[0m", + bold: "\x1b[1m", + dim: "\x1b[2m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", + red: "\x1b[31m", +} as const; + +// Parse CLI arguments +const additionalFundingLovelace = BigInt(process.argv[2] ?? "0"); +const targetOutRef = parseCliOutRef(process.argv[3]); + +if (!process.argv[2] || additionalFundingLovelace === 0n) { + throw new Error( + "Expected additional funding lovelace as first CLI argument (e.g., bun run add-fund-validator-utxo 2000000)" + ); +} + +// Load the validator UTxO +let validatorUtxo: UTxO.UTxO; +if (targetOutRef) { + validatorUtxo = await loadValidatorUtxoByTxHash( + runtime.client, + runtime.validator.address, + targetOutRef.transactionIdHex, + targetOutRef.index, + ); +} else { + validatorUtxo = await loadFirstValidatorUtxo( + runtime.client, + runtime.validator.address, + ); +} + +// Extract and decode the existing datum +let osiDatum; +try { + osiDatum = decodeValidatorDatum(validatorUtxo); +} catch (error) { + throw new Error(`Failed to decode validator datum: ${error}`); +} + +const walletAddress = await runtime.client.address(); +const currentValidatorLovelace = validatorUtxo.assets.lovelace; +const newValidatorLovelace = currentValidatorLovelace + additionalFundingLovelace; + +try { + // Build the transaction + const builder = runtime.client + .newTx() + .attachScript({ + script: runtime.validator.script, + }) + .collectFrom({ + inputs: [validatorUtxo], + redeemer: makeFundRedeemerData(), + label: "osi-fund", + }) + // Return the combined funds to the validator with the same datum + .payToAddress({ + address: runtime.validator.address, + assets: makeLovelace(newValidatorLovelace), + datum: new InlineDatum.InlineDatum({ + data: makeOsiDatumData(osiDatum), + }), + }); + + const txHash = await builder + .build() + .then((built) => built.sign()) + .then((signed: any) => signed.submit()); + + const submittedTxHash = formatTxHash(txHash); + + console.log(""); + console.log( + `${ANSI.bold}${ANSI.green}✅💸 OSI FUND SUCCESS${ANSI.reset}`, + ); + console.log( + `${ANSI.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI.reset}`, + ); + console.log( + `${ANSI.cyan}🔗 Tx hash${ANSI.reset} ${ANSI.bold}${submittedTxHash}${ANSI.reset}`, + ); + console.log( + `${ANSI.cyan}📦 Spent script UTxO${ANSI.reset} ${UTxO.toOutRefString(validatorUtxo)}`, + ); + console.log(""); + console.log(`${ANSI.bold}${ANSI.blue}🏷️ Addresses${ANSI.reset}`); + console.log(`👛 Wallet ${Address.toBech32(walletAddress)}`); + console.log(`🏛️ Validator ${Address.toBech32(runtime.validator.address)}`); + console.log(""); + console.log(`${ANSI.bold}${ANSI.magenta}📊 Value Changes${ANSI.reset}`); + console.log(`• Previous ${formatLovelace(currentValidatorLovelace)}`); + console.log( + `• Added ${ANSI.green}+${formatLovelace(additionalFundingLovelace)}${ANSI.reset}`, + ); + console.log( + `• New Total ${ANSI.bold}${ANSI.yellow}${formatLovelace(newValidatorLovelace)}${ANSI.reset}`, + ); + console.log(""); + console.log(`${ANSI.bold}${ANSI.yellow}🚀 Next${ANSI.reset}`); + console.log(`bun run spend-validator-utxo ${submittedTxHash}`); + console.log( + `${ANSI.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI.reset}`, + ); +} catch (error) { + if (error instanceof Error) { + console.error( + `${ANSI.bold}${ANSI.red}❌ Oops:${ANSI.reset} ${ANSI.red}${error.message}${ANSI.reset}`, + ); + } else { + console.error( + `${ANSI.bold}${ANSI.red}❌ Oops:${ANSI.reset} ${ANSI.red}Unknown error occurred${ANSI.reset}`, + ); + } + throw error; +} + +function decodeValidatorDatum(validatorUtxo: UTxO.UTxO) { + const datumOption = validatorUtxo.datumOption; + + if (!datumOption || datumOption._tag !== "InlineDatum") { + throw new Error("Validator UTxO is missing an inline OSI datum"); + } + + return decodeOsiDatumData(datumOption.data as Data.Constr); +} + +function formatLovelace(value: bigint): string { + const adaWhole = value / 1_000_000n; + const adaFraction = value % 1_000_000n; + + return `${value.toLocaleString()} lovelace (${adaWhole.toLocaleString()}.${adaFraction + .toString() + .padStart(6, "0")} ADA)`; +} diff --git a/lazer/cardano/OSI/off-chain/scripts/create-validator-utxo.ts b/lazer/cardano/OSI/off-chain/scripts/create-validator-utxo.ts new file mode 100644 index 00000000..8d0a2b01 --- /dev/null +++ b/lazer/cardano/OSI/off-chain/scripts/create-validator-utxo.ts @@ -0,0 +1,76 @@ +import { Address, InlineDatum } from "@evolution-sdk/evolution"; + +import { formatTxHash, loadRuntimeFromEnv, makeLovelace } from "../e2e.ts"; +import { + makeEmptyOsiDatum, + makeOsiDatumData, + makeVerificationKeyCredential, +} from "../osi.ts"; + +const ANSI = { + reset: "\x1b[0m", + bold: "\x1b[1m", + dim: "\x1b[2m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + cyan: "\x1b[36m", +} as const; + +const runtime = await loadRuntimeFromEnv(); +const walletAddress = await runtime.client.address(); +const validatorAddressBech32 = Address.toBech32(runtime.validator.address); +const defaultDeadline = BigInt(Date.now()) + 24n * 60n * 60n * 1000n; +const datum = makeEmptyOsiDatum(defaultDeadline); +const payees: readonly [string, bigint][] = [ + ["c0359ebb7d0688d79064bd118c99c8b87b5853e3af59245bb97e84d2", 10_000_000n], + ["3f7fc2419347ac70cb5fbcdf3bb8d964727ec1c3e93b364288c22f33", 10_000_000n], + ["28f60a6dcb45d06f76081888b6b749dc8829dcfb5e11596b3775220a", 10_000_000n], + ["5133ea0bdd0b0d7a3461146d5e777e8b2c013929d956032d6d6e91b4", 10_000_000n], + ["05231a2548dc81a3654e857b657960ac892c9e059af3ac4a3ed1d494", 10_000_000n], +]; +for (const [pubKey, amount] of payees) { + const credential = makeVerificationKeyCredential(pubKey); + datum.payees.set(credential, amount); +} +const txHash = await runtime.client + .newTx() + .payToAddress({ + address: runtime.validator.address, + assets: makeLovelace(runtime.fundingLovelace), + datum: new InlineDatum.InlineDatum({ + data: makeOsiDatumData(datum), + }), + }) + .build() + .then((built) => built.sign()) + .then((signed) => signed.submit()); + +const createdTxHash = formatTxHash(txHash); + +console.log(""); +console.log(`${ANSI.bold}${ANSI.green}🆕💰 OSI CREATED${ANSI.reset}`); +console.log( + `${ANSI.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI.reset}`, +); +console.log(`${ANSI.cyan}🔗 Tx hash${ANSI.reset} ${ANSI.bold}${createdTxHash}${ANSI.reset}`); +console.log(`${ANSI.cyan}👛 Wallet${ANSI.reset} ${Address.toBech32(walletAddress)}`); +console.log(`${ANSI.cyan}🏛️ Validator${ANSI.reset} ${validatorAddressBech32}`); +console.log(`${ANSI.cyan}💵 Funded${ANSI.reset} ${formatLovelace(runtime.fundingLovelace)}`); +console.log(`${ANSI.cyan}⏰ Deadline${ANSI.reset} ${datum.deadline.toString()}`); +console.log(`${ANSI.cyan}👥 Payees${ANSI.reset} ${payees.length.toString()}`); +console.log(""); +console.log(`${ANSI.bold}${ANSI.yellow}🚀 Next${ANSI.reset}`); +console.log(`bun run spend-validator-utxo ${createdTxHash}`); +console.log( + `${ANSI.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI.reset}`, +); + +function formatLovelace(value: bigint): string { + const adaWhole = value / 1_000_000n; + const adaFraction = value % 1_000_000n; + + return `${value.toLocaleString()} lovelace (${adaWhole.toLocaleString()}.${adaFraction + .toString() + .padStart(6, "0")} ADA)`; +} diff --git a/lazer/cardano/OSI/off-chain/scripts/evaluate-ogmios.ts b/lazer/cardano/OSI/off-chain/scripts/evaluate-ogmios.ts new file mode 100644 index 00000000..1aba8dea --- /dev/null +++ b/lazer/cardano/OSI/off-chain/scripts/evaluate-ogmios.ts @@ -0,0 +1,52 @@ +const endpoint = + readFirstEnv("OGMIOS_ENDPOINT", "OGMIOS_URL") ?? + "https://cardano-preprod-v6.ogmios-m1.dmtr.host"; +const apiKey = + readFirstEnv("DMTR_API_KEY_OGMIOS", "OGMIOS_API_KEY", "DMTR_API_KEY") ?? + "ogmios12mr2qnpkh8g7wyrkq6v"; + +const txCbor = process.argv[2]?.trim(); + +if (!txCbor) { + throw new Error("Expected tx CBOR hex as the first CLI argument"); +} + +if (!/^[0-9a-fA-F]+$/.test(txCbor) || txCbor.length % 2 !== 0) { + throw new Error("CBOR must be an even-length hex string"); +} + +const response = await fetch(endpoint, { + method: "POST", + headers: { + "Content-Type": "application/json", + "dmtr-api-key": apiKey, + }, + body: JSON.stringify({ + jsonrpc: "2.0", + method: "evaluateTransaction", + params: { + transaction: { cbor: txCbor }, + }, + }), +}); + +const responseText = await response.text(); + +console.log(`Ogmios endpoint: ${endpoint}`); +console.log(`HTTP status: ${response.status}`); +console.log(responseText); + +if (!response.ok) { + process.exitCode = 1; +} + +function readFirstEnv(...names: string[]): string | undefined { + for (const name of names) { + const value = process.env[name]?.trim(); + if (value) { + return value; + } + } + + return undefined; +} diff --git a/lazer/cardano/OSI/off-chain/scripts/fetch-ada-usdt-update.ts b/lazer/cardano/OSI/off-chain/scripts/fetch-ada-usdt-update.ts new file mode 100644 index 00000000..b7361693 --- /dev/null +++ b/lazer/cardano/OSI/off-chain/scripts/fetch-ada-usdt-update.ts @@ -0,0 +1,19 @@ +import { + fetchLatestSignedUpdate, + readLazerToken, +} from "../e2e.ts"; + +const ADA_USD_FEED_ID = 16; +const USDT_USD_FEED_ID = 8; + +const lazerToken = readLazerToken(); +const update = await fetchLatestSignedUpdate(lazerToken, [ + ADA_USD_FEED_ID, + USDT_USD_FEED_ID, +]); + +console.log(`Queried feed ids: ${ADA_USD_FEED_ID}, ${USDT_USD_FEED_ID}`); +console.log("Signed update hex:"); +console.log(update.signedUpdateHex); +console.log("Parsed payload:"); +console.dir(update.parsed, { depth: null, colors: true }); diff --git a/lazer/cardano/OSI/off-chain/scripts/spend-validator-utxo.ts b/lazer/cardano/OSI/off-chain/scripts/spend-validator-utxo.ts new file mode 100644 index 00000000..5f67c72d --- /dev/null +++ b/lazer/cardano/OSI/off-chain/scripts/spend-validator-utxo.ts @@ -0,0 +1,259 @@ +import { + Address, + Data, + Effect, + ScriptHash, + Transaction, + UTxO, +} from "@evolution-sdk/evolution"; +import { + getPythScriptHash, + getPythState, +} from "@pythnetwork/pyth-lazer-cardano-js"; + +import { + fetchLatestSignedUpdate, + formatTxHash, + loadFirstValidatorUtxo, + loadValidatorUtxoByTxHash, + logDetailedError, + loadRuntimeFromEnv, + makeWithdrawRedeemer, + parseCliOutRef, + readLazerToken, + makeLovelace, +} from "../e2e.ts"; +import { + decodeOsiDatumData, + makePayoutRedeemerData, + paymentCredentialToAddress, + type OsiDatum, +} from "../osi.ts"; +import type { ParsedFeedPayload } from "@pythnetwork/pyth-lazer-sdk"; + +const ANSI = { + reset: "\x1b[0m", + bold: "\x1b[1m", + dim: "\x1b[2m", + green: "\x1b[32m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + magenta: "\x1b[35m", + cyan: "\x1b[36m", + red: "\x1b[31m", +} as const; + +const runtime = await loadRuntimeFromEnv(); +const lazerToken = readLazerToken(); +const targetOutRef = parseCliOutRef(process.argv[2]); +const validatorUtxo = targetOutRef + ? await loadValidatorUtxoByTxHash( + runtime.client, + runtime.validator.address, + targetOutRef.transactionIdHex, + targetOutRef.index, + ) + : await loadFirstValidatorUtxo(runtime.client, runtime.validator.address); + +const pythState = await getPythState( + runtime.pythPolicyId, + runtime.providerClient, +); +const pythWithdrawScriptHash = getPythScriptHash(pythState); +const pythUpdate = await fetchLatestSignedUpdate(lazerToken, runtime.queryFeedIds); +const osiDatum = decodeValidatorDatum(validatorUtxo); +const paymentOutputs = buildPaymentOutputs(runtime.network, osiDatum, pythUpdate.parsed); + +const now = BigInt(Date.now()); +const loggingEvaluator = { + evaluate: ( + tx: Transaction.Transaction, + additionalUtxos: ReadonlyArray | undefined, + ) => + Effect.gen(function* () { + console.log(`${ANSI.bold}${ANSI.blue}🧪 Evaluation${ANSI.reset}`); + console.log(`${ANSI.cyan}CBOR${ANSI.reset}`); + console.log(Transaction.toCBORHex(tx)); + + if (additionalUtxos && additionalUtxos.length > 0) { + console.log(`${ANSI.cyan}Additional UTxOs${ANSI.reset}`); + console.dir(additionalUtxos.map(UTxO.toOutRefString), { + depth: null, + colors: true, + }); + } + + return yield* runtime.providerClient.Effect.evaluateTx(tx); + }), +} as const; + +console.log(""); +console.log(`${ANSI.bold}${ANSI.blue}⚙️ OSI PAYOUT CONTEXT${ANSI.reset}`); +console.log( + `${ANSI.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI.reset}`, +); +console.log(`🏛️ Validator ${Address.toBech32(runtime.validator.address)}`); +console.log(`📦 Input UTxO ${UTxO.toOutRefString(validatorUtxo)}`); +console.log(`🔐 Pyth State ${UTxO.toOutRefString(pythState)}`); +console.log(`🧾 Withdraw SH ${pythWithdrawScriptHash}`); +console.log(`📈 Feed IDs ${runtime.feedId} (primary) | ${runtime.queryFeedIds.join(", ")}`); +console.log(`🛰️ Update Hex ${pythUpdate.signedUpdateHex}`); +console.log(""); + +try { + let builder = runtime.client + .newTx() + .setValidity({ + from: now - 60_000n, + to: now + 60_000n, + }) + .readFrom({ + referenceInputs: [pythState], + }) + .withdraw({ + amount: 0n, + redeemer: makeWithdrawRedeemer(pythUpdate.signedUpdate), + stakeCredential: ScriptHash.fromHex(pythWithdrawScriptHash), + label: "pyth-withdraw", + }) + .attachScript({ + script: runtime.validator.script, + }) + .collectFrom({ + inputs: [validatorUtxo], + redeemer: makePayoutRedeemerData(), + label: "osi-payout", + }); + + let totalPaymentLvc = 0n; + for (const payment of paymentOutputs) { + builder = builder.payToAddress({ + address: payment.address, + assets: makeLovelace(payment.lovelace), + }); + + totalPaymentLvc += payment.lovelace; + } + + if (totalPaymentLvc < validatorUtxo.assets.lovelace) { + builder = builder.payToAddress({ + address: validatorUtxo.address, + assets: makeLovelace(validatorUtxo.assets.lovelace - totalPaymentLvc), + datum: validatorUtxo.datumOption + }); + } + + const remainingLovelace = + validatorUtxo.assets.lovelace > totalPaymentLvc + ? validatorUtxo.assets.lovelace - totalPaymentLvc + : 0n; + + const txHash = await builder + .build({}) + .then((built) => built.sign()) + .then((signed) => signed.submit()); + + const spendTxHash = formatTxHash(txHash); + + console.log(""); + console.log(`${ANSI.bold}${ANSI.green}✅💸 OSI PAYOUT SUCCESS${ANSI.reset}`); + console.log( + `${ANSI.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI.reset}`, + ); + console.log(`${ANSI.cyan}🔗 Tx hash${ANSI.reset} ${ANSI.bold}${spendTxHash}${ANSI.reset}`); + console.log(`${ANSI.cyan}📤 Paid out${ANSI.reset} ${formatLovelace(totalPaymentLvc)}`); + console.log(`${ANSI.cyan}🏦 Remaining${ANSI.reset} ${formatLovelace(remainingLovelace)}`); + console.log( + `${ANSI.dim}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${ANSI.reset}`, + ); +} catch (error) { + if (error instanceof Error) { + console.error( + `${ANSI.bold}${ANSI.red}❌ Oops:${ANSI.reset} ${ANSI.red}${error.message}${ANSI.reset}`, + ); + } else { + console.error( + `${ANSI.bold}${ANSI.red}❌ Oops:${ANSI.reset} ${ANSI.red}Unknown error occurred${ANSI.reset}`, + ); + } + logDetailedError(error); + throw error; +} + +function decodeValidatorDatum(validatorUtxo: UTxO.UTxO): OsiDatum { + const datumOption = validatorUtxo.datumOption; + + if (!datumOption || datumOption._tag !== "InlineDatum") { + throw new Error("Validator UTxO is missing an inline OSI datum"); + } + + return decodeOsiDatumData(datumOption.data as Data.Constr); +} + +function buildPaymentOutputs( + network: "mainnet" | "preprod" | "preview", + datum: OsiDatum, + parsedUpdate: typeof pythUpdate.parsed, +): { address: Address.Address; lovelace: bigint }[] { + if (!parsedUpdate) { + throw new Error("Pyth update is missing parsed feed data"); + } + + const quoteFeed = findFeed(parsedUpdate.priceFeeds, 16); + const baseFeed = findFeed(parsedUpdate.priceFeeds, 8); + const networkId = network === "mainnet" ? 1 : 0; + + return Array.from(datum.payees, ([paymentCredential, quoteAmount]) => ({ + address: paymentCredentialToAddress(paymentCredential, networkId), + lovelace: computeLovelacePayout(quoteAmount, quoteFeed, baseFeed), + })); +} + +function findFeed( + feeds: readonly ParsedFeedPayload[], + priceFeedId: number, +): ParsedFeedPayload { + const feed = feeds.find((candidate) => candidate.priceFeedId === priceFeedId); + + if (!feed) { + throw new Error(`Missing parsed Pyth feed ${priceFeedId}`); + } + + if (feed.price === undefined || feed.exponent === undefined) { + throw new Error(`Parsed Pyth feed ${priceFeedId} is missing price data`); + } + + return feed; +} + +function computeLovelacePayout( + quoteAmount: bigint, + quoteFeed: ParsedFeedPayload, + baseFeed: ParsedFeedPayload, +): bigint { + const quotePrice = BigInt(quoteFeed.price!); + const basePrice = BigInt(baseFeed.price!); + const quoteExponent = quoteFeed.exponent!; + const baseExponent = baseFeed.exponent!; + + if (basePrice <= 0n || quotePrice <= 0n) { + throw new Error("Pyth prices must be positive for payout calculation"); + } + + if (quoteExponent >= baseExponent) { + const scale = 10n ** BigInt(quoteExponent - baseExponent); + return (quoteAmount * quotePrice * scale) / basePrice; + } + + const scale = 10n ** BigInt(baseExponent - quoteExponent); + return (quoteAmount * quotePrice) / (basePrice * scale); +} + +function formatLovelace(value: bigint): string { + const adaWhole = value / 1_000_000n; + const adaFraction = value % 1_000_000n; + + return `${value.toLocaleString()} lovelace (${adaWhole.toLocaleString()}.${adaFraction + .toString() + .padStart(6, "0")} ADA)`; +} diff --git a/lazer/cardano/OSI/off-chain/tsconfig.json b/lazer/cardano/OSI/off-chain/tsconfig.json new file mode 100644 index 00000000..f159082f --- /dev/null +++ b/lazer/cardano/OSI/off-chain/tsconfig.json @@ -0,0 +1,30 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext", "DOM"], + "types": ["bun"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/lazer/cardano/OSI/on-chain/.gitignore b/lazer/cardano/OSI/on-chain/.gitignore new file mode 100644 index 00000000..ff7811b1 --- /dev/null +++ b/lazer/cardano/OSI/on-chain/.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/OSI/on-chain/aiken.lock b/lazer/cardano/OSI/on-chain/aiken.lock new file mode 100644 index 00000000..2a6a2ea2 --- /dev/null +++ b/lazer/cardano/OSI/on-chain/aiken.lock @@ -0,0 +1,27 @@ +# 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" + +[[requirements]] +name = "pyth-network/pyth-lazer-cardano" +version = "main" +source = "github" + +[[packages]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +requirements = [] +source = "github" + +[[packages]] +name = "pyth-network/pyth-lazer-cardano" +version = "main" +requirements = [] +source = "github" + +[etags] +"pyth-network/pyth-lazer-cardano@main" = [{ secs_since_epoch = 1774221971, nanos_since_epoch = 576433480 }, "a46dacd97a22eb07feeaf966d48c3116c8249ddc836705656e3135cea285bcfc"] diff --git a/lazer/cardano/OSI/on-chain/aiken.toml b/lazer/cardano/OSI/on-chain/aiken.toml new file mode 100644 index 00000000..efd12252 --- /dev/null +++ b/lazer/cardano/OSI/on-chain/aiken.toml @@ -0,0 +1,23 @@ +name = "delfines/on-chain" +version = "0.0.0" +compiler = "v1.1.21" +plutus = "v3" +license = "Apache-2.0" +description = "Aiken contracts for project 'delfines/on-chain'" + +[repository] +user = "delfines" +project = "on-chain" +platform = "github" + +[[dependencies]] +name = "aiken-lang/stdlib" +version = "v3.0.0" +source = "github" + +[[dependencies]] +name = "pyth-network/pyth-lazer-cardano" +version = "main" +source = "github" + +[config] diff --git a/lazer/cardano/OSI/on-chain/lib/utils.ak b/lazer/cardano/OSI/on-chain/lib/utils.ak new file mode 100644 index 00000000..72b628e0 --- /dev/null +++ b/lazer/cardano/OSI/on-chain/lib/utils.ak @@ -0,0 +1,17 @@ +use aiken/math/rational.{Rational} +use aiken/option +use pyth.{Feed} + +pub fn quote_price_multiplier( + base_price: Int, + base_exponent: Int, + quote_price: Int, + quote_exponent: Int, +) -> Rational { + expect Some(multiplier) = + rational.from_int(10) |> rational.pow(base_exponent - quote_exponent) + + expect Some(price_ratio) = rational.new(base_price, quote_price) + + rational.mul(price_ratio, multiplier) +} diff --git a/lazer/cardano/OSI/on-chain/plutus.json b/lazer/cardano/OSI/on-chain/plutus.json new file mode 100644 index 00000000..a456d498 --- /dev/null +++ b/lazer/cardano/OSI/on-chain/plutus.json @@ -0,0 +1,158 @@ +{ + "preamble": { + "title": "delfines/on-chain", + "description": "Aiken contracts for project 'delfines/on-chain'", + "version": "0.0.0", + "plutusVersion": "v3", + "compiler": { + "name": "Aiken", + "version": "v1.1.21+42babe5" + }, + "license": "Apache-2.0" + }, + "validators": [ + { + "title": "osi.oracle_settled_invoice.spend", + "datum": { + "title": "datum", + "schema": { + "$ref": "#/definitions/osi~1OsiDatum" + } + }, + "redeemer": { + "title": "redeemer", + "schema": { + "$ref": "#/definitions/osi~1OsiRedeemers" + } + }, + "parameters": [ + { + "title": "pyth_policy", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + }, + { + "title": "base_currency", + "schema": { + "$ref": "#/definitions/Int" + } + } + ], + "compiledCode": "592f380101002229800aba4aba2aba1aba0aab9faab9eaab9dab9cab9a9bae0039bad00248888888888a60022a660089212a65787065637420757064617465733a204c6973743c4279746541727261793e203d2072656465656d657200168a998022491a6578706563742073746174653a2050797468203d20737461746500168a9980224921657870656374206f75745f646174756d3a204f7369446174756d203d206461746100168a9980224938657870656374205b6f75745d203d2066696e645f7363726970745f6f757470757473286f7574707574732c207363726970745f686173682900168a99802249de6578706563742046656564207b0a20202020202020202020666565645f69643a2071756f74655f666565645f69642c0a2020202020202020202070726963653a20536f6d6528536f6d6528626173655f707269636529292c0a202020202020202020206578706f6e656e743a20536f6d6528626173655f6578706f6e656e74292c0a20202020202020202020666565645f7570646174655f74696d657374616d703a20536f6d6528536f6d652874696d657374616d705f3229292c0a202020202020202020202e2e0a20202020202020207d203d20626173655f6665656400168a99802249e56578706563742046656564207b0a20202020202020202020666565645f69643a206164615f70726963655f666565645f69642c0a2020202020202020202070726963653a20536f6d6528536f6d652871756f74655f707269636529292c0a202020202020202020206578706f6e656e743a20536f6d652871756f74655f6578706f6e656e74292c0a20202020202020202020666565645f7570646174655f74696d657374616d703a20536f6d6528536f6d652874696d657374616d705f3129292c0a202020202020202020202e2e0a20202020202020207d203d2071756f74655f6665656400168a9980224933657870656374205b71756f74655f666565642c20626173655f666565645d203d2070726963655f7570646174652e666565647300168a9980224934657870656374205b70726963655f7570646174655d203d206765745f7570646174657328707974685f706f6c6963792c2074782900168a998022493a65787065637420536372697074287363726970745f6861736829203d20696e2e616464726573732e7061796d656e745f63726564656e7469616c00168a998022491672656465656d65723a204f736952656465656d65727300164888888888966002646530013015001980a980b000cdc3a4005301500248889660026004602a6ea800e2646644b300100789919912cc004c00c0062b3001301c3754015002806a03a8acc004c0200062b3001301c3754015002806a03a806a032406433001301a375400b3722911009b8f4881009180f180f800c8c078c07cc07c00644646600200200644660060026004005370e90022444444530013758604800f37586048604a604a00f2323322330020020012259800800c00e2646644b3001300d0028acc004c03000a20030064095133005005302c00440946eb8c094004dd698130009814000a04c3232330010010042259800800c00e2646644b3001300e0028acc004c03400a20030064099133005005302d00440986eb8c098004dd598138009814800a04e14bd6f7b6300a40012232330010010032259800800c5a264b30013375e6052604c6ea800401226012604c6ea800626600600660540048118c0a000502624444b3001300c30243754027159800980618121baa00d89919912cc004c050c09cdd500144c9660020030208992cc004c0b800a264b3001300b302a375400313259800800c566002602660566ea8006264b3001001812c4c966002003026813409a26644b300100181444c9660020030298992cc004c0d800e26602600244b30010028acc004cdd7806804c4cdc498089bab30173034375401e60226eacc05cc0d0dd5005c5282062899192cc004006264b3001001817c0be05f198009bad00189802181d802cc0d8dd50012444b300130210018992cc00400606713259800800c0d206903481a44c966002608200700581aa07c375c0028208c0f800503c181d1baa0058acc004c098006264b3001001819c4c96600200303481a40d2069132598009820801c01606a81f0dd7000a082303e00140f060746ea801606481b9037205e81e0c0d800a05d02e81740b903b181a000981b801206a81520663756003029814c0a50361819800a062375a002606400502640cc60600028170c0b0dd5000c09102940920490248122062302e302b375400315330294912465787065637420496e6c696e65446174756d286461746129203d206f75742e646174756d001640a0601860546ea80060428158c0b000502a191919800800804112cc004006297ae0899912cc004c9660026034605a6ea8006266e3c018dd7181898171baa0018a5040ac6060605a6ea8c0c0c0b4dd500144cc0bc008cc0100100062660080080028150c0b8004c0bc00502c1bae302b3028375400501a40946052604c6ea8c0a4c098dd5000981498131baa00e3300100400e8a99811a492065787065637420536f6d6528696e7075745f646174756d29203d20646174756d00164089159800980618121baa00d899914c004dd698150014dd5981518158014c0a8c09cdd5181518139baa001488966002602c60526ea800633001375c605a60546ea80064605c605e605e605e00337009000cdc4a4001374a90014dd2a400091111119912cc00400604b13259800981b80144c966002003028899912cc00400605513259800981d80144c8c8ca6002607a003303d303e303e303e303e303e303e303e303e0019bad303d003981e80124444b30013025303d3754003132598009813181f1baa0018992cc004c09cc0fcdd500344c966002605060806ea801a264b3001302930413754003132323298009824000cc120c124c124c124c124c124c124c124c1240066eb4c120c114dd50024dd69824001cc1200092222259800981898249baa0018992cc004c0c8c128dd5000c4c966002606660966ea801e264b30013034304c375400f13259800981a98269baa0018cc004dd6982898271baa00191810198289ba8001330514c010101004bd70488cc88c08ccc150dd419b82375a60aa0046eb4c154004cc150dd419b82375a60aa60ac0046eb4c154c1580052f5c060a06ea8008c140dd5000c896600200514a3153304e00116413491112cc004cc0056600266e1c02966002b3001302504d899b8904d483fbfffffc7e294104f441362a660a0921216578706563742076616c7565203e3d20302026262076616c7565203c3d206d61780016413d14a315330504913071756f74655f666565645f6964203d3d2066726f6d5f696e7428626173655f63757272656e637929203f2046616c73650014a08279240138426173652063757272656e6379206665656420646f6573206e6f74206d617463682065787065637465642062617365206173736574206964008992cc004cc009660026644b3001323259800981f000c528c5660026086003132598009822182b9baa303b3058375460b660b06ea8016266e24004012266e200040110551bad305a3057375400514a082a1054182a9baa00130583055375460b060aa6ea800a26464b3001303e0018a508acc004c10c006264b3001304430573754607660b06ea8c0ecc160dd5002c4cdc4802000c4cdc4002000a0aa375a60b460ae6ea800a294505420a83055375400260b060aa6ea8c0e0c154dd5001452820a430013053375407c66e0cdd6982b18299baa021483403e29462a660a292015e74782e76616c69646974795f72616e67650a20207c3e20696e74657276616c2e636f6e7461696e73287536342e61735f696e742870726963655f7570646174652e74696d657374616d705f757329202f203130303029203f2046616c73650014a0828124013850726963652066656564206e6f7420757064617465642077697468696e207472616e73616374696f6e2076616c69646974792072616e6765008acc004cc00966002646464b300130420018992cc004c10cc158dd5181d182b9baa303a3057375400913371000206113371200206082a0dd6982c982b1baa0028acc004c0d80062942294505320a63054375400260ae60a86ea8c0dcc150dd5000980098299baa03e8a518a99828a4814474782e76616c69646974795f72616e67650a20207c3e20696e74657276616c2e69735f656e746972656c795f6265666f726528646561646c696e6529203f2046616c73650014a082812401245061796d656e74206e6f74206d616465206265666f72652074686520646561646c696e65008998012cc004c8cc88ca600200302f81ac09d00111112cc00400e2b30013371200290004528c56600200514a11325980099baf305e305b375460bc00200d13371200460706eacc178c17c0062941058182d1baa305d002416c82ba264b30010038a50899912cc004c110c170dd5000c4c96600266ebcc184c178dd51830801982e802456600266e24c8cdc19bad3062001375a60c460c600260bc6ea8c184c178dd5001000c56600266e24004016330010089831003cc18801a66e04014005008452820b68a50416d14a082d8c0e8dd598301830801454cc16d2416a65787065637420536f6d652865787065637465645f7061796f757429203d0a202020202020202020202020726174696f6e616c2e64697628726174696f6e616c2e66726f6d5f696e742871756f74655f616d6f756e74292c2071756f74655f6d756c7469706c696572290016416860b66ea8c17800cc966002609060b66ea80062980103d87a8000898169982f1980618069bad305c002305f305c375400297ae04164646644b300133710004900044c0bccc180c0bccc180dd41805800998301ba8300b0024bd7025eb822b300133710900000144c0bccc180c0bccc180dd4000998301ba80024bd7025eb822980103d87a8000416c82d8dd6982f8009bad305f3060001305b375400e82e0c17400d05b1919192cc004c0f8c158dd5000c4c966002607e60ae6ea800626601060b660b06ea8004c16cc160dd5001454cc1592414065787065637420536f6d652870726963655f726174696f29203d20726174696f6e616c2e6e657728626173655f70726963652c2071756f74655f70726963652900164154b3001303e0188a6103d87a80008acc004cdc400c24001130283305930283305937506008018660b26ea0c0100612f5c097ae0898141982c98141982c9ba800c33059375003097ae04bd7020a84151153305549016365787065637420536f6d65286d756c7469706c69657229203d0a20202020726174696f6e616c2e66726f6d5f696e7428313029207c3e20726174696f6e616c2e706f7728626173655f6578706f6e656e74202d2071756f74655f6578706f6e656e74290016415064653001375a60b6003375a60b660b8003337020180309112cc0056600260840071337120029000452820b08a60103d87a80008acc004cdc4240000031302c3305d302c3305d37506600c006002660ba6ea0cc0180080052f5c097ae08acc004cdc4000a40011302c3305d302c3305d37506600c0046010002660ba6ea0cc01800cc0200052f5c097ae08a610ad8799fd8799f0101ffff00416082c10580c15cdd50009803a40286002002444b30013371000290004520008acc004c0f80062900144c966002607e66e1800920048cc0040126002007337060049002200889800cc00401260020073370660580049002200841546e0800905420a8302633056375205497ae0370290004528c54cc1452416f7061796f7574735f616e645f72656d61696e696e675f6d61746368287061796565732c206f7574707574732c2071756f74655f6d756c7469706c6965722c20536372697074287363726970745f68617368292c207370656e6461626c655f6c6f76656c61636529203f2046616c73650014a082812401455061796f7574206f757470757473206f722072656d61696e696e67207363726970742066756e647320646f206e6f74206d617463682065787065637465642076616c756573008a50414114a082808c158c15cc15cc15cc15cc15cc15cc15c006294104f20888258c140c134dd5003c10d04a1bad304f304c375400f04241246eb4c138c12cdd5000c105048182698251baa001820208e1824182418241824000982380098211baa00c81ba07e30443041375400d03640f86eb4c10cc100dd500340d503d1bad3042303f375400303440f06082607c6ea800606681d8607a607a607a607a0026078002606e6ea800e05681c0c0e4005037181b800981c000a06a3758602a60666ea800604c81a0c0d400503319b81300d3756602660606ea8028dd6980298181baa01b323232332259800980e181a1baa0028992cc004c074c0d4dd5000c4c966002003031818c4c8cc0640048966002005132330010010042259800800c52f5c113303e98009b8c480126e0120039b89480226e3120009b8c480226e0120074888888cc88cc0408c8d6600266e3cdd71824800a450475d3c793008cc004888c8cc00cdd698268009bae304d304e00130030019b89480426e3120109b804803e600e90082444446600a4646609e6ea0cde5250375c60a00026609e60a060a200297ae05980098029b8d0018998271ba930020013304e37526600860066e340040052f5c1153304a49124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e74001641244646600e4660a06ea0cdc7000a4000660a06ea4cc008c088dc6800800a5eb808cc0208cc144dd419b8e00148000cc144dd49980198119b8d0010014bd701119191982a18119982a1ba800733054375000a660a860aa00297ae033054305530560014bd7018008012cc004c08c00a2653001001a5eb820068008889660026074003123233057374e002660ae6ea40092f5c065300100180252f5c080088896600200510018cc00400e60b600533059305a002001400c82c2246530010059982b982c0008024c0a400e6eb8c160c1640050054c004c8cc15cdd419bca4a06eb8c160004cc15cc160c1640052f5c0b30013016371a00313305637526022002660ac6ea4cc050c04cdc6800800a5eb822a660a492124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e7400164145223305800233058375200297ae09b874801a6e1d200848888c8c8c8cc018ca6002003375860be005302d3305e305f0083305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80004bd70200222259800801440063300100398310014c8c966002608e00313230323306330640013306330323306330643061375400697ae030653065001306037540071598009826000c4c8c8c0cccc190c194008cc190c194004cc190c0cccc190c194c188dd500225eb80c198c198004c194004c180dd5001c5660026080003132323230343306530660033306530660023306530660013306530343306530663063375400a97ae03067306700130660013065001306037540071598009805000c4c8c8c8c8c0d4cc198c19c010cc198c19c00ccc198c19c008cc198c19c004cc198c0d4cc198c19cc190dd500325eb80c1a0c1a0004c19c004c198004c194004c180dd5001c56600260120031323232323230363306730680053306730680043306730680033306730680023306730680013306730363306730683065375400e97ae03069306900130680013067001306600130650013060375400715980099b87480280062646464646464606e660d060d200c660d060d200a660d060d2008660d060d2006660d060d2004660d060d2002660d0606e660d060d260cc6ea80212f5c060d460d400260d200260d000260ce00260cc00260ca00260c06ea800e2b30013370e9006000c4c8c8c8c8c8c8c8c0e0cc1a4c1a801ccc1a4c1a8018cc1a4c1a8014cc1a4c1a8010cc1a4c1a800ccc1a4c1a8008cc1a4c1a8004cc1a4c0e0cc1a4c1a8c19cdd5004a5eb80c1acc1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c180dd5001c56600266e1d200e00189919191919191919181c99835183580419835183580399835183580319835183580299835183580219835183580199835183580119835183580099835181c99835183598341baa00a4bd7018361836000983580098350009834800983400098338009833000983280098301baa0038acc004cdc3a40200031323232323232323232303a3306b306c0093306b306c0083306b306c0073306b306c0063306b306c0053306b306c0043306b306c0033306b306c0023306b306c0013306b303a3306b306c3069375401697ae0306d306d001306c001306b001306a001306900130680013067001306600130650013060375400715980099b87480480062646464646464646464646076660d860da014660d860da012660d860da010660d860da00e660d860da00c660d860da00a660d860da008660d860da006660d860da004660d860da002660d86076660d860da60d46ea80312f5c060dc60dc00260da00260d800260d600260d400260d200260d000260ce00260cc00260ca00260c06ea800e2b30013370e900a000c4c8c8c8c8c8c8c8c8c8c8c8c0f0cc1b4c1b802ccc1b4c1b8028cc1b4c1b8024cc1b4c1b8020cc1b4c1b801ccc1b4c1b8018cc1b4c1b8014cc1b4c1b8010cc1b4c1b800ccc1b4c1b8008cc1b4c1b8004cc1b4c0f0cc1b4c1b8c1acdd5006a5eb80c1bcc1bc004c1b8004c1b4004c1b0004c1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c180dd5001c56600266e1d20160018991919191919191919191919181e99837183780619837183780599837183780519837183780499837183780419837183780399837183780319837183780299837183780219837183780199837183780119837183780099837181e99837183798361baa00e4bd70183818380009837800983700098368009836000983580098350009834800983400098338009833000983280098301baa0038991919191919191919191919181e998371837806198371837805998371837805198371837804998371837804198371837803998371837803198371837802998371837802198371837801998371837801198371837800998371837983800099837181e99837183798361baa00e4bd7025eb80c1bc004c1b8004c1b4004c1b0004c1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c180dd5001a0ba417482e905d20ba417482e905d20ba417482e905d182f1baa0013061002400c82f8dd7182f182f800991acc004c0b800a2653001001a5eb82006800888966002608a003123233062374e002660c46ea40092f5c065300100180252f5c080088896600200510018cc00400e60cc005330643065002001400c831a246530010059983118318008024c0d000e6eb8c18cc1900050051802800a0b68a9982ca4811165787065637420636f756e74203e3d2030001641606eb8c178c17c00ca60024464b30013001480422b30013001480022900044cdc7244108010204081020408000002416d1598009800a408113370490400219801801980a80144cdc124101010100406600600666e00009203f416c82d8dc4000c888c8cc184c00cdd698310009983098311831800a5eb80c00c006660ba6ea0cdc7000a4000660ba6ea4cc03cc0bcdc6800800a5eb8122232329800919802919198329ba833794940dd718330009983298331833800a5eb816600260366e340062660c86ea4c060004cc190dd49980d180c9b8d0010014bd70454cc181240124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016417c4646006b3001304a0018a60103d87a80008981a198329ba80014bd7020c030030019bad3063003911191acc006600260966eb4c19c00694294506144cc02000c8c014c0d8cc19cdd4000a5eb822601e600898103d87a800041846eb8c19cc1a0004cc194dd419b8e00148000cc194dd49980b981b9b8d0010014bd7024446b3001304a002898019181a99833000a5eb822b3001304f002898019181b19833000a5eb822b3001304300289801919ba548010cc1980052f5c1159800980680144cc01c8c8cc19cdd419bca4a06eb8c1a0004cc19cc1a0c1a40052f5c0b30013034371a00313306637526044002660cc6ea4cc0a0c09cdc6800800a5eb822a660c4920124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e7400164184466e95200633066375000297ae08acc004c03000a26600e464660ce6ea0cde5250375c60d0002660ce60d060d200297ae059800981a1b8d0018998331ba9302200133066375266050604e6e340040052f5c1153306249124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e7400164184466e952008330663750b3001337100029040400244006266e04004c0192020418497ae08acc004cdc3a40140051300323374a900519833000a5eb822b30013370e900600144cc0048cdd2a4018660cc00297ae0232330673750600c6eb4c1a0004cc19cc1a0c1a40052f5c064660ce6ea0cde5250375c60d0002660ce60d060d200297ae059800980e9b8d0018998331ba9301a0013306637526603860366e340040052f5c11533062490124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016418515980099b874803800a266002466e95200e330660014bd70119198339ba833794940dd718340009983398341834800a5eb8166002603a6e340062660cc6ea4c068004cc198dd49980e180d9b8d0010014bd70454cc18924124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016418515980099b874804000a266002466e952010330660014bd70119198339ba833794940dd718340009983398341834800a5eb8166002603a6e340062660cc6ea4c068004cc198dd49980e180d9b8d0010014bd70454cc18924124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016418515980099b874804800a246464660d066e9520123306830690014bd701983418349835000a5eb80c004008cc0208c8cc1a0dd419bca4a06eb8c1a4004cc1a0c1a4c1a80052f5c0b30013035371a00313306737526046002660ce6ea4cc0a4c0a0dc6800800a5eb822a660c6920124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e74001641884b3001304c0018a60103d87980008acc004c144006298103d87a80008acc004c114006298103d87b80008acc004c03c006298103d87c80008acc004c038006298103d87d80008a99831a49204d61726b657453657373696f6e2076616c7565206f7574206f662072616e676500164188831106220c4418915980099b874805000a26006466e952014330660014bd70456600266e1d201600289801919ba548058cc1980052f5c115980099b874806000a266002466e952018330660014bd70119198339ba833794940dd718340009983398341834800a5eb8166002603a6e340062660cc6ea4c068004cc198dd49980e180d9b8d0010014bd70454cc18924124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016418515330614911c4665656450726f7065727479204944206f7574206f662072616e676500164180830106020c04180830106020c04180830106020c041806eb8c198c19c0184b300133710002904040404040404040400144006266e04004c00920800141746006006375a60b800264660b66ea0cdc7000a4000660b66ea4cc034c0b4dc6800800a5eb80dd7182d982e0021050454cc13924011165787065637420636f756e74203e3d2030001641346e312002454cc111240125657870656374206d61676963203d3d2070726963655f7570646174655f6d616769635f6c650016410c6eb8c124c12800566002600e6e3400626608e6ea4c008004cc11cdd49980298021b8d0010014bd70454cc10d240124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e74001641086eb8c098c110dd5198081191acc004cdc79bae304900148904b9011a8200891919191919191991198289810198289829003998289ba90013305130520024bd70198289829182980125eb80d660026040003125980099b89002371a00313305037526601e004002660a06ea66002005337026e3400400a002b8c25eb822a66098920124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016412d153304b4911165787065637420636f756e74203e3d2030001641286eb8c140c144008dd718280021bad304f001323304e375066f29281bae304f0013304e304f30500014bd702cc004c06cdc6800c4cc134dd49804800998269ba93300f300e371a00200297ae08a99824a4924657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e74001641206eb8c134c138004d6600294624b30013371290201b8d0018998261ba93300b48100004cc130dd4cc005204099b80371a002901fc0057184bd70454cc12124124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016411d15330474911165787065637420636f756e74203e3d2030001641186eb8c130c134004c0040096600294624b3001337129040009b8d0018998251ba9330094820004004cc128dd4cc00520800199b80371a002903fc0057184bd70454cc11924124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016411515330454911165787065637420636f756e74203e3d203000164111153304449126657870656374206d61676963203d3d20736f6c616e615f666f726d61745f6d616769635f6c650016410c6eb8c124c12800566002600e6e3400626608e6ea4c008004cc11cdd49980298021b8d0010014bd70454cc10d240124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e74001641086eb8c11c024c00d200430034802066004004608000281ea264b300100181ac0d606b035899180198200021bae0014100607a00481d8dd6000c0c606281e0c0e4c0d8dd5000c54cc0d124016465787065637420536f6d652872656465656d657229203d0a2020202070616972732e6765745f66697273742873656c662e72656465656d6572732c205769746864726177285363726970742873746174652e77697468647261775f736372697074292929001640cc6466446600400400244b30010018a60103d87a80008992cc004cdd7802181c000c4c028cc0ecc0e40052f5c1133003003303d00240d8607600281c8dd5981c981d181d181d181d181d181d181d181d181d181b1baa0213374a90021981b98039981b9805181a9baa30383035375400497ae04bd70454cc0cd241ff65787065637420536f6d6528737461746529203d0a202020206c6973742e66696e645f6d6170280a20202020202073656c662e7265666572656e63655f696e707574732c0a202020202020666e28696e70757429207b0a20202020202020206966206173736574732e6861735f6e66745f73747269637428696e7075742e6f75747075742e76616c75652c20707974685f69642c2022507974682053746174652229207b0a202020202020202020202f2f20737461746520746f6b656e206f757473696465206f6620736372697074206164647265737320776f756c6420696d706c792061206275670a202020202020202020206578706563742053637269dc7074285f29203d20696e7075742e6f75747075742e616464726573732e7061796d656e745f63726564656e7469616c0a2020202020202020202065787065637420496e6c696e65446174756d28737461746529203d20696e7075742e6f75747075742e646174756d0a202020202020202020206578706563742073746174653a2050797468203d2073746174650a20202020202020202020536f6d65287374617465290a20202020202020207d20656c7365207b0a202020202020202020204e6f6e650a20202020202020207d0a2020202020207d2c0a2020202029001640c864660020026eb0c05cc0d0dd500f912cc0040062980103d87a80008992cc006600266ebc00530103d87a8000a50a5140cd1001899801801981d0012066325980099912cc00400a2942265300132598009810981c9baa00189bad303a303d3756607a60746ea800629000206e303c0019baf303c303d0019bab003488966002003159800980100344cc01400d20008a5040e51325980099baf303c0014c010140008acc004cc018010dd6981e98201bab303d001898019ba630410028a5040e91598009980300224001130030078a5040e881d0c0fc00503d0ca60020033756603860726ea8c070c0e4dd5002488cc0f4008cc0f4dd3000a5eb810011112cc00400a26600298103d87a80004bd6f7b63044ca60026eb8c0ec0066eacc0f000660800069112cc004c09000e2b300130230038998029808198209ba60024bd70000c4cc01530103d87a800000640f119800803c006446600e0046608666ec0dd48029ba6004001401c81e0607c00481e229422942294103d1ba633038337606ea40c8dd31981c19bb04c10b4a50797468205374617465004c010101004bd6f7b63025eb7bdb1808928c5660026044606a6ea8c0e4c0d8dd5181c981b1baa30193036375400313259800980b981b1baa0018992cc0040062b3001301f3037375400313259800800c0ca264b30010018992cc00400606913259800800c0d626644b300100181bc4c966002003038899912cc00400607513259800800c0ee07703b81dc4c966002608e007159800981518211baa00a8992cc00400607b13259800800c0fa07d03e81f44cc89660020030408992cc004006083041820c4cc89660020030438992cc004006089044822411226644b300100182344c966002003047823c11e264b30013053003899818009912cc00400a26606402444b3001002898121982a80e25eb8226464b30010018992cc00400609f04f827c13e265300100389802982d80341410251bae001416c60aa00504e827413a09c82d0c14c004c15800905444c8c96600200304c82641320991332259800800c660020031300530590068272046827413a09d04e41686eb8004c14c0090581828800982a00120a482420a0375a003047414c60a00028270dd7000982780120a0304d001412c6eb4004c13000a0828268c1280050481bae00130490024128608e0028228c10cdd500540f104040f10441bae001411c60880028210dd5800982180140e20710384110608200281f8dd5800982000140d606b0354104607c00281e0c0f800a067033819c0cd03f181e000a0743038375400303140d5031818c0c606281e8c0e8c0dcdd5000c54cc0d52412e65787065637420496e6c696e65446174756d28737461746529203d20696e7075742e6f75747075742e646174756d001640d06030606c6ea8c064c0d8dd5000c54cc0d124013a65787065637420536372697074285f29203d20696e7075742e6f75747075742e616464726573732e7061796d656e745f63726564656e7469616c001640cd14c103d87a800040cc607000281b088c96600260346eb8c0e4c0e800626072003153303449011b6578706563742028612c20235b5d29203d2073656c662873726329001640cc6004002444b3001301c3034375400713259800800c00a264b30010018992cc00400600913259800800c566002607a00519800801c66002003006802a010802a010802a074802c01600b00540f8607600281c8c0ec00a007003801c00d03c181c800a06e3035375400700140c8444b3001301b3033375400713259800800c00a264b30010018992cc00400600913259800800c4c96600260720031598009804981c000c01a264b3001303e0048992cc004c0880062b3001303b375400d00980420788acc004c09c006264b3001001804c4c96600200300a805402a264b30013042003806402d03f1bad0018052084303f00140f460766ea801a2b3001301b0018acc004c0ecdd5003402601081e201081c103820703039375400b00740ec604a607000281b200c81d0dd5000c01600b005802a07a303a00140e06074005003801c00e00681d8c0e0005036181a1baa003800a0623712900220388138604a6ea8c0a0c094dd500699800802007454cc08d24011a65787065637420536f6d6528646174756d29203d20646174756d001640888110301937540106e1d2000805402a01500a4078603400260346036002602c6ea800e2c8098602a00260206ea805a293454cc03924011856616c696461746f722072657475726e65642066616c7365001365640341", + "hash": "c6e1b648a954f2e78fc0d45fc12a291e638384f59669fbbb2d511a1e" + }, + { + "title": "osi.oracle_settled_invoice.else", + "redeemer": { + "schema": {} + }, + "parameters": [ + { + "title": "pyth_policy", + "schema": { + "$ref": "#/definitions/cardano~1assets~1PolicyId" + } + }, + { + "title": "base_currency", + "schema": { + "$ref": "#/definitions/Int" + } + } + ], + "compiledCode": "592f380101002229800aba4aba2aba1aba0aab9faab9eaab9dab9cab9a9bae0039bad00248888888888a60022a660089212a65787065637420757064617465733a204c6973743c4279746541727261793e203d2072656465656d657200168a998022491a6578706563742073746174653a2050797468203d20737461746500168a9980224921657870656374206f75745f646174756d3a204f7369446174756d203d206461746100168a9980224938657870656374205b6f75745d203d2066696e645f7363726970745f6f757470757473286f7574707574732c207363726970745f686173682900168a99802249de6578706563742046656564207b0a20202020202020202020666565645f69643a2071756f74655f666565645f69642c0a2020202020202020202070726963653a20536f6d6528536f6d6528626173655f707269636529292c0a202020202020202020206578706f6e656e743a20536f6d6528626173655f6578706f6e656e74292c0a20202020202020202020666565645f7570646174655f74696d657374616d703a20536f6d6528536f6d652874696d657374616d705f3229292c0a202020202020202020202e2e0a20202020202020207d203d20626173655f6665656400168a99802249e56578706563742046656564207b0a20202020202020202020666565645f69643a206164615f70726963655f666565645f69642c0a2020202020202020202070726963653a20536f6d6528536f6d652871756f74655f707269636529292c0a202020202020202020206578706f6e656e743a20536f6d652871756f74655f6578706f6e656e74292c0a20202020202020202020666565645f7570646174655f74696d657374616d703a20536f6d6528536f6d652874696d657374616d705f3129292c0a202020202020202020202e2e0a20202020202020207d203d2071756f74655f6665656400168a9980224933657870656374205b71756f74655f666565642c20626173655f666565645d203d2070726963655f7570646174652e666565647300168a9980224934657870656374205b70726963655f7570646174655d203d206765745f7570646174657328707974685f706f6c6963792c2074782900168a998022493a65787065637420536372697074287363726970745f6861736829203d20696e2e616464726573732e7061796d656e745f63726564656e7469616c00168a998022491672656465656d65723a204f736952656465656d65727300164888888888966002646530013015001980a980b000cdc3a4005301500248889660026004602a6ea800e2646644b300100789919912cc004c00c0062b3001301c3754015002806a03a8acc004c0200062b3001301c3754015002806a03a806a032406433001301a375400b3722911009b8f4881009180f180f800c8c078c07cc07c00644646600200200644660060026004005370e90022444444530013758604800f37586048604a604a00f2323322330020020012259800800c00e2646644b3001300d0028acc004c03000a20030064095133005005302c00440946eb8c094004dd698130009814000a04c3232330010010042259800800c00e2646644b3001300e0028acc004c03400a20030064099133005005302d00440986eb8c098004dd598138009814800a04e14bd6f7b6300a40012232330010010032259800800c5a264b30013375e6052604c6ea800401226012604c6ea800626600600660540048118c0a000502624444b3001300c30243754027159800980618121baa00d89919912cc004c050c09cdd500144c9660020030208992cc004c0b800a264b3001300b302a375400313259800800c566002602660566ea8006264b3001001812c4c966002003026813409a26644b300100181444c9660020030298992cc004c0d800e26602600244b30010028acc004cdd7806804c4cdc498089bab30173034375401e60226eacc05cc0d0dd5005c5282062899192cc004006264b3001001817c0be05f198009bad00189802181d802cc0d8dd50012444b300130210018992cc00400606713259800800c0d206903481a44c966002608200700581aa07c375c0028208c0f800503c181d1baa0058acc004c098006264b3001001819c4c96600200303481a40d2069132598009820801c01606a81f0dd7000a082303e00140f060746ea801606481b9037205e81e0c0d800a05d02e81740b903b181a000981b801206a81520663756003029814c0a50361819800a062375a002606400502640cc60600028170c0b0dd5000c09102940920490248122062302e302b375400315330294912465787065637420496e6c696e65446174756d286461746129203d206f75742e646174756d001640a0601860546ea80060428158c0b000502a191919800800804112cc004006297ae0899912cc004c9660026034605a6ea8006266e3c018dd7181898171baa0018a5040ac6060605a6ea8c0c0c0b4dd500144cc0bc008cc0100100062660080080028150c0b8004c0bc00502c1bae302b3028375400501a40946052604c6ea8c0a4c098dd5000981498131baa00e3300100400e8a99811a492065787065637420536f6d6528696e7075745f646174756d29203d20646174756d00164089159800980618121baa00d899914c004dd698150014dd5981518158014c0a8c09cdd5181518139baa001488966002602c60526ea800633001375c605a60546ea80064605c605e605e605e00337009000cdc4a4001374a90014dd2a400091111119912cc00400604b13259800981b80144c966002003028899912cc00400605513259800981d80144c8c8ca6002607a003303d303e303e303e303e303e303e303e303e0019bad303d003981e80124444b30013025303d3754003132598009813181f1baa0018992cc004c09cc0fcdd500344c966002605060806ea801a264b3001302930413754003132323298009824000cc120c124c124c124c124c124c124c124c1240066eb4c120c114dd50024dd69824001cc1200092222259800981898249baa0018992cc004c0c8c128dd5000c4c966002606660966ea801e264b30013034304c375400f13259800981a98269baa0018cc004dd6982898271baa00191810198289ba8001330514c010101004bd70488cc88c08ccc150dd419b82375a60aa0046eb4c154004cc150dd419b82375a60aa60ac0046eb4c154c1580052f5c060a06ea8008c140dd5000c896600200514a3153304e00116413491112cc004cc0056600266e1c02966002b3001302504d899b8904d483fbfffffc7e294104f441362a660a0921216578706563742076616c7565203e3d20302026262076616c7565203c3d206d61780016413d14a315330504913071756f74655f666565645f6964203d3d2066726f6d5f696e7428626173655f63757272656e637929203f2046616c73650014a08279240138426173652063757272656e6379206665656420646f6573206e6f74206d617463682065787065637465642062617365206173736574206964008992cc004cc009660026644b3001323259800981f000c528c5660026086003132598009822182b9baa303b3058375460b660b06ea8016266e24004012266e200040110551bad305a3057375400514a082a1054182a9baa00130583055375460b060aa6ea800a26464b3001303e0018a508acc004c10c006264b3001304430573754607660b06ea8c0ecc160dd5002c4cdc4802000c4cdc4002000a0aa375a60b460ae6ea800a294505420a83055375400260b060aa6ea8c0e0c154dd5001452820a430013053375407c66e0cdd6982b18299baa021483403e29462a660a292015e74782e76616c69646974795f72616e67650a20207c3e20696e74657276616c2e636f6e7461696e73287536342e61735f696e742870726963655f7570646174652e74696d657374616d705f757329202f203130303029203f2046616c73650014a0828124013850726963652066656564206e6f7420757064617465642077697468696e207472616e73616374696f6e2076616c69646974792072616e6765008acc004cc00966002646464b300130420018992cc004c10cc158dd5181d182b9baa303a3057375400913371000206113371200206082a0dd6982c982b1baa0028acc004c0d80062942294505320a63054375400260ae60a86ea8c0dcc150dd5000980098299baa03e8a518a99828a4814474782e76616c69646974795f72616e67650a20207c3e20696e74657276616c2e69735f656e746972656c795f6265666f726528646561646c696e6529203f2046616c73650014a082812401245061796d656e74206e6f74206d616465206265666f72652074686520646561646c696e65008998012cc004c8cc88ca600200302f81ac09d00111112cc00400e2b30013371200290004528c56600200514a11325980099baf305e305b375460bc00200d13371200460706eacc178c17c0062941058182d1baa305d002416c82ba264b30010038a50899912cc004c110c170dd5000c4c96600266ebcc184c178dd51830801982e802456600266e24c8cdc19bad3062001375a60c460c600260bc6ea8c184c178dd5001000c56600266e24004016330010089831003cc18801a66e04014005008452820b68a50416d14a082d8c0e8dd598301830801454cc16d2416a65787065637420536f6d652865787065637465645f7061796f757429203d0a202020202020202020202020726174696f6e616c2e64697628726174696f6e616c2e66726f6d5f696e742871756f74655f616d6f756e74292c2071756f74655f6d756c7469706c696572290016416860b66ea8c17800cc966002609060b66ea80062980103d87a8000898169982f1980618069bad305c002305f305c375400297ae04164646644b300133710004900044c0bccc180c0bccc180dd41805800998301ba8300b0024bd7025eb822b300133710900000144c0bccc180c0bccc180dd4000998301ba80024bd7025eb822980103d87a8000416c82d8dd6982f8009bad305f3060001305b375400e82e0c17400d05b1919192cc004c0f8c158dd5000c4c966002607e60ae6ea800626601060b660b06ea8004c16cc160dd5001454cc1592414065787065637420536f6d652870726963655f726174696f29203d20726174696f6e616c2e6e657728626173655f70726963652c2071756f74655f70726963652900164154b3001303e0188a6103d87a80008acc004cdc400c24001130283305930283305937506008018660b26ea0c0100612f5c097ae0898141982c98141982c9ba800c33059375003097ae04bd7020a84151153305549016365787065637420536f6d65286d756c7469706c69657229203d0a20202020726174696f6e616c2e66726f6d5f696e7428313029207c3e20726174696f6e616c2e706f7728626173655f6578706f6e656e74202d2071756f74655f6578706f6e656e74290016415064653001375a60b6003375a60b660b8003337020180309112cc0056600260840071337120029000452820b08a60103d87a80008acc004cdc4240000031302c3305d302c3305d37506600c006002660ba6ea0cc0180080052f5c097ae08acc004cdc4000a40011302c3305d302c3305d37506600c0046010002660ba6ea0cc01800cc0200052f5c097ae08a610ad8799fd8799f0101ffff00416082c10580c15cdd50009803a40286002002444b30013371000290004520008acc004c0f80062900144c966002607e66e1800920048cc0040126002007337060049002200889800cc00401260020073370660580049002200841546e0800905420a8302633056375205497ae0370290004528c54cc1452416f7061796f7574735f616e645f72656d61696e696e675f6d61746368287061796565732c206f7574707574732c2071756f74655f6d756c7469706c6965722c20536372697074287363726970745f68617368292c207370656e6461626c655f6c6f76656c61636529203f2046616c73650014a082812401455061796f7574206f757470757473206f722072656d61696e696e67207363726970742066756e647320646f206e6f74206d617463682065787065637465642076616c756573008a50414114a082808c158c15cc15cc15cc15cc15cc15cc15c006294104f20888258c140c134dd5003c10d04a1bad304f304c375400f04241246eb4c138c12cdd5000c105048182698251baa001820208e1824182418241824000982380098211baa00c81ba07e30443041375400d03640f86eb4c10cc100dd500340d503d1bad3042303f375400303440f06082607c6ea800606681d8607a607a607a607a0026078002606e6ea800e05681c0c0e4005037181b800981c000a06a3758602a60666ea800604c81a0c0d400503319b81300d3756602660606ea8028dd6980298181baa01b323232332259800980e181a1baa0028992cc004c074c0d4dd5000c4c966002003031818c4c8cc0640048966002005132330010010042259800800c52f5c113303e98009b8c480126e0120039b89480226e3120009b8c480226e0120074888888cc88cc0408c8d6600266e3cdd71824800a450475d3c793008cc004888c8cc00cdd698268009bae304d304e00130030019b89480426e3120109b804803e600e90082444446600a4646609e6ea0cde5250375c60a00026609e60a060a200297ae05980098029b8d0018998271ba930020013304e37526600860066e340040052f5c1153304a49124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e74001641244646600e4660a06ea0cdc7000a4000660a06ea4cc008c088dc6800800a5eb808cc0208cc144dd419b8e00148000cc144dd49980198119b8d0010014bd701119191982a18119982a1ba800733054375000a660a860aa00297ae033054305530560014bd7018008012cc004c08c00a2653001001a5eb820068008889660026074003123233057374e002660ae6ea40092f5c065300100180252f5c080088896600200510018cc00400e60b600533059305a002001400c82c2246530010059982b982c0008024c0a400e6eb8c160c1640050054c004c8cc15cdd419bca4a06eb8c160004cc15cc160c1640052f5c0b30013016371a00313305637526022002660ac6ea4cc050c04cdc6800800a5eb822a660a492124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e7400164145223305800233058375200297ae09b874801a6e1d200848888c8c8c8cc018ca6002003375860be005302d3305e305f0083305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80003305e4c103d87a80004bd70200222259800801440063300100398310014c8c966002608e00313230323306330640013306330323306330643061375400697ae030653065001306037540071598009826000c4c8c8c0cccc190c194008cc190c194004cc190c0cccc190c194c188dd500225eb80c198c198004c194004c180dd5001c5660026080003132323230343306530660033306530660023306530660013306530343306530663063375400a97ae03067306700130660013065001306037540071598009805000c4c8c8c8c8c0d4cc198c19c010cc198c19c00ccc198c19c008cc198c19c004cc198c0d4cc198c19cc190dd500325eb80c1a0c1a0004c19c004c198004c194004c180dd5001c56600260120031323232323230363306730680053306730680043306730680033306730680023306730680013306730363306730683065375400e97ae03069306900130680013067001306600130650013060375400715980099b87480280062646464646464606e660d060d200c660d060d200a660d060d2008660d060d2006660d060d2004660d060d2002660d0606e660d060d260cc6ea80212f5c060d460d400260d200260d000260ce00260cc00260ca00260c06ea800e2b30013370e9006000c4c8c8c8c8c8c8c8c0e0cc1a4c1a801ccc1a4c1a8018cc1a4c1a8014cc1a4c1a8010cc1a4c1a800ccc1a4c1a8008cc1a4c1a8004cc1a4c0e0cc1a4c1a8c19cdd5004a5eb80c1acc1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c180dd5001c56600266e1d200e00189919191919191919181c99835183580419835183580399835183580319835183580299835183580219835183580199835183580119835183580099835181c99835183598341baa00a4bd7018361836000983580098350009834800983400098338009833000983280098301baa0038acc004cdc3a40200031323232323232323232303a3306b306c0093306b306c0083306b306c0073306b306c0063306b306c0053306b306c0043306b306c0033306b306c0023306b306c0013306b303a3306b306c3069375401697ae0306d306d001306c001306b001306a001306900130680013067001306600130650013060375400715980099b87480480062646464646464646464646076660d860da014660d860da012660d860da010660d860da00e660d860da00c660d860da00a660d860da008660d860da006660d860da004660d860da002660d86076660d860da60d46ea80312f5c060dc60dc00260da00260d800260d600260d400260d200260d000260ce00260cc00260ca00260c06ea800e2b30013370e900a000c4c8c8c8c8c8c8c8c8c8c8c8c0f0cc1b4c1b802ccc1b4c1b8028cc1b4c1b8024cc1b4c1b8020cc1b4c1b801ccc1b4c1b8018cc1b4c1b8014cc1b4c1b8010cc1b4c1b800ccc1b4c1b8008cc1b4c1b8004cc1b4c0f0cc1b4c1b8c1acdd5006a5eb80c1bcc1bc004c1b8004c1b4004c1b0004c1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c180dd5001c56600266e1d20160018991919191919191919191919181e99837183780619837183780599837183780519837183780499837183780419837183780399837183780319837183780299837183780219837183780199837183780119837183780099837181e99837183798361baa00e4bd70183818380009837800983700098368009836000983580098350009834800983400098338009833000983280098301baa0038991919191919191919191919181e998371837806198371837805998371837805198371837804998371837804198371837803998371837803198371837802998371837802198371837801998371837801198371837800998371837983800099837181e99837183798361baa00e4bd7025eb80c1bc004c1b8004c1b4004c1b0004c1ac004c1a8004c1a4004c1a0004c19c004c198004c194004c180dd5001a0ba417482e905d20ba417482e905d20ba417482e905d182f1baa0013061002400c82f8dd7182f182f800991acc004c0b800a2653001001a5eb82006800888966002608a003123233062374e002660c46ea40092f5c065300100180252f5c080088896600200510018cc00400e60cc005330643065002001400c831a246530010059983118318008024c0d000e6eb8c18cc1900050051802800a0b68a9982ca4811165787065637420636f756e74203e3d2030001641606eb8c178c17c00ca60024464b30013001480422b30013001480022900044cdc7244108010204081020408000002416d1598009800a408113370490400219801801980a80144cdc124101010100406600600666e00009203f416c82d8dc4000c888c8cc184c00cdd698310009983098311831800a5eb80c00c006660ba6ea0cdc7000a4000660ba6ea4cc03cc0bcdc6800800a5eb8122232329800919802919198329ba833794940dd718330009983298331833800a5eb816600260366e340062660c86ea4c060004cc190dd49980d180c9b8d0010014bd70454cc181240124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016417c4646006b3001304a0018a60103d87a80008981a198329ba80014bd7020c030030019bad3063003911191acc006600260966eb4c19c00694294506144cc02000c8c014c0d8cc19cdd4000a5eb822601e600898103d87a800041846eb8c19cc1a0004cc194dd419b8e00148000cc194dd49980b981b9b8d0010014bd7024446b3001304a002898019181a99833000a5eb822b3001304f002898019181b19833000a5eb822b3001304300289801919ba548010cc1980052f5c1159800980680144cc01c8c8cc19cdd419bca4a06eb8c1a0004cc19cc1a0c1a40052f5c0b30013034371a00313306637526044002660cc6ea4cc0a0c09cdc6800800a5eb822a660c4920124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e7400164184466e95200633066375000297ae08acc004c03000a26600e464660ce6ea0cde5250375c60d0002660ce60d060d200297ae059800981a1b8d0018998331ba9302200133066375266050604e6e340040052f5c1153306249124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e7400164184466e952008330663750b3001337100029040400244006266e04004c0192020418497ae08acc004cdc3a40140051300323374a900519833000a5eb822b30013370e900600144cc0048cdd2a4018660cc00297ae0232330673750600c6eb4c1a0004cc19cc1a0c1a40052f5c064660ce6ea0cde5250375c60d0002660ce60d060d200297ae059800980e9b8d0018998331ba9301a0013306637526603860366e340040052f5c11533062490124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016418515980099b874803800a266002466e95200e330660014bd70119198339ba833794940dd718340009983398341834800a5eb8166002603a6e340062660cc6ea4c068004cc198dd49980e180d9b8d0010014bd70454cc18924124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016418515980099b874804000a266002466e952010330660014bd70119198339ba833794940dd718340009983398341834800a5eb8166002603a6e340062660cc6ea4c068004cc198dd49980e180d9b8d0010014bd70454cc18924124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016418515980099b874804800a246464660d066e9520123306830690014bd701983418349835000a5eb80c004008cc0208c8cc1a0dd419bca4a06eb8c1a4004cc1a0c1a4c1a80052f5c0b30013035371a00313306737526046002660ce6ea4cc0a4c0a0dc6800800a5eb822a660c6920124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e74001641884b3001304c0018a60103d87980008acc004c144006298103d87a80008acc004c114006298103d87b80008acc004c03c006298103d87c80008acc004c038006298103d87d80008a99831a49204d61726b657453657373696f6e2076616c7565206f7574206f662072616e676500164188831106220c4418915980099b874805000a26006466e952014330660014bd70456600266e1d201600289801919ba548058cc1980052f5c115980099b874806000a266002466e952018330660014bd70119198339ba833794940dd718340009983398341834800a5eb8166002603a6e340062660cc6ea4c068004cc198dd49980e180d9b8d0010014bd70454cc18924124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016418515330614911c4665656450726f7065727479204944206f7574206f662072616e676500164180830106020c04180830106020c04180830106020c041806eb8c198c19c0184b300133710002904040404040404040400144006266e04004c00920800141746006006375a60b800264660b66ea0cdc7000a4000660b66ea4cc034c0b4dc6800800a5eb80dd7182d982e0021050454cc13924011165787065637420636f756e74203e3d2030001641346e312002454cc111240125657870656374206d61676963203d3d2070726963655f7570646174655f6d616769635f6c650016410c6eb8c124c12800566002600e6e3400626608e6ea4c008004cc11cdd49980298021b8d0010014bd70454cc10d240124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e74001641086eb8c098c110dd5198081191acc004cdc79bae304900148904b9011a8200891919191919191991198289810198289829003998289ba90013305130520024bd70198289829182980125eb80d660026040003125980099b89002371a00313305037526601e004002660a06ea66002005337026e3400400a002b8c25eb822a66098920124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016412d153304b4911165787065637420636f756e74203e3d2030001641286eb8c140c144008dd718280021bad304f001323304e375066f29281bae304f0013304e304f30500014bd702cc004c06cdc6800c4cc134dd49804800998269ba93300f300e371a00200297ae08a99824a4924657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e74001641206eb8c134c138004d6600294624b30013371290201b8d0018998261ba93300b48100004cc130dd4cc005204099b80371a002901fc0057184bd70454cc12124124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016411d15330474911165787065637420636f756e74203e3d2030001641186eb8c130c134004c0040096600294624b3001337129040009b8d0018998251ba9330094820004004cc128dd4cc00520800199b80371a002903fc0057184bd70454cc11924124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e740016411515330454911165787065637420636f756e74203e3d203000164111153304449126657870656374206d61676963203d3d20736f6c616e615f666f726d61745f6d616769635f6c650016410c6eb8c124c12800566002600e6e3400626608e6ea4c008004cc11cdd49980298021b8d0010014bd70454cc10d240124657870656374206279746561727261792e6c656e67746828627329203e3d20636f756e74001641086eb8c11c024c00d200430034802066004004608000281ea264b300100181ac0d606b035899180198200021bae0014100607a00481d8dd6000c0c606281e0c0e4c0d8dd5000c54cc0d124016465787065637420536f6d652872656465656d657229203d0a2020202070616972732e6765745f66697273742873656c662e72656465656d6572732c205769746864726177285363726970742873746174652e77697468647261775f736372697074292929001640cc6466446600400400244b30010018a60103d87a80008992cc004cdd7802181c000c4c028cc0ecc0e40052f5c1133003003303d00240d8607600281c8dd5981c981d181d181d181d181d181d181d181d181d181b1baa0213374a90021981b98039981b9805181a9baa30383035375400497ae04bd70454cc0cd241ff65787065637420536f6d6528737461746529203d0a202020206c6973742e66696e645f6d6170280a20202020202073656c662e7265666572656e63655f696e707574732c0a202020202020666e28696e70757429207b0a20202020202020206966206173736574732e6861735f6e66745f73747269637428696e7075742e6f75747075742e76616c75652c20707974685f69642c2022507974682053746174652229207b0a202020202020202020202f2f20737461746520746f6b656e206f757473696465206f6620736372697074206164647265737320776f756c6420696d706c792061206275670a202020202020202020206578706563742053637269dc7074285f29203d20696e7075742e6f75747075742e616464726573732e7061796d656e745f63726564656e7469616c0a2020202020202020202065787065637420496e6c696e65446174756d28737461746529203d20696e7075742e6f75747075742e646174756d0a202020202020202020206578706563742073746174653a2050797468203d2073746174650a20202020202020202020536f6d65287374617465290a20202020202020207d20656c7365207b0a202020202020202020204e6f6e650a20202020202020207d0a2020202020207d2c0a2020202029001640c864660020026eb0c05cc0d0dd500f912cc0040062980103d87a80008992cc006600266ebc00530103d87a8000a50a5140cd1001899801801981d0012066325980099912cc00400a2942265300132598009810981c9baa00189bad303a303d3756607a60746ea800629000206e303c0019baf303c303d0019bab003488966002003159800980100344cc01400d20008a5040e51325980099baf303c0014c010140008acc004cc018010dd6981e98201bab303d001898019ba630410028a5040e91598009980300224001130030078a5040e881d0c0fc00503d0ca60020033756603860726ea8c070c0e4dd5002488cc0f4008cc0f4dd3000a5eb810011112cc00400a26600298103d87a80004bd6f7b63044ca60026eb8c0ec0066eacc0f000660800069112cc004c09000e2b300130230038998029808198209ba60024bd70000c4cc01530103d87a800000640f119800803c006446600e0046608666ec0dd48029ba6004001401c81e0607c00481e229422942294103d1ba633038337606ea40c8dd31981c19bb04c10b4a50797468205374617465004c010101004bd6f7b63025eb7bdb1808928c5660026044606a6ea8c0e4c0d8dd5181c981b1baa30193036375400313259800980b981b1baa0018992cc0040062b3001301f3037375400313259800800c0ca264b30010018992cc00400606913259800800c0d626644b300100181bc4c966002003038899912cc00400607513259800800c0ee07703b81dc4c966002608e007159800981518211baa00a8992cc00400607b13259800800c0fa07d03e81f44cc89660020030408992cc004006083041820c4cc89660020030438992cc004006089044822411226644b300100182344c966002003047823c11e264b30013053003899818009912cc00400a26606402444b3001002898121982a80e25eb8226464b30010018992cc00400609f04f827c13e265300100389802982d80341410251bae001416c60aa00504e827413a09c82d0c14c004c15800905444c8c96600200304c82641320991332259800800c660020031300530590068272046827413a09d04e41686eb8004c14c0090581828800982a00120a482420a0375a003047414c60a00028270dd7000982780120a0304d001412c6eb4004c13000a0828268c1280050481bae00130490024128608e0028228c10cdd500540f104040f10441bae001411c60880028210dd5800982180140e20710384110608200281f8dd5800982000140d606b0354104607c00281e0c0f800a067033819c0cd03f181e000a0743038375400303140d5031818c0c606281e8c0e8c0dcdd5000c54cc0d52412e65787065637420496e6c696e65446174756d28737461746529203d20696e7075742e6f75747075742e646174756d001640d06030606c6ea8c064c0d8dd5000c54cc0d124013a65787065637420536372697074285f29203d20696e7075742e6f75747075742e616464726573732e7061796d656e745f63726564656e7469616c001640cd14c103d87a800040cc607000281b088c96600260346eb8c0e4c0e800626072003153303449011b6578706563742028612c20235b5d29203d2073656c662873726329001640cc6004002444b3001301c3034375400713259800800c00a264b30010018992cc00400600913259800800c566002607a00519800801c66002003006802a010802a010802a074802c01600b00540f8607600281c8c0ec00a007003801c00d03c181c800a06e3035375400700140c8444b3001301b3033375400713259800800c00a264b30010018992cc00400600913259800800c4c96600260720031598009804981c000c01a264b3001303e0048992cc004c0880062b3001303b375400d00980420788acc004c09c006264b3001001804c4c96600200300a805402a264b30013042003806402d03f1bad0018052084303f00140f460766ea801a2b3001301b0018acc004c0ecdd5003402601081e201081c103820703039375400b00740ec604a607000281b200c81d0dd5000c01600b005802a07a303a00140e06074005003801c00e00681d8c0e0005036181a1baa003800a0623712900220388138604a6ea8c0a0c094dd500699800802007454cc08d24011a65787065637420536f6d6528646174756d29203d20646174756d001640888110301937540106e1d2000805402a01500a4078603400260346036002602c6ea800e2c8098602a00260206ea805a293454cc03924011856616c696461746f722072657475726e65642066616c7365001365640341", + "hash": "c6e1b648a954f2e78fc0d45fc12a291e638384f59669fbbb2d511a1e" + } + ], + "definitions": { + "Int": { + "dataType": "integer" + }, + "Pairs": { + "title": "Pairs", + "dataType": "map", + "keys": { + "$ref": "#/definitions/cardano~1address~1PaymentCredential" + }, + "values": { + "$ref": "#/definitions/Int" + } + }, + "aiken/crypto/ScriptHash": { + "title": "ScriptHash", + "dataType": "bytes" + }, + "aiken/crypto/VerificationKeyHash": { + "title": "VerificationKeyHash", + "dataType": "bytes" + }, + "cardano/address/PaymentCredential": { + "title": "PaymentCredential", + "description": "A general structure for representing an on-chain `Credential`.\n\n Credentials are always one of two kinds: a direct public/private key\n pair, or a script (native or Plutus).", + "anyOf": [ + { + "title": "VerificationKey", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1VerificationKeyHash" + } + ] + }, + { + "title": "Script", + "dataType": "constructor", + "index": 1, + "fields": [ + { + "$ref": "#/definitions/aiken~1crypto~1ScriptHash" + } + ] + } + ] + }, + "cardano/assets/PolicyId": { + "title": "PolicyId", + "dataType": "bytes" + }, + "osi/OsiDatum": { + "title": "OsiDatum", + "anyOf": [ + { + "title": "OsiDatum", + "dataType": "constructor", + "index": 0, + "fields": [ + { + "title": "deadline", + "$ref": "#/definitions/Int" + }, + { + "title": "payees", + "$ref": "#/definitions/Pairs" + } + ] + } + ] + }, + "osi/OsiRedeemers": { + "title": "OsiRedeemers", + "anyOf": [ + { + "title": "Fund", + "dataType": "constructor", + "index": 0, + "fields": [] + }, + { + "title": "Payout", + "dataType": "constructor", + "index": 1, + "fields": [] + } + ] + } + } +} \ No newline at end of file diff --git a/lazer/cardano/OSI/on-chain/validators/osi.ak b/lazer/cardano/OSI/on-chain/validators/osi.ak new file mode 100644 index 00000000..25766ce5 --- /dev/null +++ b/lazer/cardano/OSI/on-chain/validators/osi.ak @@ -0,0 +1,176 @@ +use aiken/interval +use aiken/math/rational +use cardano/address.{PaymentCredential, Script} +use cardano/assets.{PolicyId, lovelace_of} +use cardano/transaction.{ + Output, OutputReference, Transaction, find_script_outputs, InlineDatum +} +use pyth.{Feed, PriceUpdate, get_updates} +use types/u32.{U32, from_int} +use types/u64 +use utils.{quote_price_multiplier} + +pub type OsiDatum { + deadline: Int, + payees: Pairs, +} + +pub type OsiRedeemers { + Fund + Payout +} + +/// Oracle-settled Invoice +validator oracle_settled_invoice(pyth_policy: PolicyId, base_currency: Int) { + spend( + datum: Option, + redeemer: OsiRedeemers, + utxo: OutputReference, + tx: Transaction, + ) { + let Transaction { inputs, outputs, .. } = tx + + when redeemer is { + Fund -> { + expect Some(input_datum) = datum + let in = inputs |> transaction.resolve_input(utxo) + expect Script(script_hash) = in.address.payment_credential + expect [out] = find_script_outputs(outputs, script_hash) + + // Verify that the datum is preserved in the output + expect InlineDatum(data) = out.datum + expect out_datum: OsiDatum = data + + and { + input_datum == out_datum, + lovelace_of(out.value) >= lovelace_of(in.value), + } + } + + Payout -> { + expect Some(datum) = datum + let OsiDatum { deadline, payees } = datum + let in = inputs |> transaction.resolve_input(utxo) + expect Script(script_hash) = in.address.payment_credential + let spendable_lovelace = lovelace_of(in.value) - tx.fee + + expect [price_update] = get_updates(pyth_policy, tx) + + expect [quote_feed, base_feed] = price_update.feeds + + expect Feed { + feed_id: ada_price_feed_id, + price: Some(Some(quote_price)), + exponent: Some(quote_exponent), + feed_update_timestamp: Some(Some(timestamp_1)), + .. + } = quote_feed + + expect Feed { + feed_id: quote_feed_id, + price: Some(Some(base_price)), + exponent: Some(base_exponent), + feed_update_timestamp: Some(Some(timestamp_2)), + .. + } = base_feed + + let quote_multiplier = + quote_price_multiplier( + base_price, + base_exponent, + quote_price, + quote_exponent, + ) + + and { + // Base currency feed must match the expected base asset id + trace_if_false( + (quote_feed_id == from_int(base_currency))?, + @"Base currency feed does not match expected base asset id", + ), + // Price feed must be updated within the transaction's validity range + trace_if_false( + (tx.validity_range + |> interval.contains(u64.as_int(price_update.timestamp_us) / 1000))?, + @"Price feed not updated within transaction validity range" + ), + // Payment must be made before the deadline + trace_if_false( + (tx.validity_range + |> interval.is_entirely_before(deadline))?, + @"Payment not made before the deadline", + ), + // Payouts must be made in datum order, and any remainder must stay in the script + trace_if_false( + payouts_and_remaining_match( + payees, + outputs, + quote_multiplier, + Script(script_hash), + spendable_lovelace, + )?, + @"Payout outputs or remaining script funds do not match expected values" + ) + } + } + } + } + + else(_) { + fail + } +} + +fn trace_if_false(condition: Bool, message: String) -> Bool { + if condition { + True + } else { + fail message + } +} + +fn payouts_and_remaining_match( + payees: Pairs, + outputs: List, + quote_multiplier: rational.Rational, + script_credential: PaymentCredential, + remaining_input_lovelace: Int, +) -> Bool { + when payees is { + [] -> + if remaining_input_lovelace <= 0 { + True + } else { + when outputs is { + [Output { address, value, .. }, ..] -> + and { + address.payment_credential == script_credential, + lovelace_of(value) >= remaining_input_lovelace, + } + [] -> False + } + } + [Pair(payee, quote_amount), ..rest_payees] -> + when outputs is { + [] -> False + [Output { address, value, .. }, ..rest_outputs] -> { + expect Some(expected_payout) = + rational.div(rational.from_int(quote_amount), quote_multiplier) + let paid_lovelace = lovelace_of(value) + + and { + address.payment_credential == payee, + paid_lovelace >= rational.floor(expected_payout), + paid_lovelace <= remaining_input_lovelace, + payouts_and_remaining_match( + rest_payees, + rest_outputs, + quote_multiplier, + script_credential, + remaining_input_lovelace - paid_lovelace, + ), + } + } + } + } +}