diff --git a/.cspell/custom-dictionary.txt b/.cspell/custom-dictionary.txt index a38f1c7..e03e01d 100644 --- a/.cspell/custom-dictionary.txt +++ b/.cspell/custom-dictionary.txt @@ -1,10 +1,16 @@ # Custom Dictionary Words Aptos +secp +borsh +Borsh CCTP Devnet hashv idls keccak +keypair +keypairs +KEYPAIR Linea Mezo permissionless diff --git a/.github/workflows/svm.yml b/.github/workflows/svm.yml index f78f19a..4e99c04 100644 --- a/.github/workflows/svm.yml +++ b/.github/workflows/svm.yml @@ -4,12 +4,12 @@ on: pull_request: jobs: - test: + anchor-test: name: Anchor Test runs-on: ubuntu-latest defaults: run: - working-directory: svm + working-directory: svm/anchor steps: - uses: actions/checkout@v4 - name: Get solana version @@ -29,8 +29,47 @@ jobs: node-version: "22.16.0" anchor-version: "${{steps.anchor.outputs.version}}" solana-cli-version: "${{steps.solana.outputs.version}}" - working-directory: "svm" + working-directory: "svm/anchor" - run: cargo fmt --check --all - run: cargo clippy - run: cargo test + + pinocchio-test: + name: Pinocchio Test + runs-on: ubuntu-latest + defaults: + run: + working-directory: svm/pinocchio + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: nightly-2025-04-14 + components: rustfmt, clippy + - name: Install Solana CLI + run: | + sh -c "$(curl -sSfL https://release.anza.xyz/v1.18.17/install)" + echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH + - name: Generate test keypair + run: | + mkdir -p ../test-keys + solana-keygen new --no-bip39-passphrase -o ../test-keys/quoter-updater.json + echo "QUOTER_UPDATER_PUBKEY=$(solana-keygen pubkey ../test-keys/quoter-updater.json)" >> $GITHUB_ENV + echo "QUOTER_UPDATER_KEYPAIR_PATH=${{ github.workspace }}/svm/test-keys/quoter-updater.json" >> $GITHUB_ENV + - name: Build pinocchio programs + run: | + cargo build-sbf --manifest-path programs/executor-quoter/Cargo.toml + cargo build-sbf --manifest-path programs/executor-quoter-router/Cargo.toml + - name: Build anchor executor program + working-directory: svm/anchor + run: | + cargo build-sbf --manifest-path programs/executor/Cargo.toml + cp target/deploy/executor.so ../pinocchio/target/deploy/ + - run: cargo fmt --check --all + - run: cargo clippy --all-targets + - name: Run tests + env: + SBF_OUT_DIR: ${{ github.workspace }}/svm/pinocchio/target/deploy + run: cargo test -p executor-quoter -p executor-quoter-tests -p executor-quoter-router-tests diff --git a/.gitignore b/.gitignore index c4ca88d..5af53aa 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ node_modules/ lcov.info .env .env.* +.devcontainer +.claude diff --git a/.prettierignore b/.prettierignore index bfdf25e..6158b18 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,3 +1,11 @@ evm/cache/ evm/lib/ evm/out/ +evm/broadcast/ +svm/anchor/target/ +svm/node_modules/ +svm/modules/*/target/ +svm/pinocchio/target/ +svm/pinocchio/programs/*/target/ +svm/pinocchio/tests/*/target/ +svm/target/ diff --git a/README.md b/README.md index 3ca0e3d..ee64968 100644 --- a/README.md +++ b/README.md @@ -188,7 +188,6 @@ This will generate: After building, you can view the API documentation using: 1. **Swagger Editor** (Online) - - Visit https://editor.swagger.io/ - Copy and paste the contents of `tsp-output/schema/openapi.yaml` diff --git a/cspell.json b/cspell.json index 368b62d..616f3b2 100644 --- a/cspell.json +++ b/cspell.json @@ -1,6 +1,6 @@ { "version": "0.2", - "ignorePaths": [], + "ignorePaths": ["node_modules/", "svm/node_modules/", "evm/lib/"], "dictionaryDefinitions": [ { "name": "custom-dictionary", diff --git a/design/02_On_Chain_Quotes.md b/design/02_On_Chain_Quotes.md index dcf75af..39aae83 100644 --- a/design/02_On_Chain_Quotes.md +++ b/design/02_On_Chain_Quotes.md @@ -7,7 +7,7 @@ Some would-be Executor integrators may need on-chain quotes as they do not neces ## Runtime Support - [x] [EVM](./evm/) -- [ ] [SVM](./svm/) +- [x] [SVM](./svm/) # **Background** @@ -91,7 +91,6 @@ interface IExecutorQuoter { ``` 2. **ExecutorQuoterRouter** replaces **Executor** as the entry-point for integrators. It MUST be immutable and non-administered / fully permissionless. This provides three critical functionalities. - 1. `updateQuoterContract(bytes calldata gov)` allows a Quoter to set their `ExecutorQuoter` contract via signed governance (detailed below). This MUST 1. Verify the chain ID matches the Executor’s `ourChain`. 2. Verify the contract address is an EVM address. @@ -135,11 +134,33 @@ interface IExecutorQuoterRouter { ### SVM -The SVM implementation should follow the requirements above relevant to the SVM Executor implementation. +On SVM, two programs are introduced. + +1. **ExecutorQuoter** is a Pinocchio-based program that implements the quoting logic. It exposes two CPI-callable instructions: + - `RequestQuote` (discriminator `[2, 0, 0, 0, 0, 0, 0, 0]`): Returns `(payee_address, required_payment)` via return data. + - `RequestExecutionQuote` (discriminator `[3, 0, 0, 0, 0, 0, 0, 0]`): Returns `(required_payment, payee_address, quote_body)` as 72 bytes via return data. + + The quoter reads pricing data from on-chain PDAs (`ChainInfo`, `QuoteBody`) maintained by an authorized updater. + +2. **ExecutorQuoterRouter** is the entry-point for integrators. It provides three instructions: + 1. `UpdateQuoterContract` registers or updates a quoter's implementation mapping. This MUST: + - Verify the chain ID matches `OUR_CHAIN`. + - Verify the sender matches `universal_sender_address` in the governance message. + - Verify the governance has not expired. + - Verify the secp256k1 signature recovers to the quoter address (20-byte EVM address). + - Create or update a `QuoterRegistration` PDA seeded by `["quoter_registration", quoter_address]`. + + 2. `QuoteExecution` gets a quote from a registered quoter via CPI. The quoter's return data is forwarded to the caller. + + 3. `RequestExecution` requests execution through the router. This MUST: + - CPI to the quoter's `RequestExecutionQuote` to get payment/payee/quote body. + - Verify payment amount is sufficient. + - Construct an `EQ02` signed quote on-chain. + - CPI to Executor's `request_for_execution`. + + **Account Handling**: The router uses a fixed account layout for quoter CPIs. The quoter accounts (`config`, `chain_info`, `quote_body`) are passed by the client and forwarded to the quoter program. This allows different quoter implementations to use different account structures while maintaining a standardized router interface. - + **Quoter Identity**: Quoters are identified by their 20-byte EVM address (derived from secp256k1 public key). The `QuoterRegistration` PDA maps this address to a Solana program ID that implements the quoting logic. ### Other diff --git a/svm/.gitignore b/svm/.gitignore index 2e0446b..5532e19 100644 --- a/svm/.gitignore +++ b/svm/.gitignore @@ -4,4 +4,11 @@ target **/*.rs.bk node_modules test-ledger +test-keys .yarn + +# Keypair files (sensitive) +*-keypair.json +*_keypair.json +keypair.json +keypairs/ diff --git a/svm/Anchor.toml b/svm/anchor/Anchor.toml similarity index 88% rename from svm/Anchor.toml rename to svm/anchor/Anchor.toml index 41cd6a1..b180e6b 100644 --- a/svm/Anchor.toml +++ b/svm/anchor/Anchor.toml @@ -18,3 +18,6 @@ wallet = "~/.config/solana/id.json" [scripts] test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" + +[test.validator] +url = "http://127.0.0.1:8899" diff --git a/svm/Cargo.lock b/svm/anchor/Cargo.lock similarity index 99% rename from svm/Cargo.lock rename to svm/anchor/Cargo.lock index 9e0f5f9..97365f7 100644 --- a/svm/Cargo.lock +++ b/svm/anchor/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "ahash" @@ -779,6 +779,7 @@ name = "executor" version = "0.1.0" dependencies = [ "anchor-lang", + "executor-requests", ] [[package]] diff --git a/svm/Cargo.toml b/svm/anchor/Cargo.toml similarity index 87% rename from svm/Cargo.toml rename to svm/anchor/Cargo.toml index 9a84f4a..f397704 100644 --- a/svm/Cargo.toml +++ b/svm/anchor/Cargo.toml @@ -1,6 +1,5 @@ [workspace] -members = [ - "modules/*", +members = [ "programs/*" ] resolver = "2" diff --git a/svm/programs/executor/Cargo.toml b/svm/anchor/programs/executor/Cargo.toml similarity index 83% rename from svm/programs/executor/Cargo.toml rename to svm/anchor/programs/executor/Cargo.toml index 78a5c07..ea1fc10 100644 --- a/svm/programs/executor/Cargo.toml +++ b/svm/anchor/programs/executor/Cargo.toml @@ -18,3 +18,4 @@ idl-build = ["anchor-lang/idl-build"] [dependencies] anchor-lang = "0.30.1" +executor-requests = { path = "../../../modules/executor-requests" } diff --git a/svm/programs/executor/src/lib.rs b/svm/anchor/programs/executor/src/lib.rs similarity index 100% rename from svm/programs/executor/src/lib.rs rename to svm/anchor/programs/executor/src/lib.rs diff --git a/svm/bun.lock b/svm/bun.lock new file mode 100644 index 0000000..d584d50 --- /dev/null +++ b/svm/bun.lock @@ -0,0 +1,187 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "dependencies": { + "@coral-xyz/anchor": "^0.30.1", + "@noble/hashes": "^2.0.1", + "@noble/secp256k1": "^3.0.0", + "js-sha3": "^0.9.3", + }, + "devDependencies": { + "@types/bun": "^1.3.4", + "@types/node": "^24.10.1", + "typescript": "^5.9.3", + }, + }, + }, + "packages": { + "@babel/runtime": ["@babel/runtime@7.25.6", "", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ=="], + + "@coral-xyz/anchor": ["@coral-xyz/anchor@0.30.1", "", { "dependencies": { "@coral-xyz/anchor-errors": "^0.30.1", "@coral-xyz/borsh": "^0.30.1", "@noble/hashes": "^1.3.1", "@solana/web3.js": "^1.68.0", "bn.js": "^5.1.2", "bs58": "^4.0.1", "buffer-layout": "^1.2.2", "camelcase": "^6.3.0", "cross-fetch": "^3.1.5", "crypto-hash": "^1.3.0", "eventemitter3": "^4.0.7", "pako": "^2.0.3", "snake-case": "^3.0.4", "superstruct": "^0.15.4", "toml": "^3.0.0" } }, "sha512-gDXFoF5oHgpriXAaLpxyWBHdCs8Awgf/gLHIo6crv7Aqm937CNdY+x+6hoj7QR5vaJV7MxWSQ0NGFzL3kPbWEQ=="], + + "@coral-xyz/anchor-errors": ["@coral-xyz/anchor-errors@0.30.1", "", {}, "sha512-9Mkradf5yS5xiLWrl9WrpjqOrAV+/W2RQHDlbnAZBivoGpOs1ECjoDCkVk4aRG8ZdiFiB8zQEVlxf+8fKkmSfQ=="], + + "@coral-xyz/borsh": ["@coral-xyz/borsh@0.30.1", "", { "dependencies": { "bn.js": "^5.1.2", "buffer-layout": "^1.2.0" } }, "sha512-aaxswpPrCFKl8vZTbxLssA2RvwX2zmKLlRCIktJOwW+VpVwYtXRtlWiIP+c2pPRKneiTiWCN2GEMSH9j1zTlWQ=="], + + "@noble/curves": ["@noble/curves@1.6.0", "", { "dependencies": { "@noble/hashes": "1.5.0" } }, "sha512-TlaHRXDehJuRNR9TfZDNQ45mMEd5dwUwmicsafcIX4SsNiqnCHKjE/1alYPd/lDRVhxdhUAlv8uEhMCI5zjIJQ=="], + + "@noble/hashes": ["@noble/hashes@2.0.1", "", {}, "sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw=="], + + "@noble/secp256k1": ["@noble/secp256k1@3.0.0", "", {}, "sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg=="], + + "@solana/buffer-layout": ["@solana/buffer-layout@4.0.1", "", { "dependencies": { "buffer": "~6.0.3" } }, "sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA=="], + + "@solana/web3.js": ["@solana/web3.js@1.95.3", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "agentkeepalive": "^4.5.0", "bigint-buffer": "^1.1.5", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-O6rPUN0w2fkNqx/Z3QJMB9L225Ex10PRDH8bTaIUPZXMPV0QP8ZpPvjQnXK+upUczlRgzHzd6SjKIha1p+I6og=="], + + "@swc/helpers": ["@swc/helpers@0.5.13", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w=="], + + "@types/bun": ["@types/bun@1.3.4", "", { "dependencies": { "bun-types": "1.3.4" } }, "sha512-EEPTKXHP+zKGPkhRLv+HI0UEX8/o+65hqARxLy8Ov5rIxMBPNTjeZww00CIihrIQGEQBYg+0roO5qOnS/7boGA=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/node": ["@types/node@24.10.1", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ=="], + + "@types/uuid": ["@types/uuid@8.3.4", "", {}, "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="], + + "@types/ws": ["@types/ws@7.4.7", "", { "dependencies": { "@types/node": "*" } }, "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww=="], + + "JSONStream": ["JSONStream@1.3.5", "", { "dependencies": { "jsonparse": "^1.2.0", "through": ">=2.2.7 <3" }, "bin": { "JSONStream": "./bin.js" } }, "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ=="], + + "agentkeepalive": ["agentkeepalive@4.5.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew=="], + + "base-x": ["base-x@3.0.10", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "bigint-buffer": ["bigint-buffer@1.1.5", "", { "dependencies": { "bindings": "^1.3.0" } }, "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA=="], + + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], + + "bn.js": ["bn.js@5.2.1", "", {}, "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ=="], + + "borsh": ["borsh@0.7.0", "", { "dependencies": { "bn.js": "^5.2.0", "bs58": "^4.0.0", "text-encoding-utf-8": "^1.0.2" } }, "sha512-CLCsZGIBCFnPtkNnieW/a8wmreDmfUtjU2m9yHrzPXIlNbqVs0AQrSatSG6vdNYUqdc83tkQi2eHfF98ubzQLA=="], + + "bs58": ["bs58@4.0.1", "", { "dependencies": { "base-x": "^3.0.2" } }, "sha512-Ok3Wdf5vOIlBrgCvTq96gBkJw+JUEzdBgyaza5HLtPm7yTHkjRy8+JzNyHF7BHa0bNWOQIp3m5YF0nnFcOIKLw=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "buffer-layout": ["buffer-layout@1.2.2", "", {}, "sha512-kWSuLN694+KTk8SrYvCqwP2WcgQjoRCiF5b4QDvkkz8EmgD+aWAIceGFKMIAdmF/pH+vpgNV3d3kAKorcdAmWA=="], + + "bufferutil": ["bufferutil@4.0.8", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw=="], + + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], + + "camelcase": ["camelcase@6.3.0", "", {}, "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA=="], + + "commander": ["commander@2.20.3", "", {}, "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ=="], + + "cross-fetch": ["cross-fetch@3.1.8", "", { "dependencies": { "node-fetch": "^2.6.12" } }, "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg=="], + + "crypto-hash": ["crypto-hash@1.3.0", "", {}, "sha512-lyAZ0EMyjDkVvz8WOeVnuCPvKVBXcMv1l5SVqO1yC7PzTwrD/pPje/BIRbWhMoPe436U+Y2nD7f5bFx0kt+Sbg=="], + + "delay": ["delay@5.0.0", "", {}, "sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw=="], + + "dot-case": ["dot-case@3.0.4", "", { "dependencies": { "no-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w=="], + + "es6-promise": ["es6-promise@4.2.8", "", {}, "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="], + + "es6-promisify": ["es6-promisify@5.0.0", "", { "dependencies": { "es6-promise": "^4.0.3" } }, "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ=="], + + "eventemitter3": ["eventemitter3@4.0.7", "", {}, "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw=="], + + "eyes": ["eyes@0.1.8", "", {}, "sha512-GipyPsXO1anza0AOZdy69Im7hGFCNB7Y/NGjDlZGJ3GJJLtwNSb2vrzYrTYJRrRloVx7pl+bhUaTB8yiccPvFQ=="], + + "fast-stable-stringify": ["fast-stable-stringify@1.0.0", "", {}, "sha512-wpYMUmFu5f00Sm0cj2pfivpmawLZ0NKdviQ4w9zJeR8JVtOpOxHmLaJuj0vxvGqMJQWyP/COUkF75/57OKyRag=="], + + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], + + "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "isomorphic-ws": ["isomorphic-ws@4.0.1", "", {}, "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w=="], + + "jayson": ["jayson@4.1.2", "", { "dependencies": { "@types/connect": "^3.4.33", "@types/node": "^12.12.54", "@types/ws": "^7.4.4", "JSONStream": "^1.3.5", "commander": "^2.20.3", "delay": "^5.0.0", "es6-promisify": "^5.0.0", "eyes": "^0.1.8", "isomorphic-ws": "^4.0.1", "json-stringify-safe": "^5.0.1", "uuid": "^8.3.2", "ws": "^7.5.10" }, "bin": { "jayson": "bin/jayson.js" } }, "sha512-5nzMWDHy6f+koZOuYsArh2AXs73NfWYVlFyJJuCedr93GpY+Ku8qq10ropSXVfHK+H0T6paA88ww+/dV+1fBNA=="], + + "js-sha3": ["js-sha3@0.9.3", "", {}, "sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg=="], + + "json-stringify-safe": ["json-stringify-safe@5.0.1", "", {}, "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="], + + "jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="], + + "lower-case": ["lower-case@2.0.2", "", { "dependencies": { "tslib": "^2.0.3" } }, "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], + + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" } }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], + + "node-gyp-build": ["node-gyp-build@4.8.2", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-test": "build-test.js", "node-gyp-build-optional": "optional.js" } }, "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw=="], + + "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], + + "regenerator-runtime": ["regenerator-runtime@0.14.1", "", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="], + + "rpc-websockets": ["rpc-websockets@9.0.2", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "uuid": "^8.3.2", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" } }, "sha512-YzggvfItxMY3Lwuax5rC18inhbjJv9Py7JXRHxTIi94JOLrqBsSsUUc5bbl5W6c11tXhdfpDPK0KzBhoGe8jjw=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "snake-case": ["snake-case@3.0.4", "", { "dependencies": { "dot-case": "^3.0.4", "tslib": "^2.0.3" } }, "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg=="], + + "superstruct": ["superstruct@0.15.5", "", {}, "sha512-4AOeU+P5UuE/4nOUkmcQdW5y7i9ndt1cQd/3iUe+LTz3RxESf/W/5lg4B74HbDMMv8PHnPnGCQFH45kBcrQYoQ=="], + + "text-encoding-utf-8": ["text-encoding-utf-8@1.0.2", "", {}, "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg=="], + + "through": ["through@2.3.8", "", {}, "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg=="], + + "toml": ["toml@3.0.0", "", {}, "sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], + + "tslib": ["tslib@2.7.0", "", {}, "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "utf-8-validate": ["utf-8-validate@5.0.10", "", { "dependencies": { "node-gyp-build": "^4.3.0" } }, "sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ=="], + + "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "ws": ["ws@7.5.10", "", {}, "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ=="], + + "@coral-xyz/anchor/@noble/hashes": ["@noble/hashes@1.5.0", "", {}, "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA=="], + + "@noble/curves/@noble/hashes": ["@noble/hashes@1.5.0", "", {}, "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA=="], + + "@solana/web3.js/@noble/hashes": ["@noble/hashes@1.5.0", "", {}, "sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA=="], + + "@solana/web3.js/superstruct": ["superstruct@2.0.2", "", {}, "sha512-uV+TFRZdXsqXTL2pRvujROjdZQ4RAlBUS5BTh9IGm+jTqQntYThciG/qu57Gs69yjnVUSqdxF9YLmSnpupBW9A=="], + + "@types/connect/@types/node": ["@types/node@22.5.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA=="], + + "@types/ws/@types/node": ["@types/node@22.5.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA=="], + + "jayson/@types/node": ["@types/node@12.20.55", "", {}, "sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ=="], + + "rpc-websockets/@types/ws": ["@types/ws@8.5.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-3tPRkv1EtkDpzlgyKyI8pGsGZAGPEaXeu0DOj5DI25Ja91bdAYddYHbADRYVrZMRbfW+1l5YwXVDKohDJNQxkQ=="], + + "rpc-websockets/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], + + "rpc-websockets/ws": ["ws@8.18.0", "", {}, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="], + + "@types/connect/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "@types/ws/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + + "rpc-websockets/@types/ws/@types/node": ["@types/node@22.5.5", "", { "dependencies": { "undici-types": "~6.19.2" } }, "sha512-Xjs4y5UPO/CLdzpgR6GirZJx36yScjh73+2NlLlkFRSoQN8B0DpfXPdZGnvVmLRLOsqDpOfTNv7D9trgGhmOIA=="], + + "rpc-websockets/@types/ws/@types/node/undici-types": ["undici-types@6.19.8", "", {}, "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="], + } +} diff --git a/svm/modules/executor-requests/Cargo.lock b/svm/modules/executor-requests/Cargo.lock new file mode 100644 index 0000000..9b2c9a3 --- /dev/null +++ b/svm/modules/executor-requests/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "executor-requests" +version = "0.0.1" diff --git a/svm/modules/executor-requests/src/lib.rs b/svm/modules/executor-requests/src/lib.rs index 7c1118d..e53fd72 100644 --- a/svm/modules/executor-requests/src/lib.rs +++ b/svm/modules/executor-requests/src/lib.rs @@ -1,3 +1,9 @@ +#![no_std] + +extern crate alloc; +use alloc::vec::Vec; + +// Request type prefixes const REQ_VAA_V1: &[u8; 4] = b"ERV1"; const REQ_NTT_V1: &[u8; 4] = b"ERN1"; const REQ_CCTP_V1: &[u8; 4] = b"ERC1"; @@ -63,9 +69,183 @@ pub fn make_cctp_v2_request() -> Vec { out } +// ============================================================================ +// Relay Instructions +// ============================================================================ +// +// Relay instructions tell the executor how to relay a message. The format +// matches the Wormhole SDK `relayInstructionsLayout` from +// @wormhole-foundation/sdk-definitions. +// +// Instructions are concatenated together. Each instruction starts with a +// 1-byte type discriminator followed by type-specific data. All multi-byte +// integers are big-endian. + +/// Relay instruction type discriminators +pub const RELAY_IX_GAS: u8 = 1; +pub const RELAY_IX_GAS_DROP_OFF: u8 = 2; + +/// Encodes a GasInstruction relay instruction. +/// +/// Layout (33 bytes): +/// - type: u8 = 1 +/// - gas_limit: u128 be (16 bytes) +/// - msg_value: u128 be (16 bytes) +pub fn make_relay_instruction_gas(gas_limit: u128, msg_value: u128) -> Vec { + let mut out = Vec::with_capacity(33); + out.push(RELAY_IX_GAS); + out.extend_from_slice(&gas_limit.to_be_bytes()); + out.extend_from_slice(&msg_value.to_be_bytes()); + out +} + +/// Encodes a GasDropOffInstruction relay instruction. +/// +/// Layout (49 bytes): +/// - type: u8 = 2 +/// - drop_off: u128 be (16 bytes) +/// - recipient: [u8; 32] (universal address) +pub fn make_relay_instruction_gas_drop_off(drop_off: u128, recipient: &[u8; 32]) -> Vec { + let mut out = Vec::with_capacity(49); + out.push(RELAY_IX_GAS_DROP_OFF); + out.extend_from_slice(&drop_off.to_be_bytes()); + out.extend_from_slice(recipient); + out +} + +/// Builder for constructing relay instructions. +/// +/// Multiple instructions can be combined by appending them together. +/// This is a convenience wrapper that allows chaining. +#[derive(Default)] +pub struct RelayInstructionsBuilder { + data: Vec, +} + +impl RelayInstructionsBuilder { + pub fn new() -> Self { + Self { data: Vec::new() } + } + + /// Add a GasInstruction to the relay instructions. + pub fn with_gas(mut self, gas_limit: u128, msg_value: u128) -> Self { + self.data + .extend(make_relay_instruction_gas(gas_limit, msg_value)); + self + } + + /// Add a GasDropOffInstruction to the relay instructions. + pub fn with_gas_drop_off(mut self, drop_off: u128, recipient: &[u8; 32]) -> Self { + self.data + .extend(make_relay_instruction_gas_drop_off(drop_off, recipient)); + self + } + + /// Build the final relay instructions bytes. + pub fn build(self) -> Vec { + self.data + } +} + +// ============================================================================ +// Relay Instruction Parsing +// ============================================================================ + +/// Relay instruction parsing errors. +/// +/// Discriminants are ordered to align with executor-quoter error codes (base 0x1002): +/// - 0 -> UnsupportedInstruction (0x1002) +/// - 1 -> MoreThanOneDropOff (0x1003) +/// - 2 -> MathOverflow (0x1004) +/// - 3 -> InvalidRelayInstructions (0x1005) +#[repr(u8)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum RelayParseError { + /// Unknown relay instruction type + UnsupportedType = 0, + /// More than one drop-off instruction found + MultipleDropoff = 1, + /// Arithmetic overflow when accumulating gas_limit or msg_value + Overflow = 2, + /// Instruction data truncated / not enough bytes + Truncated = 3, +} + +/// Parses relay instructions to extract total gas limit and msg value. +/// +/// Returns `(gas_limit, msg_value)` on success, or `RelayParseError` on failure. +/// Multiple gas instructions are summed. Only one dropoff is allowed. +/// +/// Instruction format: +/// - Type 1 (Gas): 1 byte type + 16 bytes gas_limit + 16 bytes msg_value = 33 bytes +/// - Type 2 (DropOff): 1 byte type + 16 bytes msg_value + 32 bytes recipient = 49 bytes +pub fn parse_relay_instructions(data: &[u8]) -> Result<(u128, u128), RelayParseError> { + let mut offset = 0; + let mut gas_limit: u128 = 0; + let mut msg_value: u128 = 0; + let mut has_drop_off = false; + + while offset < data.len() { + let ix_type = data[offset]; + offset += 1; + + match ix_type { + RELAY_IX_GAS => { + // Gas instruction: 16 bytes gas_limit + 16 bytes msg_value + if offset + 32 > data.len() { + return Err(RelayParseError::Truncated); + } + + let mut ix_gas_bytes = [0u8; 16]; + ix_gas_bytes.copy_from_slice(&data[offset..offset + 16]); + let ix_gas_limit = u128::from_be_bytes(ix_gas_bytes); + offset += 16; + + let mut ix_val_bytes = [0u8; 16]; + ix_val_bytes.copy_from_slice(&data[offset..offset + 16]); + let ix_msg_value = u128::from_be_bytes(ix_val_bytes); + offset += 16; + + gas_limit = gas_limit + .checked_add(ix_gas_limit) + .ok_or(RelayParseError::Overflow)?; + msg_value = msg_value + .checked_add(ix_msg_value) + .ok_or(RelayParseError::Overflow)?; + } + RELAY_IX_GAS_DROP_OFF => { + if has_drop_off { + return Err(RelayParseError::MultipleDropoff); + } + has_drop_off = true; + + // DropOff instruction: 16 bytes msg_value + 32 bytes recipient + if offset + 48 > data.len() { + return Err(RelayParseError::Truncated); + } + + let mut ix_val_bytes = [0u8; 16]; + ix_val_bytes.copy_from_slice(&data[offset..offset + 16]); + let ix_msg_value = u128::from_be_bytes(ix_val_bytes); + offset += 48; // Skip msg_value (16) + recipient (32) + + msg_value = msg_value + .checked_add(ix_msg_value) + .ok_or(RelayParseError::Overflow)?; + } + _ => { + return Err(RelayParseError::UnsupportedType); + } + } + } + + Ok((gas_limit, msg_value)) +} + #[cfg(test)] mod tests { use super::*; + use alloc::vec; #[test] fn test_vaa_v1() { @@ -131,4 +311,200 @@ mod tests { let result = make_cctp_v2_request(); assert_eq!(result, [0x45, 0x52, 0x43, 0x32, 0x01]); } + + #[test] + fn test_relay_instruction_gas() { + // GasInstruction with gasLimit=250_000 and msgValue=1_000_000 + let result = make_relay_instruction_gas(250_000, 1_000_000); + assert_eq!(result.len(), 33); + assert_eq!(result[0], RELAY_IX_GAS); // type = 1 + + // gas_limit: 250_000 = 0x3D090 as u128 big-endian (16 bytes) + let expected_gas_limit: [u8; 16] = 250_000u128.to_be_bytes(); + assert_eq!(&result[1..17], &expected_gas_limit); + + // msg_value: 1_000_000 = 0xF4240 as u128 big-endian (16 bytes) + let expected_msg_value: [u8; 16] = 1_000_000u128.to_be_bytes(); + assert_eq!(&result[17..33], &expected_msg_value); + } + + #[test] + fn test_relay_instruction_gas_drop_off() { + let recipient = [0xAB; 32]; + let result = make_relay_instruction_gas_drop_off(500_000, &recipient); + assert_eq!(result.len(), 49); + assert_eq!(result[0], RELAY_IX_GAS_DROP_OFF); // type = 2 + + // drop_off: 500_000 as u128 big-endian (16 bytes) + let expected_drop_off: [u8; 16] = 500_000u128.to_be_bytes(); + assert_eq!(&result[1..17], &expected_drop_off); + + // recipient: 32 bytes + assert_eq!(&result[17..49], &recipient); + } + + #[test] + fn test_relay_instructions_builder() { + let recipient = [0xCD; 32]; + let result = RelayInstructionsBuilder::new() + .with_gas(100_000, 200_000) + .with_gas_drop_off(300_000, &recipient) + .build(); + + // Total: 33 + 49 = 82 bytes + assert_eq!(result.len(), 82); + + // First instruction: GasInstruction + assert_eq!(result[0], RELAY_IX_GAS); + assert_eq!(&result[1..17], &100_000u128.to_be_bytes()); + assert_eq!(&result[17..33], &200_000u128.to_be_bytes()); + + // Second instruction: GasDropOffInstruction + assert_eq!(result[33], RELAY_IX_GAS_DROP_OFF); + assert_eq!(&result[34..50], &300_000u128.to_be_bytes()); + assert_eq!(&result[50..82], &recipient); + } + + #[test] + fn test_relay_instructions_builder_empty() { + let result = RelayInstructionsBuilder::new().build(); + assert_eq!(result.len(), 0); + } + + // ======================================================================== + // parse_relay_instructions tests + // ======================================================================== + + #[test] + fn test_parse_relay_instructions_empty() { + let result = parse_relay_instructions(&[]); + assert_eq!(result, Ok((0, 0))); + } + + #[test] + fn test_parse_relay_instructions_gas() { + let data = make_relay_instruction_gas(250_000, 1_000_000); + let result = parse_relay_instructions(&data); + assert_eq!(result, Ok((250_000, 1_000_000))); + } + + #[test] + fn test_parse_relay_instructions_dropoff() { + let recipient = [0xAB; 32]; + let data = make_relay_instruction_gas_drop_off(500_000, &recipient); + let result = parse_relay_instructions(&data); + // DropOff contributes to msg_value, not gas_limit + assert_eq!(result, Ok((0, 500_000))); + } + + #[test] + fn test_parse_relay_instructions_gas_and_dropoff() { + let recipient = [0xCD; 32]; + let data = RelayInstructionsBuilder::new() + .with_gas(100_000, 200_000) + .with_gas_drop_off(300_000, &recipient) + .build(); + let result = parse_relay_instructions(&data); + // gas_limit = 100_000, msg_value = 200_000 + 300_000 = 500_000 + assert_eq!(result, Ok((100_000, 500_000))); + } + + #[test] + fn test_parse_relay_instructions_multiple_gas() { + let mut data = make_relay_instruction_gas(100_000, 50_000); + data.extend(make_relay_instruction_gas(200_000, 75_000)); + data.extend(make_relay_instruction_gas(50_000, 25_000)); + let result = parse_relay_instructions(&data); + // gas_limit = 100k + 200k + 50k = 350k + // msg_value = 50k + 75k + 25k = 150k + assert_eq!(result, Ok((350_000, 150_000))); + } + + #[test] + fn test_parse_relay_instructions_invalid_type() { + let data = [0xFF, 0x00, 0x00]; // Invalid type 0xFF + let result = parse_relay_instructions(&data); + assert_eq!(result, Err(RelayParseError::UnsupportedType)); + } + + #[test] + fn test_parse_relay_instructions_truncated_gas() { + // Gas instruction needs 33 bytes (1 type + 16 gas_limit + 16 msg_value) + // Provide only 10 bytes after type + let mut data = vec![RELAY_IX_GAS]; + data.extend_from_slice(&[0u8; 10]); + let result = parse_relay_instructions(&data); + assert_eq!(result, Err(RelayParseError::Truncated)); + } + + #[test] + fn test_parse_relay_instructions_truncated_dropoff() { + // DropOff instruction needs 49 bytes (1 type + 16 msg_value + 32 recipient) + // Provide only 20 bytes after type + let mut data = vec![RELAY_IX_GAS_DROP_OFF]; + data.extend_from_slice(&[0u8; 20]); + let result = parse_relay_instructions(&data); + assert_eq!(result, Err(RelayParseError::Truncated)); + } + + #[test] + fn test_parse_relay_instructions_multiple_dropoff() { + let recipient = [0xAB; 32]; + let mut data = make_relay_instruction_gas_drop_off(100_000, &recipient); + data.extend(make_relay_instruction_gas_drop_off(200_000, &recipient)); + let result = parse_relay_instructions(&data); + assert_eq!(result, Err(RelayParseError::MultipleDropoff)); + } + + #[test] + fn test_parse_relay_instructions_overflow_gas_limit() { + let mut data = make_relay_instruction_gas(u128::MAX, 0); + data.extend(make_relay_instruction_gas(1, 0)); // This should overflow + let result = parse_relay_instructions(&data); + assert_eq!(result, Err(RelayParseError::Overflow)); + } + + #[test] + fn test_parse_relay_instructions_overflow_msg_value() { + let mut data = make_relay_instruction_gas(0, u128::MAX); + data.extend(make_relay_instruction_gas(0, 1)); // This should overflow + let result = parse_relay_instructions(&data); + assert_eq!(result, Err(RelayParseError::Overflow)); + } + + // Roundtrip tests + + #[test] + fn test_roundtrip_gas() { + let gas_limit = 1_000_000u128; + let msg_value = 2_000_000_000_000_000_000u128; // 2 ETH in wei + let data = make_relay_instruction_gas(gas_limit, msg_value); + let result = parse_relay_instructions(&data); + assert_eq!(result, Ok((gas_limit, msg_value))); + } + + #[test] + fn test_roundtrip_dropoff() { + let drop_off = 500_000_000_000_000_000u128; // 0.5 ETH in wei + let recipient = [0x42; 32]; + let data = make_relay_instruction_gas_drop_off(drop_off, &recipient); + let result = parse_relay_instructions(&data); + assert_eq!(result, Ok((0, drop_off))); + } + + #[test] + fn test_roundtrip_builder() { + let gas_limit = 300_000u128; + let gas_msg_value = 100_000_000_000_000_000u128; // 0.1 ETH + let drop_off = 250_000_000_000_000_000u128; // 0.25 ETH + let recipient = [0x99; 32]; + + let data = RelayInstructionsBuilder::new() + .with_gas(gas_limit, gas_msg_value) + .with_gas_drop_off(drop_off, &recipient) + .build(); + + let result = parse_relay_instructions(&data); + assert_eq!(result, Ok((gas_limit, gas_msg_value + drop_off))); + } } diff --git a/svm/package.json b/svm/package.json index ff5e99e..f2b6002 100644 --- a/svm/package.json +++ b/svm/package.json @@ -1,17 +1,18 @@ { "license": "Apache-2.0", + "scripts": { + "test": "bun test", + "test:quoters": "bun test ./tests/executor-quoters.test.ts" + }, "dependencies": { "@coral-xyz/anchor": "^0.30.1", - "@types/chai-as-promised": "7.1.8", - "chai-as-promised": "7.1.1" + "@noble/hashes": "^2.0.1", + "@noble/secp256k1": "^3.0.0", + "js-sha3": "^0.9.3" }, "devDependencies": { - "@types/bn.js": "^5.1.0", - "@types/chai": "^4.3.0", - "@types/mocha": "^9.0.0", - "chai": "^4.3.4", - "mocha": "^9.0.3", - "ts-mocha": "^10.0.0", - "typescript": "^4.3.5" + "@types/bun": "^1.3.4", + "@types/node": "^24.10.1", + "typescript": "^5.9.3" } } diff --git a/svm/pinocchio/Cargo.lock b/svm/pinocchio/Cargo.lock new file mode 100644 index 0000000..2816f97 --- /dev/null +++ b/svm/pinocchio/Cargo.lock @@ -0,0 +1,6588 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "opaque-debug", +] + +[[package]] +name = "aes-gcm-siv" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589c637f0e68c877bbd59a4599bbe849cac8e5f3e4b5a3ebae8f528cd218dcdc" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "polyval", + "subtle", + "zeroize", +] + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61" + +[[package]] +name = "aquamarine" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da02abba9f9063d786eab1509833ebb2fac0f966862ca59439c76b9c566760" +dependencies = [ + "include_dir", + "itertools", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-bn254" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a22f4561524cd949590d78d7d4c5df8f592430d221f7f3c9497bbafd8972120f" +dependencies = [ + "ark-ec", + "ark-ff", + "ark-std", +] + +[[package]] +name = "ark-ec" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "defd9a439d56ac24968cca0571f598a61bc8c55f71d50a89cda591cb750670ba" +dependencies = [ + "ark-ff", + "ark-poly", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", + "itertools", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm", + "ark-ff-macros", + "ark-serialize", + "ark-std", + "derivative", + "digest 0.10.7", + "itertools", + "num-bigint 0.4.6", + "num-traits", + "paste", + "rustc_version", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-poly" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d320bfc44ee185d899ccbadfa8bc31aab923ce1558716e1997a1e74057fe86bf" +dependencies = [ + "ark-ff", + "ark-serialize", + "ark-std", + "derivative", + "hashbrown 0.13.2", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-serialize-derive", + "ark-std", + "digest 0.10.7", + "num-bigint 0.4.6", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae3281bc6d0fd7e549af32b52511e1302185bd688fd3359fa36423346ff682ea" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.5", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "ascii" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e" + +[[package]] +name = "asn1-rs" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6fd5ddaf0351dff5b8da21b2fb4ff8e08ddd02857f0bf69c47639106c0fff0" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "726535892e8eae7e70657b4c8ea93d26b8553afb1ce617caee529ef96d7dee6c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure 0.12.6", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2777730b2039ac0f95f093556e61b6d26cebed5393ca6f152717777cec3a42ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-compression" +version = "0.4.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e86f6d3dc9dc4352edeea6b8e499e13e3f5dc3b964d7ca5fd411415a3498473" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "async-mutex" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73112ce9e1059d8604242af62c7ec8e5975ac58ac251686c8403b45e8a6fe778" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitmaps" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031043d04099746d8db04daf1fa424b2bc8bd69d92b25962dcde24da39ab64a2" +dependencies = [ + "typenum", +] + +[[package]] +name = "blake3" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "digest 0.10.7", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "block-padding", + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "borsh" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15bf3650200d8bffa99015595e10f1fbd17de07abbc25bb067da79e769939bfa" +dependencies = [ + "borsh-derive 0.9.3", + "hashbrown 0.11.2", +] + +[[package]] +name = "borsh" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115e54d64eb62cdebad391c19efc9dce4981c690c85a33a12199d99bb9546fee" +dependencies = [ + "borsh-derive 0.10.4", + "hashbrown 0.13.2", +] + +[[package]] +name = "borsh" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1da5ab77c1437701eeff7c88d968729e7766172279eab0676857b3d63af7a6f" +dependencies = [ + "borsh-derive 1.6.0", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6441c552f230375d18e3cc377677914d2ca2b0d36e52129fe15450a2dce46775" +dependencies = [ + "borsh-derive-internal 0.9.3", + "borsh-schema-derive-internal 0.9.3", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831213f80d9423998dd696e2c5345aba6be7a0bd8cd19e31c5243e13df1cef89" +dependencies = [ + "borsh-derive-internal 0.10.4", + "borsh-schema-derive-internal 0.10.4", + "proc-macro-crate 0.1.5", + "proc-macro2", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0686c856aa6aac0c4498f936d7d6a02df690f614c03e4d906d1018062b5c5e2c" +dependencies = [ + "once_cell", + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5449c28a7b352f2d1e592a8a28bf139bc71afb0764a14f3c02500935d8c44065" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65d6ba50644c98714aa2a70d13d7df3cd75cd2b523a2b452bf010443800976b3" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdbd5696d8bfa21d53d9fe39a714a18538bad11492a42d066dbbc395fb1951c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "borsh-schema-derive-internal" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276691d96f063427be83e6692b86148e488ebba9f48f77788724ca027ba3b6d4" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "brotli" +version = "8.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bd8b9603c7aa97359dbd97ecf258968c95f3adddd6db2f7e7a5bef101c84560" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "874bb8112abecc98cbd6d81ea4fa7e94fb9449648c93cc89aa40c81c24d7de03" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + +[[package]] +name = "bs58" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "771fe0050b883fcc3ea2359b1a96bcfbc090b7116eae7c3c512c7a083fdf23d3" + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bv" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8834bb1d8ee5dc048ee3124f2c7c1afcc6bc9aed03f11e9dfd8c69470a5db340" +dependencies = [ + "feature-probe", + "serde", +] + +[[package]] +name = "bytemuck" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbdf580320f38b612e485521afda1ee26d10cc9884efaaa750d383e13e3c5f4" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" + +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "caps" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd1ddba47aba30b6a889298ad0109c3b8dcb0e8fc993b459daa7067d46f865e0" +dependencies = [ + "libc", +] + +[[package]] +name = "cc" +version = "1.2.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c481bdbf0ed3b892f6f806287d72acd515b352a4ec27a208489b8c1bc839633a" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "chrono-humanize" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799627e6b4d27827a814e837b9d8a504832086081806d45b1afa34dc982b023b" +dependencies = [ + "chrono", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags 1.3.2", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width 0.1.14", + "vec_map", +] + +[[package]] +name = "clap" +version = "3.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ea181bf566f71cb9a5d17a59e1871af638180a18fb0035c92ae62b705207123" +dependencies = [ + "atty", + "bitflags 1.3.2", + "clap_lex", + "indexmap 1.9.3", + "once_cell", + "strsim 0.10.0", + "termcolor", + "textwrap 0.16.2", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "combine" +version = "3.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3da6baa321ec19e1cc41d31bf599f00c783d0517095cdaf0332e3fe8d20680" +dependencies = [ + "ascii", + "byteorder", + "either", + "memchr", + "unreachable", +] + +[[package]] +name = "compression-codecs" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "302266479cb963552d11bd042013a58ef1adc56768016c8b82b4199488f2d4ad" +dependencies = [ + "brotli", + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width 0.2.2", + "windows-sys 0.59.0", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "console_log" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89f72f65e8501878b8a004d5a1afb780987e2ce2b4532c562e367a72c57499f" +dependencies = [ + "log", + "web-sys", +] + +[[package]] +name = "const-oid" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c78c047431fee22c1a7bb92e00ad095a02a983affe4d8a72e2a2c62c1b94f3" + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f9d052967f590a76e62eb387bd0bbb1b000182c3cefe5364db6b7211651bc0" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "serde", + "subtle", + "zeroize", +] + +[[package]] +name = "darling" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.11.1", + "syn 2.0.111", +] + +[[package]] +name = "darling_macro" +version = "0.20.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" +dependencies = [ + "cfg-if", + "hashbrown 0.14.5", + "lock_api", + "once_cell", + "parking_lot_core", + "rayon", +] + +[[package]] +name = "data-encoding" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2330da5de22e8a3cb63252ce2abb30116bf5265e89c0e01bc17015ce30a476" + +[[package]] +name = "der" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6919815d73839e7ad218de758883aae3a257ba6759ce7a9992501efbb53d705c" +dependencies = [ + "const-oid", +] + +[[package]] +name = "der-parser" +version = "8.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbd676fbbab537128ef0278adb5576cf363cff6aa22a7b24effe97347cfab61e" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint 0.4.6", + "num-traits", + "rusticata-macros", +] + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derivation-path" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e5c37193a1db1d8ed868c03ec7b152175f26160a5b740e5e484143877e0adf0" + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dialoguer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c6f2989294b9a498d3ad5491a79c6deb604617378e1cdc4bfc1c1361fe2f87" +dependencies = [ + "console", + "shell-words", + "tempfile", + "zeroize", +] + +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "dir-diff" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7ad16bf5f84253b50d6557681c58c3ab67c47c77d39fed9aeb56e947290bd10" +dependencies = [ + "walkdir", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "dlopen2" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b4f5f101177ff01b8ec4ecc81eead416a8aa42819a2869311b3420fa114ffa" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cbae11b3de8fce2a456e8ea3dada226b35fe791f0dc1d360c0941f0bb681f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "eager" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe71d579d1812060163dff96056261deb5bf6729b100fa2e36a68b9649ba3d3" + +[[package]] +name = "ed25519" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91cff35c70bba8a626e3185d8cd48cc11b5437e1a5bcd15b9b5fa3c64b6dfee7" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek", + "ed25519", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-dalek-bip32" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d2be62a4061b872c8c0873ee4fc6f101ce7b889d039f019c5fa2af471a59908" +dependencies = [ + "derivation-path", + "ed25519-dalek", + "hmac 0.12.1", + "sha2 0.10.9", +] + +[[package]] +name = "educe" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0042ff8246a363dbe77d2ceedb073339e85a804b9a47636c6e016a9a32c05f" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-iterator" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd242f399be1da0a5354aa462d57b4ab2b4ee0683cc552f7c007d2d12d36e94" +dependencies = [ + "enum-iterator-derive", +] + +[[package]] +name = "enum-iterator-derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685adfa4d6f3d765a26bc5dbc936577de9abf756c1feeb3089b01dd395034842" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "enum-ordinalize" +version = "3.1.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bf1fa3f06bbff1ea5b1a9c7b14aa992a39657db60a2759457328d7e058f49ee" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "env_logger" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a12e6657c4c97ebab115a42dcee77225f7f482cdd841cf7088c657a42e9e00e7" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "executor-quoter" +version = "0.1.0" +dependencies = [ + "bs58 0.5.1", + "bytemuck", + "executor-requests", + "pinocchio", + "pinocchio-system", +] + +[[package]] +name = "executor-quoter-router" +version = "0.1.0" +dependencies = [ + "bytemuck", + "pinocchio", + "pinocchio-system", +] + +[[package]] +name = "executor-quoter-router-tests" +version = "0.1.0" +dependencies = [ + "executor-requests", + "libsecp256k1 0.7.2", + "mollusk-svm", + "mollusk-svm-bencher", + "rand 0.8.5", + "solana-program-test", + "solana-sdk", +] + +[[package]] +name = "executor-quoter-tests" +version = "0.1.0" +dependencies = [ + "executor-requests", + "mollusk-svm", + "mollusk-svm-bencher", + "solana-program-test", + "solana-sdk", +] + +[[package]] +name = "executor-requests" +version = "0.0.1" + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "feature-probe" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835a3dc7d1ec9e75e2b5fb4ba75396837112d2060b03f7d43bc1897c7f7211da" + +[[package]] +name = "filetime" +version = "0.2.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0505cd1b6fa6580283f6bdf70a73fcf4aba1184038c90902b92b3dd0df63ed" +dependencies = [ + "cfg-if", + "libc", + "libredox", + "windows-sys 0.60.2", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" + +[[package]] +name = "five8_const" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26dec3da8bc3ef08f2c04f61eab298c3ab334523e55f076354d6d6f613799a7b" +dependencies = [ + "five8_core", +] + +[[package]] +name = "five8_core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2551bf44bc5f776c15044b9b94153a00198be06743e262afaaa61f11ac7523a5" + +[[package]] +name = "flate2" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfe33edd8e85a12a67454e37f8c75e730830d83e313556ab9ebf9ee7fbeb3bfb" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "serde", + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1ebd34e35c46e00bb73e81363248d627782724609fe1b6396f553f68fe3862e" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "goblin" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7666983ed0dd8d21a6f6576ee00053ca0926fb281a5522577a4dbd0f1b54143" +dependencies = [ + "log", + "plain", + "scroll", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap 2.12.1", + "slab", + "tokio", + "tokio-util 0.7.17", + "tracing", +] + +[[package]] +name = "hash32" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67" +dependencies = [ + "byteorder", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.12", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "histogram" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cb882ccb290b8646e554b157ab0b71e64e8d5bef775cd66b6531e52d302669" + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "humantime" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" + +[[package]] +name = "icu_properties" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899" + +[[package]] +name = "icu_provider" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "im" +version = "15.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0acd33ff0285af998aaf9b57342af478078f53492322fafc47450e09397e0e9" +dependencies = [ + "bitmaps", + "rand_core 0.6.4", + "rand_xoshiro", + "rayon", + "serde", + "sized-chunks", + "typenum", + "version_check", +] + +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "index_list" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30141a73bc8a129ac1ce472e33f45af3e2091d86b3479061b9c2f92fdbe9a28c" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown 0.16.1", +] + +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width 0.2.2", + "web-time", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonrpc-core" +version = "18.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb" +dependencies = [ + "futures", + "futures-executor", + "futures-util", + "log", + "serde", + "serde_derive", + "serde_json", +] + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "libredox" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f7e718bdb06000964960ffa43b4335ad4012ae8b99060261aa4a8088d5ccb" +dependencies = [ + "bitflags 2.10.0", + "libc", + "redox_syscall", +] + +[[package]] +name = "libsecp256k1" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9d220bc1feda2ac231cb78c3d26f27676b8cf82c96971f7aeef3d0cf2797c73" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core 0.2.2", + "libsecp256k1-gen-ecmult 0.2.1", + "libsecp256k1-gen-genmult 0.2.1", + "rand 0.7.3", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79019718125edc905a079a70cfa5f3820bc76139fc91d6f9abc27ea2a887139" +dependencies = [ + "arrayref", + "base64 0.22.1", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core 0.3.0", + "libsecp256k1-gen-ecmult 0.3.0", + "libsecp256k1-gen-genmult 0.3.0", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f6ab710cec28cef759c5f18671a27dae2a5f952cdaaee1d8e2908cb2478a80" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccab96b584d38fac86a83f07e659f0deafd0253dc096dab5a36d53efe653c5c3" +dependencies = [ + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core 0.3.0", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67abfe149395e3aa1c48a2beb32b068e2334402df8181f818d3aee2b304c4f5d" +dependencies = [ + "libsecp256k1-core 0.2.2", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core 0.3.0", +] + +[[package]] +name = "light-poseidon" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c9a85a9752c549ceb7578064b4ed891179d20acd85f27318573b64d2d7ee7ee" +dependencies = [ + "ark-bn254", + "ark-ff", + "num-bigint 0.4.6", + "thiserror", +] + +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "litemap" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "lru" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999beba7b6e8345721bd280141ed958096a2e4abdf74f67ff4ce49b4b54e47a" +dependencies = [ + "hashbrown 0.12.3", +] + +[[package]] +name = "lz4" +version = "1.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4" +dependencies = [ + "lz4-sys", +] + +[[package]] +name = "lz4-sys" +version = "1.11.1+lz4-1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "memmap2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83faa42c0a078c393f6b29d5db232d8be22776a891f8f56e5284faee4a20b327" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5de893c32cde5f383baa4c04c5d6dbdd735cfd4a794b0debdb2bb1b421da5ff4" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.61.2", +] + +[[package]] +name = "mockall" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c84490118f2ee2d74570d114f3d0493cbf02790df303d2707606c3e14e07c96" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ce75669015c4f47b289fd4d4f56e894e4c96003ffdf3ac51313126f94c6cbb" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "modular-bitfield" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +dependencies = [ + "modular-bitfield-impl", + "static_assertions", +] + +[[package]] +name = "modular-bitfield-impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mollusk-svm" +version = "0.0.9-solana-1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c29b97b1de2f2f1e1868b062afec7bf63b29042c5f4d2b14dadd5a653e245e7e" +dependencies = [ + "bincode", + "solana-bpf-loader-program", + "solana-logger", + "solana-program-runtime", + "solana-sdk", + "solana-system-program", + "thiserror", +] + +[[package]] +name = "mollusk-svm-bencher" +version = "0.0.9-solana-1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "660f49d6d707060b3e1e07763f1359a0b6fdf6d0b3eb5d7045d13da3961da354" +dependencies = [ + "chrono", + "mollusk-svm", + "num-format", + "serde_json", + "solana-sdk", +] + +[[package]] +name = "nix" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", + "memoffset 0.7.1", + "pin-utils", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + +[[package]] +name = "num" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8536030f9fea7127f841b45bb6243b27255787fb4eb83958aa1ef9d2fdc0c36" +dependencies = [ + "num-bigint 0.2.6", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "090c7f9998ee0ff65aa5b723e4009f7b217707f1fb5ea551329cc4d6231fb304" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-complex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b19411a9719e753aff12e5187b74d60d3dc449ec3f4dc21e3989c3f554bc95" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec", + "itoa", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c000134b5dbf44adc5cb772486d335293351644b801551abe8f75c84cfa4aef" +dependencies = [ + "autocfg", + "num-bigint 0.2.6", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi 0.5.2", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a015b430d3c108a207fd776d2e2196aaf8b1cf8cf93253e3a097ff3085076a1" +dependencies = [ + "num_enum_derive 0.6.1", +] + +[[package]] +name = "num_enum" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1207a7e20ad57b847bbddc6776b968420d38292bbfe2089accff5e19e82454c" +dependencies = [ + "num_enum_derive 0.7.5", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96667db765a921f7b295ffee8b60472b686a51d4f21c2ee4ffdb94c7013b65a6" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff32365de1b6743cb203b710788263c44a03de03802daf96092f2da4fe6ba4d7" +dependencies = [ + "proc-macro-crate 3.4.0", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "oid-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bedf36ffb6ba96c2eb7144ef6270557b52e54b20c0a8e1eb2ff99a6c6959bff" +dependencies = [ + "asn1-rs", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "opentelemetry" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6105e89802af13fdf48c49d7646d3b533a70e536d818aae7e78ba0433d01acb8" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "js-sys", + "lazy_static", + "percent-encoding", + "pin-project", + "rand 0.8.5", + "thiserror", +] + +[[package]] +name = "os_str_bytes" +version = "6.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1" + +[[package]] +name = "ouroboros" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1358bd1558bd2a083fed428ffeda486fbfb323e698cdda7794259d592ca72db" +dependencies = [ + "aliasable", + "ouroboros_macro", +] + +[[package]] +name = "ouroboros_macro" +version = "0.15.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7d21ccd03305a674437ee1248f3ab5d4b1db095cf1caf49f1713ddf61956b7" +dependencies = [ + "Inflector", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pbkdf2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "216eaa586a190f0a738f2f918511eecfa90f13295abec0e457cdebcceda80cbd" +dependencies = [ + "crypto-mac", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "percentage" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fd23b938276f14057220b707937bcb42fa76dda7560e57a2da30cb52d557937" +dependencies = [ + "num", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pinocchio" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b971851087bc3699b001954ad02389d50c41405ece3548cbcafc88b3e20017a" + +[[package]] +name = "pinocchio-pubkey" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb0225638cadcbebae8932cb7f49cb5da7c15c21beb19f048f05a5ca7d93f065" +dependencies = [ + "five8_const", + "pinocchio", + "sha2-const-stable", +] + +[[package]] +name = "pinocchio-system" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4be2a6dad40b5e75d1486f021619c4bd504c34c1362c9b94ed7fa525b1cc63cc" +dependencies = [ + "pinocchio", + "pinocchio-pubkey", +] + +[[package]] +name = "pkcs8" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cabda3fb821068a9a4fab19a683eac3af12edf0f34b94a8be53c4972b8149d0" +dependencies = [ + "der", + "spki", + "zeroize", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + +[[package]] +name = "potential_utf" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "2.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59230a63c37f3e18569bdb90e4a89cbf5bf8b06fea0b84e65ea10cc4df47addd" +dependencies = [ + "difflib", + "float-cmp", + "itertools", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "proc-macro-crate" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +dependencies = [ + "toml", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit 0.19.15", +] + +[[package]] +name = "proc-macro-crate" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983" +dependencies = [ + "toml_edit 0.23.7", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.103" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "qstring" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d464fae65fff2680baf48019211ce37aaec0c78e9264c84a3e484717f965104e" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "qualifier_attr" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e2e25ee72f5b24d773cae88422baddefff7714f97aab68d96fe2b6fc4a28fb2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "quinn" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cc2c5017e4b43d5995dcea317bc46c1e09404c0a9664d2908f7f02dfe943d75" +dependencies = [ + "bytes", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "thiserror", + "tokio", + "tracing", +] + +[[package]] +name = "quinn-proto" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "141bf7dfde2fbc246bfd3fe12f2455aa24b0fbd9af535d8c86c7bd1381ff2b1a" +dependencies = [ + "bytes", + "rand 0.8.5", + "ring 0.16.20", + "rustc-hash", + "rustls", + "rustls-native-certs", + "slab", + "thiserror", + "tinyvec", + "tracing", +] + +[[package]] +name = "quinn-udp" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "055b4e778e8feb9f93c4e439f71dc2156ef13360b432b799e179a8c4cdf0b1d7" +dependencies = [ + "bytes", + "libc", + "socket2 0.5.10", + "tracing", + "windows-sys 0.48.0", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xoshiro" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f97cdb2a36ed4183de61b2f824cc45c9f1037f28afe0a322e9fff4c108b5aaa" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "rayon" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rcgen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffbe84efe2f38dea12e9bfc1f65377fdf03e53a18cb3b995faedf7934c7e785b" +dependencies = [ + "pem", + "ring 0.16.20", + "time", + "yasna", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.10.0", +] + +[[package]] +name = "regex" +version = "1.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "async-compression", + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-rustls", + "tokio-util 0.7.17", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots 0.25.4", + "winreg", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted 0.7.1", + "web-sys", + "winapi", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted 0.9.0", + "windows-sys 0.52.0", +] + +[[package]] +name = "rpassword" +version = "7.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d4c8b64f049c6721ec8ccec37ddfc3d641c4a7fca57e8f2a89de509c73df39" +dependencies = [ + "libc", + "rtoolbox", + "windows-sys 0.59.0", +] + +[[package]] +name = "rtoolbox" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7cc970b249fbe527d6e02e0a227762c9108b2f49d81094fe357ffc6d14d7f6f" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + +[[package]] +name = "rustix" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" +dependencies = [ + "bitflags 2.10.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring 0.17.14", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "scroll" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04c565b551bafbef4157586fa379538366e4385d42082f255bfd96e4fe8519da" +dependencies = [ + "scroll_derive", +] + +[[package]] +name = "scroll_derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db149f81d46d2deba7cd3c50772474707729550221e69588478ebf9ada425ae" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring 0.17.14", + "untrusted 0.9.0", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.10.0", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + +[[package]] +name = "seqlock" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5c67b6f14ecc5b86c66fa63d76b5092352678545a8a3cdae80aef5128371910" +dependencies = [ + "parking_lot", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "serde_json" +version = "1.0.145" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07ff71d2c147a7b57362cead5e22f772cd52f6ab31cfcd9edcd7f6aeb2a0afbe" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "2.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "881b6f881b17d13214e5d494c939ebab463d01264ce1811e9d4ac3a882e7695f" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha2-const-stable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f179d4e11094a893b82fff208f74d448a7512f99f5a0acbd5c679b705f83ed9" + +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shell-words" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7664a098b8e616bdfcc2dc0e9ac44eb231eedf41db4e9fe95d8d32ec728dedad" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" + +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + +[[package]] +name = "siphasher" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" + +[[package]] +name = "sized-chunks" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d69225bde7a69b235da73377861095455d298f2b970996eec25ddbb42b3d1e" +dependencies = [ + "bitmaps", + "typenum", +] + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "solana-account-decoder" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b109fd3a106e079005167e5b0e6f6d2c88bbedec32530837b584791a8b5abf36" +dependencies = [ + "Inflector", + "base64 0.21.7", + "bincode", + "bs58 0.4.0", + "bv", + "lazy_static", + "serde", + "serde_derive", + "serde_json", + "solana-config-program", + "solana-sdk", + "spl-token", + "spl-token-2022", + "spl-token-group-interface", + "spl-token-metadata-interface", + "thiserror", + "zstd", +] + +[[package]] +name = "solana-accounts-db" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9829d10d521f3ed5e50c12d2b62784e2901aa484a92c2aa3924151da046139" +dependencies = [ + "arrayref", + "bincode", + "blake3", + "bv", + "bytemuck", + "byteorder", + "bzip2", + "crossbeam-channel", + "dashmap", + "flate2", + "fnv", + "im", + "index_list", + "itertools", + "lazy_static", + "log", + "lz4", + "memmap2", + "modular-bitfield", + "num-derive 0.4.2", + "num-traits", + "num_cpus", + "num_enum 0.7.5", + "ouroboros", + "percentage", + "qualifier_attr", + "rand 0.8.5", + "rayon", + "regex", + "rustc_version", + "seqlock", + "serde", + "serde_derive", + "smallvec", + "solana-bucket-map", + "solana-config-program", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-measure", + "solana-metrics", + "solana-nohash-hasher", + "solana-program-runtime", + "solana-rayon-threadlimit", + "solana-sdk", + "solana-stake-program", + "solana-system-program", + "solana-vote-program", + "static_assertions", + "strum", + "strum_macros", + "tar", + "tempfile", + "thiserror", +] + +[[package]] +name = "solana-address-lookup-table-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3527a26138b5deb126f13c27743f3d95ac533abee5979e4113f6d59ef919cc6" +dependencies = [ + "bincode", + "bytemuck", + "log", + "num-derive 0.4.2", + "num-traits", + "rustc_version", + "serde", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-program", + "solana-program-runtime", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-banks-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e58fa66e1e240097665e7f87b267aa8e976ea3fcbd86918c8fd218c875395ada" +dependencies = [ + "borsh 1.6.0", + "futures", + "solana-banks-interface", + "solana-program", + "solana-sdk", + "tarpc", + "thiserror", + "tokio", + "tokio-serde", +] + +[[package]] +name = "solana-banks-interface" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f54d0a4334c153eadaa0326296a47a92d110c1cc975075fd6e1a7b67067f9812" +dependencies = [ + "serde", + "solana-sdk", + "tarpc", +] + +[[package]] +name = "solana-banks-server" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cbe287a0f859362de9b155fabd44e479eba26d5d80e07a7d021297b7b06ecba" +dependencies = [ + "bincode", + "crossbeam-channel", + "futures", + "solana-accounts-db", + "solana-banks-interface", + "solana-client", + "solana-runtime", + "solana-sdk", + "solana-send-transaction-service", + "tarpc", + "tokio", + "tokio-serde", +] + +[[package]] +name = "solana-bpf-loader-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8cc27ceda9a22804d73902f5d718ff1331aa53990c2665c90535f6b182db259" +dependencies = [ + "bincode", + "byteorder", + "libsecp256k1 0.6.0", + "log", + "scopeguard", + "solana-measure", + "solana-program-runtime", + "solana-sdk", + "solana-zk-token-sdk", + "solana_rbpf", + "thiserror", +] + +[[package]] +name = "solana-bucket-map" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca55ec9b8d01d2e3bba9fad77b27c9a8fd51fe12475549b93a853d921b653139" +dependencies = [ + "bv", + "bytemuck", + "log", + "memmap2", + "modular-bitfield", + "num_enum 0.7.5", + "rand 0.8.5", + "solana-measure", + "solana-sdk", + "tempfile", +] + +[[package]] +name = "solana-clap-utils" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "074ef478856a45d5627270fbc6b331f91de9aae7128242d9e423931013fb8a2a" +dependencies = [ + "chrono", + "clap 2.34.0", + "rpassword", + "solana-remote-wallet", + "solana-sdk", + "thiserror", + "tiny-bip39", + "uriparse", + "url", +] + +[[package]] +name = "solana-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24a9f32c42402c4b9484d5868ac74b7e0a746e3905d8bfd756e1203e50cbb87e" +dependencies = [ + "async-trait", + "bincode", + "dashmap", + "futures", + "futures-util", + "indexmap 2.12.1", + "indicatif", + "log", + "quinn", + "rayon", + "solana-connection-cache", + "solana-measure", + "solana-metrics", + "solana-pubsub-client", + "solana-quic-client", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-rpc-client-nonce-utils", + "solana-sdk", + "solana-streamer", + "solana-thin-client", + "solana-tpu-client", + "solana-udp-client", + "thiserror", + "tokio", +] + +[[package]] +name = "solana-compute-budget-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6af050a6e0b402e322aa21f5441c7e27cdd52624a2d659f455b68afd7cda218c" +dependencies = [ + "solana-program-runtime", + "solana-sdk", +] + +[[package]] +name = "solana-config-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d75b803860c0098e021a26f0624129007c15badd5b0bc2fbd9f0e1a73060d3b" +dependencies = [ + "bincode", + "chrono", + "serde", + "serde_derive", + "solana-program-runtime", + "solana-sdk", +] + +[[package]] +name = "solana-connection-cache" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9306ede13e8ceeab8a096bcf5fa7126731e44c201ca1721ea3c38d89bcd4111" +dependencies = [ + "async-trait", + "bincode", + "crossbeam-channel", + "futures-util", + "indexmap 2.12.1", + "log", + "rand 0.8.5", + "rayon", + "rcgen", + "solana-measure", + "solana-metrics", + "solana-sdk", + "thiserror", + "tokio", +] + +[[package]] +name = "solana-cost-model" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c852790063f7646a1c5199234cc82e1304b55a3b3fb8055a0b5c8b0393565c1c" +dependencies = [ + "lazy_static", + "log", + "rustc_version", + "solana-address-lookup-table-program", + "solana-bpf-loader-program", + "solana-compute-budget-program", + "solana-config-program", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-loader-v4-program", + "solana-metrics", + "solana-program-runtime", + "solana-sdk", + "solana-stake-program", + "solana-system-program", + "solana-vote-program", +] + +[[package]] +name = "solana-frozen-abi" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ab2c30c15311b511c0d1151e4ab6bc9a3e080a37e7c6e7c2d96f5784cf9434" +dependencies = [ + "block-buffer 0.10.4", + "bs58 0.4.0", + "bv", + "either", + "generic-array", + "im", + "lazy_static", + "log", + "memmap2", + "rustc_version", + "serde", + "serde_bytes", + "serde_derive", + "sha2 0.10.9", + "solana-frozen-abi-macro", + "subtle", + "thiserror", +] + +[[package]] +name = "solana-frozen-abi-macro" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c142f779c3633ac83c84d04ff06c70e1f558c876f13358bed77ba629c7417932" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.111", +] + +[[package]] +name = "solana-loader-v4-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b58f70f5883b0f26a6011ed23f76c493a3f22df63aec46cfe8e1b9bf82b5cc" +dependencies = [ + "log", + "solana-measure", + "solana-program-runtime", + "solana-sdk", + "solana_rbpf", +] + +[[package]] +name = "solana-logger" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121d36ffb3c6b958763312cbc697fbccba46ee837d3a0aa4fc0e90fcb3b884f3" +dependencies = [ + "env_logger", + "lazy_static", + "log", +] + +[[package]] +name = "solana-measure" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c01a7f9cdc9d9d37a3d5651b2fe7ec9d433c2a3470b9f35897e373b421f0737" +dependencies = [ + "log", + "solana-sdk", +] + +[[package]] +name = "solana-metrics" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e36052aff6be1536bdf6f737c6e69aca9dbb6a2f3f582e14ecb0ddc0cd66ce" +dependencies = [ + "crossbeam-channel", + "gethostname", + "lazy_static", + "log", + "reqwest", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-net-utils" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a1f5c6be9c5b272866673741e1ebc64b2ea2118e5c6301babbce526fdfb15f4" +dependencies = [ + "bincode", + "clap 3.2.25", + "crossbeam-channel", + "log", + "nix", + "rand 0.8.5", + "serde", + "serde_derive", + "socket2 0.5.10", + "solana-logger", + "solana-sdk", + "solana-version", + "tokio", + "url", +] + +[[package]] +name = "solana-nohash-hasher" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b8a731ed60e89177c8a7ab05fe0f1511cedd3e70e773f288f9de33a9cfdc21e" + +[[package]] +name = "solana-perf" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28acaf22477566a0fbddd67249ea5d859b39bacdb624aff3fadd3c5745e2643c" +dependencies = [ + "ahash 0.8.12", + "bincode", + "bv", + "caps", + "curve25519-dalek", + "dlopen2", + "fnv", + "lazy_static", + "libc", + "log", + "nix", + "rand 0.8.5", + "rayon", + "rustc_version", + "serde", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-metrics", + "solana-rayon-threadlimit", + "solana-sdk", + "solana-vote-program", +] + +[[package]] +name = "solana-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c10f4588cefd716b24a1a40dd32c278e43a560ab8ce4de6b5805c9d113afdfa1" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff", + "ark-serialize", + "base64 0.21.7", + "bincode", + "bitflags 2.10.0", + "blake3", + "borsh 0.10.4", + "borsh 0.9.3", + "borsh 1.6.0", + "bs58 0.4.0", + "bv", + "bytemuck", + "cc", + "console_error_panic_hook", + "console_log", + "curve25519-dalek", + "getrandom 0.2.16", + "itertools", + "js-sys", + "lazy_static", + "libc", + "libsecp256k1 0.6.0", + "light-poseidon", + "log", + "memoffset 0.9.1", + "num-bigint 0.4.6", + "num-derive 0.4.2", + "num-traits", + "parking_lot", + "rand 0.8.5", + "rustc_version", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "sha2 0.10.9", + "sha3 0.10.8", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk-macro", + "thiserror", + "tiny-bip39", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "solana-program-runtime" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbf0c3eab2a80f514289af1f422c121defb030937643c43b117959d6f1932fb5" +dependencies = [ + "base64 0.21.7", + "bincode", + "eager", + "enum-iterator", + "itertools", + "libc", + "log", + "num-derive 0.4.2", + "num-traits", + "percentage", + "rand 0.8.5", + "rustc_version", + "serde", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-measure", + "solana-metrics", + "solana-sdk", + "solana_rbpf", + "thiserror", +] + +[[package]] +name = "solana-program-test" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1382a5768ff738e283770ee331d0a4fa04aa1aceed8eb820a97094c93d53b72" +dependencies = [ + "assert_matches", + "async-trait", + "base64 0.21.7", + "bincode", + "chrono-humanize", + "crossbeam-channel", + "log", + "serde", + "solana-accounts-db", + "solana-banks-client", + "solana-banks-interface", + "solana-banks-server", + "solana-bpf-loader-program", + "solana-logger", + "solana-program-runtime", + "solana-runtime", + "solana-sdk", + "solana-vote-program", + "solana_rbpf", + "test-case", + "thiserror", + "tokio", +] + +[[package]] +name = "solana-pubsub-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b064e76909d33821b80fdd826e6757251934a52958220c92639f634bea90366d" +dependencies = [ + "crossbeam-channel", + "futures-util", + "log", + "reqwest", + "semver", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-rpc-client-api", + "solana-sdk", + "thiserror", + "tokio", + "tokio-stream", + "tokio-tungstenite", + "tungstenite", + "url", +] + +[[package]] +name = "solana-quic-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a90e40ee593f6e9ddd722d296df56743514ae804975a76d47e7afed4e3da244" +dependencies = [ + "async-mutex", + "async-trait", + "futures", + "itertools", + "lazy_static", + "log", + "quinn", + "quinn-proto", + "rcgen", + "rustls", + "solana-connection-cache", + "solana-measure", + "solana-metrics", + "solana-net-utils", + "solana-rpc-client-api", + "solana-sdk", + "solana-streamer", + "thiserror", + "tokio", +] + +[[package]] +name = "solana-rayon-threadlimit" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66468f9c014992167de10cc68aad6ac8919a8c8ff428dc88c0d2b4da8c02b8b7" +dependencies = [ + "lazy_static", + "num_cpus", +] + +[[package]] +name = "solana-remote-wallet" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c191019f4d4f84281a6d0dd9a43181146b33019627fc394e42e08ade8976b431" +dependencies = [ + "console", + "dialoguer", + "log", + "num-derive 0.4.2", + "num-traits", + "parking_lot", + "qstring", + "semver", + "solana-sdk", + "thiserror", + "uriparse", +] + +[[package]] +name = "solana-rpc-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36ed4628e338077c195ddbf790693d410123d17dec0a319b5accb4aaee3fb15c" +dependencies = [ + "async-trait", + "base64 0.21.7", + "bincode", + "bs58 0.4.0", + "indicatif", + "log", + "reqwest", + "semver", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-rpc-client-api", + "solana-sdk", + "solana-transaction-status", + "solana-version", + "solana-vote-program", + "tokio", +] + +[[package]] +name = "solana-rpc-client-api" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c913551faa4a1ae4bbfef6af19f3a5cf847285c05b4409e37c8993b3444229" +dependencies = [ + "base64 0.21.7", + "bs58 0.4.0", + "jsonrpc-core", + "reqwest", + "semver", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-sdk", + "solana-transaction-status", + "solana-version", + "spl-token-2022", + "thiserror", +] + +[[package]] +name = "solana-rpc-client-nonce-utils" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a47b6bb1834e6141a799db62bbdcf80d17a7d58d7bc1684c614e01a7293d7cf" +dependencies = [ + "clap 2.34.0", + "solana-clap-utils", + "solana-rpc-client", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-runtime" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73a12e1270121e1ca6a4e86d6d0f5c339f0811a8435161d9eee54cbb0a083859" +dependencies = [ + "aquamarine", + "arrayref", + "base64 0.21.7", + "bincode", + "blake3", + "bv", + "bytemuck", + "byteorder", + "bzip2", + "crossbeam-channel", + "dashmap", + "dir-diff", + "flate2", + "fnv", + "im", + "index_list", + "itertools", + "lazy_static", + "log", + "lru", + "lz4", + "memmap2", + "mockall", + "modular-bitfield", + "num-derive 0.4.2", + "num-traits", + "num_cpus", + "num_enum 0.7.5", + "ouroboros", + "percentage", + "qualifier_attr", + "rand 0.8.5", + "rayon", + "regex", + "rustc_version", + "serde", + "serde_derive", + "serde_json", + "solana-accounts-db", + "solana-address-lookup-table-program", + "solana-bpf-loader-program", + "solana-bucket-map", + "solana-compute-budget-program", + "solana-config-program", + "solana-cost-model", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-loader-v4-program", + "solana-measure", + "solana-metrics", + "solana-perf", + "solana-program-runtime", + "solana-rayon-threadlimit", + "solana-sdk", + "solana-stake-program", + "solana-system-program", + "solana-version", + "solana-vote", + "solana-vote-program", + "solana-zk-token-proof-program", + "solana-zk-token-sdk", + "static_assertions", + "strum", + "strum_macros", + "symlink", + "tar", + "tempfile", + "thiserror", + "zstd", +] + +[[package]] +name = "solana-sdk" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "580ad66c2f7a4c3cb3244fe21440546bd500f5ecb955ad9826e92a78dded8009" +dependencies = [ + "assert_matches", + "base64 0.21.7", + "bincode", + "bitflags 2.10.0", + "borsh 1.6.0", + "bs58 0.4.0", + "bytemuck", + "byteorder", + "chrono", + "derivation-path", + "digest 0.10.7", + "ed25519-dalek", + "ed25519-dalek-bip32", + "generic-array", + "hmac 0.12.1", + "itertools", + "js-sys", + "lazy_static", + "libsecp256k1 0.6.0", + "log", + "memmap2", + "num-derive 0.4.2", + "num-traits", + "num_enum 0.7.5", + "pbkdf2 0.11.0", + "qstring", + "qualifier_attr", + "rand 0.7.3", + "rand 0.8.5", + "rustc_version", + "rustversion", + "serde", + "serde_bytes", + "serde_derive", + "serde_json", + "serde_with", + "sha2 0.10.9", + "sha3 0.10.8", + "siphasher", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-logger", + "solana-program", + "solana-sdk-macro", + "thiserror", + "uriparse", + "wasm-bindgen", +] + +[[package]] +name = "solana-sdk-macro" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b75d0f193a27719257af19144fdaebec0415d1c9e9226ae4bd29b791be5e9bd" +dependencies = [ + "bs58 0.4.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.111", +] + +[[package]] +name = "solana-security-txt" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "156bb61a96c605fa124e052d630dba2f6fb57e08c7d15b757e1e958b3ed7b3fe" +dependencies = [ + "hashbrown 0.15.2", +] + +[[package]] +name = "solana-send-transaction-service" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3218f670f582126a3859c4fd152e922b93b3748a636bb143f970391925723577" +dependencies = [ + "crossbeam-channel", + "log", + "solana-client", + "solana-measure", + "solana-metrics", + "solana-runtime", + "solana-sdk", + "solana-tpu-client", +] + +[[package]] +name = "solana-stake-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeb3e0d2dc7080b9fa61b34699b176911684f5e04e8df4b565b2b6c962bb4321" +dependencies = [ + "bincode", + "log", + "rustc_version", + "solana-config-program", + "solana-program-runtime", + "solana-sdk", + "solana-vote-program", +] + +[[package]] +name = "solana-streamer" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8476e41ad94fe492e8c06697ee35912cf3080aae0c9e9ac6430835256ccf056" +dependencies = [ + "async-channel", + "bytes", + "crossbeam-channel", + "futures-util", + "histogram", + "indexmap 2.12.1", + "itertools", + "libc", + "log", + "nix", + "pem", + "percentage", + "pkcs8", + "quinn", + "quinn-proto", + "rand 0.8.5", + "rcgen", + "rustls", + "smallvec", + "solana-metrics", + "solana-perf", + "solana-sdk", + "thiserror", + "tokio", + "x509-parser", +] + +[[package]] +name = "solana-system-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26f31e04f5baad7cbc2281fea312c4e48277da42a93a0ba050b74edc5a74d63c" +dependencies = [ + "bincode", + "log", + "serde", + "serde_derive", + "solana-program-runtime", + "solana-sdk", +] + +[[package]] +name = "solana-thin-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c02245d0d232430e79dc0d624aa42d50006097c3aec99ac82ac299eaa3a73f" +dependencies = [ + "bincode", + "log", + "rayon", + "solana-connection-cache", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk", +] + +[[package]] +name = "solana-tpu-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67251506ed03de15f1347b46636b45c47da6be75015b4a13f0620b21beb00566" +dependencies = [ + "async-trait", + "bincode", + "futures-util", + "indexmap 2.12.1", + "indicatif", + "log", + "rayon", + "solana-connection-cache", + "solana-measure", + "solana-metrics", + "solana-pubsub-client", + "solana-rpc-client", + "solana-rpc-client-api", + "solana-sdk", + "thiserror", + "tokio", +] + +[[package]] +name = "solana-transaction-status" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d3d36db1b2ab2801afd5482aad9fb15ed7959f774c81a77299fdd0ddcf839d4" +dependencies = [ + "Inflector", + "base64 0.21.7", + "bincode", + "borsh 0.10.4", + "bs58 0.4.0", + "lazy_static", + "log", + "serde", + "serde_derive", + "serde_json", + "solana-account-decoder", + "solana-sdk", + "spl-associated-token-account", + "spl-memo", + "spl-token", + "spl-token-2022", + "thiserror", +] + +[[package]] +name = "solana-udp-client" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a754a3c2265eb02e0c35aeaca96643951f03cee6b376afe12e0cf8860ffccd1" +dependencies = [ + "async-trait", + "solana-connection-cache", + "solana-net-utils", + "solana-sdk", + "solana-streamer", + "thiserror", + "tokio", +] + +[[package]] +name = "solana-version" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f44776bd685cc02e67ba264384acc12ef2931d01d1a9f851cb8cdbd3ce455b9e" +dependencies = [ + "log", + "rustc_version", + "semver", + "serde", + "serde_derive", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk", +] + +[[package]] +name = "solana-vote" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5983370c95b615dc5f5d0e85414c499f05380393c578749bcd14c114c77c9bc" +dependencies = [ + "crossbeam-channel", + "itertools", + "log", + "rustc_version", + "serde", + "serde_derive", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-sdk", + "solana-vote-program", + "thiserror", +] + +[[package]] +name = "solana-vote-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25810970c91feb579bd3f67dca215fce971522e42bfd59696af89c5dfebd997c" +dependencies = [ + "bincode", + "log", + "num-derive 0.4.2", + "num-traits", + "rustc_version", + "serde", + "serde_derive", + "solana-frozen-abi", + "solana-frozen-abi-macro", + "solana-metrics", + "solana-program", + "solana-program-runtime", + "solana-sdk", + "thiserror", +] + +[[package]] +name = "solana-zk-token-proof-program" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1be1c15d4aace575e2de73ebeb9b37bac455e89bee9a8c3531f47ac5066b33e1" +dependencies = [ + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "solana-program-runtime", + "solana-sdk", + "solana-zk-token-sdk", +] + +[[package]] +name = "solana-zk-token-sdk" +version = "1.18.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cbdf4249b6dfcbba7d84e2b53313698043f60f8e22ce48286e6fbe8a17c8d16" +dependencies = [ + "aes-gcm-siv", + "base64 0.21.7", + "bincode", + "bytemuck", + "byteorder", + "curve25519-dalek", + "getrandom 0.1.16", + "itertools", + "lazy_static", + "merlin", + "num-derive 0.4.2", + "num-traits", + "rand 0.7.3", + "serde", + "serde_json", + "sha3 0.9.1", + "solana-program", + "solana-sdk", + "subtle", + "thiserror", + "zeroize", +] + +[[package]] +name = "solana_rbpf" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da5d083187e3b3f453e140f292c09186881da8a02a7b5e27f645ee26de3d9cc5" +dependencies = [ + "byteorder", + "combine", + "goblin", + "hash32", + "libc", + "log", + "rand 0.8.5", + "rustc-demangle", + "scroll", + "thiserror", + "winapi", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d01ac02a6ccf3e07db148d2be087da624fea0221a16152ed01f0496a6b0a27" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "spl-associated-token-account" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "992d9c64c2564cc8f63a4b508bf3ebcdf2254b0429b13cd1d31adb6162432a5f" +dependencies = [ + "assert_matches", + "borsh 0.10.4", + "num-derive 0.4.2", + "num-traits", + "solana-program", + "spl-token", + "spl-token-2022", + "thiserror", +] + +[[package]] +name = "spl-discriminator" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce5d563b58ef1bb2cdbbfe0dfb9ffdc24903b10ae6a4df2d8f425ece375033f" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator-derive", +] + +[[package]] +name = "spl-discriminator-derive" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07fd7858fc4ff8fb0e34090e41d7eb06a823e1057945c26d480bfc21d2338a93" +dependencies = [ + "quote", + "spl-discriminator-syn", + "syn 2.0.111", +] + +[[package]] +name = "spl-discriminator-syn" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fea7be851bd98d10721782ea958097c03a0c2a07d8d4997041d0ece6319a63" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.9", + "syn 2.0.111", + "thiserror", +] + +[[package]] +name = "spl-memo" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f180b03318c3dbab3ef4e1e4d46d5211ae3c780940dd0a28695aba4b59a75a" +dependencies = [ + "solana-program", +] + +[[package]] +name = "spl-pod" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2881dddfca792737c0706fa0175345ab282b1b0879c7d877bad129645737c079" +dependencies = [ + "borsh 0.10.4", + "bytemuck", + "solana-program", + "solana-zk-token-sdk", + "spl-program-error", +] + +[[package]] +name = "spl-program-error" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "249e0318493b6bcf27ae9902600566c689b7dfba9f1bdff5893e92253374e78c" +dependencies = [ + "num-derive 0.4.2", + "num-traits", + "solana-program", + "spl-program-error-derive", + "thiserror", +] + +[[package]] +name = "spl-program-error-derive" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1845dfe71fd68f70382232742e758557afe973ae19e6c06807b2c30f5d5cb474" +dependencies = [ + "proc-macro2", + "quote", + "sha2 0.10.9", + "syn 2.0.111", +] + +[[package]] +name = "spl-tlv-account-resolution" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "615d381f48ddd2bb3c57c7f7fb207591a2a05054639b18a62e785117dd7a8683" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-type-length-value", +] + +[[package]] +name = "spl-token" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08459ba1b8f7c1020b4582c4edf0f5c7511a5e099a7a97570c9698d4f2337060" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.3.3", + "num-traits", + "num_enum 0.6.1", + "solana-program", + "thiserror", +] + +[[package]] +name = "spl-token-2022" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d697fac19fd74ff472dfcc13f0b442dd71403178ce1de7b5d16f83a33561c059" +dependencies = [ + "arrayref", + "bytemuck", + "num-derive 0.4.2", + "num-traits", + "num_enum 0.7.5", + "solana-program", + "solana-security-txt", + "solana-zk-token-sdk", + "spl-memo", + "spl-pod", + "spl-token", + "spl-token-group-interface", + "spl-token-metadata-interface", + "spl-transfer-hook-interface", + "spl-type-length-value", + "thiserror", +] + +[[package]] +name = "spl-token-group-interface" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b889509d49fa74a4a033ca5dae6c2307e9e918122d97e58562f5c4ffa795c75d" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", +] + +[[package]] +name = "spl-token-metadata-interface" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c16ce3ba6979645fb7627aa1e435576172dd63088dc7848cb09aa331fa1fe4f" +dependencies = [ + "borsh 0.10.4", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-type-length-value", +] + +[[package]] +name = "spl-transfer-hook-interface" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aabdb7c471566f6ddcee724beb8618449ea24b399e58d464d6b5bc7db550259" +dependencies = [ + "arrayref", + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", + "spl-tlv-account-resolution", + "spl-type-length-value", +] + +[[package]] +name = "spl-type-length-value" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a468e6f6371f9c69aae760186ea9f1a01c2908351b06a5e0026d21cfc4d7ecac" +dependencies = [ + "bytemuck", + "solana-program", + "spl-discriminator", + "spl-pod", + "spl-program-error", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 1.0.109", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.111" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tar" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d863878d212c87a19c1a610eb53bb01fe12951c0501cf5a0d65f724914a667a" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tarpc" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c38a012bed6fb9681d3bf71ffaa4f88f3b4b9ed3198cda6e4c8462d24d4bb80" +dependencies = [ + "anyhow", + "fnv", + "futures", + "humantime", + "opentelemetry", + "pin-project", + "rand 0.8.5", + "serde", + "static_assertions", + "tarpc-plugins", + "thiserror", + "tokio", + "tokio-serde", + "tokio-util 0.6.10", + "tracing", + "tracing-opentelemetry", +] + +[[package]] +name = "tarpc-plugins" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee42b4e559f17bce0385ebf511a7beb67d5cc33c12c96b7f4e9789919d9c10f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tempfile" +version = "3.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +dependencies = [ + "fastrand", + "getrandom 0.3.4", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "test-case" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2550dd13afcd286853192af8601920d959b14c401fcece38071d53bf0768a8" +dependencies = [ + "test-case-macros", +] + +[[package]] +name = "test-case-core" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adcb7fd841cd518e279be3d5a3eb0636409487998a4aff22f3de87b81e88384f" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "test-case-macros" +version = "3.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "test-case-core", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width 0.1.14", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e7d9e3bb61134e77bde20dd4825b97c010155709965fedf0f49bb138e52a9d" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40868e7c1d2f0b8d73e4a8c7f0ff63af4f6d19be117e90bd73eb1d62cf831c6b" + +[[package]] +name = "time-macros" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30cfb0125f12d9c277f35663a0a33f8c30190f4e4574868a330595412d34ebf3" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-bip39" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffc59cb9dfc85bb312c3a78fd6aa8a8582e310b0fa885d5bb877f6dcc601839d" +dependencies = [ + "anyhow", + "hmac 0.8.1", + "once_cell", + "pbkdf2 0.4.0", + "rand 0.7.3", + "rustc-hash", + "sha2 0.9.9", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "tinystr" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2 0.6.1", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-serde" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "911a61637386b789af998ee23f50aa30d5fd7edcec8d6d3dedae5e5815205466" +dependencies = [ + "bincode", + "bytes", + "educe", + "futures-core", + "futures-sink", + "pin-project", + "serde", + "serde_json", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" +dependencies = [ + "futures-util", + "log", + "rustls", + "tokio", + "tokio-rustls", + "tungstenite", + "webpki-roots 0.25.4", +] + +[[package]] +name = "tokio-util" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "slab", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" + +[[package]] +name = "toml_datetime" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" +dependencies = [ + "indexmap 2.12.1", + "toml_datetime 0.6.11", + "winnow 0.5.40", +] + +[[package]] +name = "toml_edit" +version = "0.23.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d" +dependencies = [ + "indexmap 2.12.1", + "toml_datetime 0.7.3", + "toml_parser", + "winnow 0.7.14", +] + +[[package]] +name = "toml_parser" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e" +dependencies = [ + "winnow 0.7.14", +] + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d15d90a0b5c19378952d479dc858407149d7bb45a14de0142f6c534b16fc647" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "tracing-core" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a04e24fab5c89c6a36eb8558c9656f30d81de51dfa4d3b45f26b21d61fa0a6c" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.17.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbbe89715c1dbbb790059e2565353978564924ee85017b5fff365c872ff6721f" +dependencies = [ + "once_cell", + "opentelemetry", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "tungstenite" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http", + "httparse", + "log", + "rand 0.8.5", + "rustls", + "sha1", + "thiserror", + "url", + "utf-8", + "webpki-roots 0.24.0", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "unicode-normalization" +version = "0.1.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "unreachable" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382810877fe448991dfc7f0dd6e3ae5d58088fd0ea5e35189655f84e6814fa56" +dependencies = [ + "void", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "uriparse" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0200d0fc04d809396c2ad43f3c95da3582a2556eba8d453c1087f4120ee352ff" +dependencies = [ + "fnv", + "lazy_static", +] + +[[package]] +name = "url" +version = "2.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "void" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.111", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b291546d5d9d1eab74f069c77749f2cb8504a12caa20f0f2de93ddbf6f411888" +dependencies = [ + "rustls-webpki", +] + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.5.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "writeable" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" + +[[package]] +name = "x509-parser" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0ecbeb7b67ce215e40e3cc7f2ff902f94a223acf44995934763467e7b1febc8" +dependencies = [ + "asn1-rs", + "base64 0.13.1", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "rusticata-macros", + "thiserror", + "time", +] + +[[package]] +name = "xattr" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156" +dependencies = [ + "libc", + "rustix", +] + +[[package]] +name = "yasna" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd" +dependencies = [ + "time", +] + +[[package]] +name = "yoke" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure 0.13.2", +] + +[[package]] +name = "zerocopy" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", + "synstructure 0.13.2", +] + +[[package]] +name = "zeroize" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zerotrie" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/svm/pinocchio/Cargo.toml b/svm/pinocchio/Cargo.toml new file mode 100644 index 0000000..7352b94 --- /dev/null +++ b/svm/pinocchio/Cargo.toml @@ -0,0 +1,12 @@ +[workspace] +members = ["programs/*", "tests/*"] +resolver = "2" + +[profile.release] +overflow-checks = true +lto = "fat" +codegen-units = 1 +[profile.release.build-override] +opt-level = 3 +incremental = false +codegen-units = 1 diff --git a/svm/pinocchio/README.md b/svm/pinocchio/README.md new file mode 100644 index 0000000..ced9c3d --- /dev/null +++ b/svm/pinocchio/README.md @@ -0,0 +1,119 @@ +# Pinocchio Programs + +Solana programs for the executor quoter system. + +## Overview + +- **executor-quoter-router** - Defines the quoter interface specification and routes CPI calls to registered quoter implementations. See [programs/executor-quoter-router/README.md](programs/executor-quoter-router/README.md). +- **executor-quoter** - Example quoter implementation. Integrators can use this as a reference or build their own. See [programs/executor-quoter/README.md](programs/executor-quoter/README.md). + +These programs use the [Pinocchio](https://github.com/febo/pinocchio) framework, but quoter implementations are framework-agnostic. Any program adhering to the CPI interface defined by the router will work. + +## Devnet Deployments + + + +| Program | Address | +| ---------------------- | --------------------------------------------- | +| executor-quoter | `qtrxiqVAfVS61utwZLUi7UKugjCgFaNxBGyskmGingz` | +| executor-quoter-router | `qtrrrV7W3E1jnX1145wXR6ZpthG19ur5xHC1n6PPhDV` | + + + +## Directory Structure + +- `programs/executor-quoter/` - Example quoter implementation +- `programs/executor-quoter-router/` - Router program defining the quoter spec +- `tests/executor-quoter-tests/` - Integration tests and benchmarks for executor-quoter +- `tests/executor-quoter-router-tests/` - Integration tests and benchmarks for executor-quoter-router + +## Prerequisites + +- Solana CLI v1.18.17 or later + +### Testing Prerequisites + +Generate test keypairs before building or running tests: + +```bash +mkdir -p ../test-keys +solana-keygen new --no-bip39-passphrase -o ../test-keys/quoter-updater.json +solana-keygen new --no-bip39-passphrase -o ../test-keys/quoter-payee.json +``` + +## Building + +The Pinocchio programs must be built using `cargo build-sbf` before running tests. + +### Build Programs + +The `executor-quoter` program requires the `QUOTER_UPDATER_PUBKEY` environment variable to be set at build time. This is the public key authorized to update quotes. + +```bash +cd svm/pinocchio + +# Get the updater pubkey from your keypair +export QUOTER_UPDATER_PUBKEY=$(solana-keygen pubkey ../test-keys/quoter-updater.json) + +# Build executor-quoter +cargo build-sbf --manifest-path programs/executor-quoter/Cargo.toml + +# Build executor-quoter-router +cargo build-sbf --manifest-path programs/executor-quoter-router/Cargo.toml +``` + +### Build Anchor Executor (for router tests) + +The router integration tests require the anchor executor program. Build it from the anchor directory: + +```bash +cd ../anchor +cargo build-sbf --manifest-path programs/executor/Cargo.toml + +# Copy to pinocchio deploy directory +cp target/deploy/executor.so ../pinocchio/target/deploy/ +``` + +## Running Tests + +Tests require several environment variables to be set: + +- `QUOTER_UPDATER_PUBKEY` - Public key of the authorized updater +- `QUOTER_UPDATER_KEYPAIR_PATH` - Path to the updater keypair file +- `SBF_OUT_DIR` - Directory containing the compiled `.so` files + +```bash +cd svm/pinocchio + +export QUOTER_UPDATER_PUBKEY=$(solana-keygen pubkey ../test-keys/quoter-updater.json) +export QUOTER_UPDATER_KEYPAIR_PATH=$(pwd)/../test-keys/quoter-updater.json +export SBF_OUT_DIR=$(pwd)/target/deploy + +# Run unit tests (pure Rust math module) +cargo test -p executor-quoter + +# Run integration tests (uses solana-program-test to simulate program execution) +cargo test -p executor-quoter-tests -p executor-quoter-router-tests -- --test-threads=1 +``` + +Note: These tests use native `cargo test`, not `cargo test-sbf`. The unit tests are pure Rust without SBF dependencies. The integration tests use solana-program-test which loads the pre-built `.so` files and simulates program execution natively. + +The `--test-threads=1` flag is required because `solana-program-test` can exhibit race conditions when multiple tests load BPF programs in parallel. Running tests sequentially avoids these issues. + +## Running Benchmarks + +```bash +cd svm/pinocchio + +# Benchmark executor-quoter +cargo bench -p executor-quoter-tests + +# Benchmark executor-quoter-router +cargo bench -p executor-quoter-router-tests +``` + +## Notes + +- The test crates use `solana-program-test` to load and execute the compiled `.so` files in a simulated SVM environment. Benchmarks use [mollusk-svm](https://github.com/buffalojoec/mollusk) for compute unit measurements. +- Tests will fail if the `.so` files are not built first. +- The `QUOTER_UPDATER_PUBKEY` is baked into the program at compile time and cannot be changed without rebuilding. diff --git a/svm/pinocchio/programs/executor-quoter-router/Cargo.toml b/svm/pinocchio/programs/executor-quoter-router/Cargo.toml new file mode 100644 index 0000000..92778a8 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "executor-quoter-router" +version = "0.1.0" +description = "Executor Quoter Router - routes execution requests to registered quoter implementations" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "executor_quoter_router" + +[features] +default = [] +no-entrypoint = [] +custom-heap = [] +custom-panic = [] + +[dependencies] +pinocchio = "0.9.2" +pinocchio-system = "0.4" +bytemuck = { version = "1.14", features = ["derive"] } diff --git a/svm/pinocchio/programs/executor-quoter-router/README.md b/svm/pinocchio/programs/executor-quoter-router/README.md new file mode 100644 index 0000000..601d06f --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/README.md @@ -0,0 +1,47 @@ +# Executor Quoter Router + +Router program that dispatches quote requests and execution requests to registered quoter implementations. + +## Overview + +The router manages quoter registrations and routes CPI calls to the appropriate quoter program. It defines the interface that quoter implementations must adhere to. + +## Instructions + +**UpdateQuoterContract (discriminator: 0)** + +- Registers or updates a quoter's implementation mapping +- Accounts: `[payer, sender, config, quoter_registration, system_program]` + +**QuoteExecution (discriminator: 1)** + +- Gets a quote from a registered quoter via CPI +- Accounts: `[quoter_registration, quoter_program, config, chain_info, quote_body]` +- CPI to quoter's `RequestQuote` instruction (discriminator: `[2, 0, 0, 0, 0, 0, 0, 0]`) + +**RequestExecution (discriminator: 2)** + +- Executes cross-chain request through the router +- Accounts: `[payer, config, quoter_registration, quoter_program, executor_program, payee, refund_addr, system_program, quoter_config, chain_info, quote_body, event_cpi]` +- CPI to quoter's `RequestExecutionQuote` instruction (discriminator: `[3, 0, 0, 0, 0, 0, 0, 0]`) + +## Quoter Interface Requirements + +Quoter implementations must support the following CPI interface: + +### RequestQuote + +- Discriminator: 8 bytes (`[2, 0, 0, 0, 0, 0, 0, 0]`) +- Accounts: `[config, chain_info, quote_body]` +- Returns: `u64` (big endian) payment amount via `set_return_data` + +### RequestExecutionQuote + +- Discriminator: 8 bytes (`[3, 0, 0, 0, 0, 0, 0, 0]`) +- Accounts: `[config, chain_info, quote_body, event_cpi]` +- Returns: 72 bytes via `set_return_data`: + - bytes 0-7: `u64` required payment (big-endian) + - bytes 8-39: 32-byte payee address + - bytes 40-71: 32-byte quote body (EQ01 format) + +See `executor-quoter` for a reference implementation. diff --git a/svm/pinocchio/programs/executor-quoter-router/src/error.rs b/svm/pinocchio/programs/executor-quoter-router/src/error.rs new file mode 100644 index 0000000..237d85a --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/src/error.rs @@ -0,0 +1,44 @@ +use pinocchio::program_error::ProgramError; + +/// Custom errors for the ExecutorQuoterRouter program. +#[repr(u32)] +pub enum ExecutorQuoterRouterError { + /// Invalid account owner + InvalidOwner = 0, + /// Invalid account discriminator + InvalidDiscriminator = 1, + /// Invalid governance message prefix + InvalidGovernancePrefix = 2, + /// Chain ID mismatch + ChainIdMismatch = 3, + /// Invalid sender - msg.sender does not match universal_sender_address + InvalidSender = 4, + /// Governance message has expired + GovernanceExpired = 5, + /// Invalid signature - ecrecover failed or signer mismatch + InvalidSignature = 6, + /// Universal address is not a valid EVM address (upper 12 bytes non-zero) + NotAnEvmAddress = 7, + /// Quoter not registered + QuoterNotRegistered = 8, + /// Underpaid - payment less than required + Underpaid = 9, + /// Refund failed + RefundFailed = 10, + /// Invalid instruction data + InvalidInstructionData = 11, + /// CPI failed + CpiFailed = 12, + /// Invalid return data from quoter + InvalidReturnData = 13, + /// Math overflow + MathOverflow = 14, + /// Invalid account data + InvalidAccountData = 15, +} + +impl From for ProgramError { + fn from(e: ExecutorQuoterRouterError) -> Self { + ProgramError::Custom(e as u32) + } +} diff --git a/svm/pinocchio/programs/executor-quoter-router/src/instructions/executor_cpi.rs b/svm/pinocchio/programs/executor-quoter-router/src/instructions/executor_cpi.rs new file mode 100644 index 0000000..8ebeb26 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/src/instructions/executor_cpi.rs @@ -0,0 +1,72 @@ +//! CPI instruction builder for executor program. +//! +//! The quoter CPI now uses zero-copy: instruction data is passed directly from the router's +//! input without reconstruction. Only the executor CPI requires building instruction data +//! because the signed_quote is constructed on-chain. + +extern crate alloc; +use alloc::vec::Vec; + +/// Anchor discriminator for executor::request_for_execution +/// Generated from: sha256("global:request_for_execution")[0..8] +const EXECUTOR_REQUEST_FOR_EXECUTION_DISCRIMINATOR: [u8; 8] = + [0x6d, 0x6b, 0x57, 0x25, 0x97, 0xc0, 0x77, 0x73]; + +/// Builds instruction data for Anchor executor::request_for_execution CPI. +/// +/// The executor program uses Anchor serialization (Borsh): +/// - 8-byte discriminator +/// - RequestForExecutionArgs struct (Borsh-serialized) +/// +/// RequestForExecutionArgs layout: +/// - amount: u64 (8 bytes, little-endian) +/// - dst_chain: u16 (2 bytes, little-endian) +/// - dst_addr: [u8; 32] (32 bytes) +/// - refund_addr: Pubkey (32 bytes) +/// - signed_quote_bytes: Vec (4-byte length prefix + data) +/// - request_bytes: Vec (4-byte length prefix + data) +/// - relay_instructions: Vec (4-byte length prefix + data) +/// +/// Note: This function still allocates because the signed_quote is constructed on-chain +/// and must be combined with other fields into a contiguous buffer. +pub fn make_executor_request_for_execution_ix( + amount: u64, + dst_chain: u16, + dst_addr: &[u8; 32], + refund_addr: &[u8; 32], + signed_quote_bytes: &[u8], + request_bytes: &[u8], + relay_instructions: &[u8], +) -> Vec { + let mut out = Vec::with_capacity({ + 8 // discriminator + + 8 // amount + + 2 // dst_chain + + 32 // dst_addr + + 32 // refund_addr + + 4 + signed_quote_bytes.len() // signed_quote_bytes Vec + + 4 + request_bytes.len() // request_bytes Vec + + 4 + relay_instructions.len() // relay_instructions Vec + }); + + // Anchor discriminator + out.extend_from_slice(&EXECUTOR_REQUEST_FOR_EXECUTION_DISCRIMINATOR); + + // RequestForExecutionArgs (Borsh serialization - all little-endian) + out.extend_from_slice(&amount.to_le_bytes()); + out.extend_from_slice(&dst_chain.to_le_bytes()); + out.extend_from_slice(dst_addr); + out.extend_from_slice(refund_addr); + + // Vec in Borsh: 4-byte length prefix (u32 le) + data + out.extend_from_slice(&(signed_quote_bytes.len() as u32).to_le_bytes()); + out.extend_from_slice(signed_quote_bytes); + + out.extend_from_slice(&(request_bytes.len() as u32).to_le_bytes()); + out.extend_from_slice(request_bytes); + + out.extend_from_slice(&(relay_instructions.len() as u32).to_le_bytes()); + out.extend_from_slice(relay_instructions); + + out +} diff --git a/svm/pinocchio/programs/executor-quoter-router/src/instructions/mod.rs b/svm/pinocchio/programs/executor-quoter-router/src/instructions/mod.rs new file mode 100644 index 0000000..f1d4cf1 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/src/instructions/mod.rs @@ -0,0 +1,6 @@ +pub mod quote_execution; +pub mod request_execution; +pub mod update_quoter_contract; + +mod executor_cpi; +mod serialization; diff --git a/svm/pinocchio/programs/executor-quoter-router/src/instructions/quote_execution.rs b/svm/pinocchio/programs/executor-quoter-router/src/instructions/quote_execution.rs new file mode 100644 index 0000000..ed7ae60 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/src/instructions/quote_execution.rs @@ -0,0 +1,160 @@ +//! QuoteExecution instruction for the ExecutorQuoterRouter. +//! +//! Gets a quote from a registered quoter via CPI. +//! +//! Input layout (zero-copy optimized): +//! - bytes 0-19: quoter_address (20 bytes, for registration lookup) +//! - bytes 20+: quoter CPI data (passed directly, includes 8-byte discriminator) +//! +//! The client must set bytes 20-27 to the quoter's RequestQuote discriminator +//! (Anchor-compatible: byte 0 = 2, bytes 1-7 = padding zeros). + +use pinocchio::{ + account_info::AccountInfo, cpi::set_return_data, program_error::ProgramError, pubkey::Pubkey, + ProgramResult, +}; + +use crate::{ + error::ExecutorQuoterRouterError, + state::{load_account, QuoterRegistration}, +}; + +/// Offset where quoter CPI data starts (after quoter_address). +const QUOTER_CPI_OFFSET: usize = 20; + +/// Expected discriminator for quoter RequestQuote instruction (8 bytes, Anchor-compatible). +/// Byte 0 = instruction ID (2), bytes 1-7 = padding (zeros). +const EXPECTED_QUOTER_DISCRIMINATOR: [u8; 8] = [2, 0, 0, 0, 0, 0, 0, 0]; + +/// Minimum instruction data size: +/// quoter_address (20) + discriminator (8) + dst_chain (2) + dst_addr (32) + +/// refund_addr (32) + request_bytes_len (4) + relay_instructions_len (4) = 102 +const MIN_DATA_LEN: usize = 102; + +/// QuoteExecution instruction. +/// +/// Accounts: +/// 0. `[]` quoter_registration - QuoterRegistration PDA for the quoter +/// 1. `[]` quoter_program - The quoter implementation program +/// 2-4. `[]` quoter accounts: config, chain_info, quote_body (passed to quoter) +/// +/// Instruction Data Layout (minimum 102 bytes): +/// ```text +/// Offset Size Field +/// ------ ---- ----- +/// 0 20 quoter_address - For registration lookup +/// +/// --- Quoter CPI data (passed directly to quoter) --- +/// 20 8 discriminator - Must be [2, 0, 0, 0, 0, 0, 0, 0] +/// 28 2 dst_chain (u16 LE) - Destination chain ID +/// 30 32 dst_addr - Destination address +/// 62 32 refund_addr - Refund address +/// 94 4 request_bytes_len (u32 LE) +/// 98 var request_bytes +/// var 4 relay_instructions_len (u32 LE) +/// var var relay_instructions +/// ``` + +pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + if data.len() < MIN_DATA_LEN { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + // Parse quoter_address (bytes 0-19) + let quoter_address: [u8; 20] = data[0..20] + .try_into() + .map_err(|_| ExecutorQuoterRouterError::InvalidInstructionData)?; + + // Parse accounts + let [quoter_registration_account, quoter_program, quoter_config, quoter_chain_info, quoter_quote_body] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Load and verify quoter registration + let registration = load_account::(quoter_registration_account, program_id)?; + + if registration.quoter_address != quoter_address { + return Err(ExecutorQuoterRouterError::QuoterNotRegistered.into()); + } + + if quoter_program.key() != ®istration.implementation_program_id { + return Err(ExecutorQuoterRouterError::QuoterNotRegistered.into()); + } + + // Validate CPI data bounds before passing + // CPI data layout: discriminator (8) + dst_chain (2) + dst_addr (32) + refund_addr (32) + + // request_bytes_len (4) + request_bytes + relay_instructions_len (4) + relay_instructions + let cpi_data = &data[QUOTER_CPI_OFFSET..]; + + // Minimum CPI data: discriminator (8) + dst_chain (2) + dst_addr (32) + refund_addr (32) + + // request_bytes_len (4) + relay_instructions_len (4) = 82 bytes + if cpi_data.len() < 82 { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + // Validate discriminator matches expected RequestQuote instruction (8-byte Anchor-compatible) + if cpi_data[0..8] != EXPECTED_QUOTER_DISCRIMINATOR { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + // Validate request_bytes bounds (offset 74 = 8 discriminator + 2 dst_chain + 32 dst_addr + 32 refund_addr) + let request_bytes_len = u32::from_le_bytes( + cpi_data[74..78] + .try_into() + .map_err(|_| ExecutorQuoterRouterError::InvalidInstructionData)?, + ) as usize; + + let relay_len_offset = 78 + request_bytes_len; + if cpi_data.len() < relay_len_offset + 4 { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + // Validate relay_instructions bounds + let relay_instructions_len = u32::from_le_bytes( + cpi_data[relay_len_offset..relay_len_offset + 4] + .try_into() + .map_err(|_| ExecutorQuoterRouterError::InvalidInstructionData)?, + ) as usize; + + let expected_len = relay_len_offset + 4 + relay_instructions_len; + if cpi_data.len() < expected_len { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + // Zero-copy: use the CPI data slice directly (includes discriminator set by client) + let cpi_instruction = pinocchio::instruction::Instruction { + program_id: ®istration.implementation_program_id, + accounts: &[ + pinocchio::instruction::AccountMeta { + pubkey: quoter_config.key(), + is_signer: false, + is_writable: false, + }, + pinocchio::instruction::AccountMeta { + pubkey: quoter_chain_info.key(), + is_signer: false, + is_writable: false, + }, + pinocchio::instruction::AccountMeta { + pubkey: quoter_quote_body.key(), + is_signer: false, + is_writable: false, + }, + ], + data: cpi_data, + }; + + pinocchio::cpi::invoke( + &cpi_instruction, + &[quoter_config, quoter_chain_info, quoter_quote_body], + )?; + + // Get return data from quoter and forward it + let return_data = + pinocchio::cpi::get_return_data().ok_or(ExecutorQuoterRouterError::InvalidReturnData)?; + set_return_data(return_data.as_slice()); + + Ok(()) +} diff --git a/svm/pinocchio/programs/executor-quoter-router/src/instructions/request_execution.rs b/svm/pinocchio/programs/executor-quoter-router/src/instructions/request_execution.rs new file mode 100644 index 0000000..a97d174 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/src/instructions/request_execution.rs @@ -0,0 +1,281 @@ +//! RequestExecution instruction for the ExecutorQuoterRouter. +//! +//! The main execution flow: +//! 1. CPI to quoter's RequestExecutionQuote to get payment/payee/quote body +//! 2. Handle payment (transfer to payee, refund excess) +//! 3. Construct EQ02 signed quote +//! 4. CPI to Executor's request_for_execution +//! +//! Input layout (zero-copy optimized): +//! - bytes 0-7: amount (u64 le, payment amount) +//! - bytes 8-27: quoter_address (20 bytes, for registration lookup) +//! - bytes 28+: quoter CPI data (passed directly, includes 8-byte discriminator) +//! +//! The client must set bytes 28-35 to the quoter's RequestExecutionQuote discriminator +//! (Anchor-compatible: byte 0 = 3, bytes 1-7 = padding zeros). + +use pinocchio::{ + account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +use super::executor_cpi::make_executor_request_for_execution_ix; +use super::serialization::make_signed_quote_eq02; +use crate::{ + error::ExecutorQuoterRouterError, + state::{load_account, QuoterRegistration, EXPIRY_TIME_MAX}, + EXECUTOR_PROGRAM_ID, OUR_CHAIN, +}; + +/// Offset where quoter CPI data starts (after amount + quoter_address). +const QUOTER_CPI_OFFSET: usize = 28; + +/// Expected discriminator for quoter RequestExecutionQuote instruction (8 bytes, Anchor-compatible). +/// Byte 0 = instruction ID (3), bytes 1-7 = padding (zeros). +const EXPECTED_QUOTER_DISCRIMINATOR: [u8; 8] = [3, 0, 0, 0, 0, 0, 0, 0]; + +/// Minimum instruction data size: +/// amount (8) + quoter_address (20) + discriminator (8) + dst_chain (2) + dst_addr (32) + +/// refund_addr (32) + request_bytes_len (4) + relay_instructions_len (4) = 110 +const MIN_DATA_LEN: usize = 110; + +/// RequestExecution instruction. +/// +/// Accounts: +/// 0. `[signer, writable]` payer - Pays for execution +/// 1. `[]` _config - reserved for integrator use +/// 2. `[]` quoter_registration - QuoterRegistration PDA for the quoter +/// 3. `[]` quoter_program - The quoter implementation program +/// 4. `[]` executor_program - The executor program to CPI into +/// 5. `[writable]` payee - Receives the payment +/// 6. `[writable]` refund_addr - Receives any excess payment +/// 7. `[]` system_program +/// 8-11. `[]` quoter accounts: quoter_config, chain_info, quote_body, event_cpi +/// +/// Instruction Data Layout (minimum 110 bytes): +/// ```text +/// Offset Size Field +/// ------ ---- ----- +/// 0 8 amount (u64 LE) - Payment amount +/// 8 20 quoter_address - For registration lookup +/// +/// --- Quoter CPI data (passed directly to quoter) --- +/// 28 8 discriminator - Must be [3, 0, 0, 0, 0, 0, 0, 0] +/// 36 2 dst_chain (u16 LE) - Destination chain ID +/// 38 32 dst_addr - Destination address +/// 70 32 refund_addr - Refund address +/// 102 4 request_bytes_len (u32 LE) +/// 106 var request_bytes +/// var 4 relay_instructions_len (u32 LE) +/// var var relay_instructions +/// ``` +pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + if data.len() < MIN_DATA_LEN { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + // Parse amount (bytes 0-7) + let amount = u64::from_le_bytes( + data[0..8] + .try_into() + .map_err(|_| ExecutorQuoterRouterError::InvalidInstructionData)?, + ); + + // Parse quoter_address (bytes 8-27) + let quoter_address: [u8; 20] = data[8..28] + .try_into() + .map_err(|_| ExecutorQuoterRouterError::InvalidInstructionData)?; + + // CPI data starts at byte 28 + let cpi_data = &data[QUOTER_CPI_OFFSET..]; + + // Validate CPI data structure and extract fields we need for executor CPI + // CPI layout: discriminator (8) + dst_chain (2) + dst_addr (32) + refund_addr (32) + + // request_bytes_len (4) + request_bytes + relay_instructions_len (4) + relay_instructions + if cpi_data.len() < 82 { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + // Validate discriminator matches expected RequestExecutionQuote instruction (8-byte Anchor-compatible) + if cpi_data[0..8] != EXPECTED_QUOTER_DISCRIMINATOR { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + // Extract fields from CPI data (after 8-byte discriminator) + let dst_chain = u16::from_le_bytes( + cpi_data[8..10] + .try_into() + .map_err(|_| ExecutorQuoterRouterError::InvalidInstructionData)?, + ); + + let dst_addr: [u8; 32] = cpi_data[10..42] + .try_into() + .map_err(|_| ExecutorQuoterRouterError::InvalidInstructionData)?; + + let refund_addr_bytes: [u8; 32] = cpi_data[42..74] + .try_into() + .map_err(|_| ExecutorQuoterRouterError::InvalidInstructionData)?; + + let request_bytes_len = u32::from_le_bytes( + cpi_data[74..78] + .try_into() + .map_err(|_| ExecutorQuoterRouterError::InvalidInstructionData)?, + ) as usize; + + let request_bytes_start = 78; + let request_bytes_end = request_bytes_start + request_bytes_len; + + if cpi_data.len() < request_bytes_end + 4 { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + let request_bytes = &cpi_data[request_bytes_start..request_bytes_end]; + + let relay_len_offset = request_bytes_end; + let relay_instructions_len = u32::from_le_bytes( + cpi_data[relay_len_offset..relay_len_offset + 4] + .try_into() + .map_err(|_| ExecutorQuoterRouterError::InvalidInstructionData)?, + ) as usize; + + let relay_instructions_start = relay_len_offset + 4; + let relay_instructions_end = relay_instructions_start + relay_instructions_len; + + if cpi_data.len() < relay_instructions_end { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + let relay_instructions = &cpi_data[relay_instructions_start..relay_instructions_end]; + + // Parse accounts + let [payer, _config, quoter_registration_account, quoter_program, _executor_program, payee, _refund_account, system_program, quoter_config, quoter_chain_info, quoter_quote_body, event_cpi] = + accounts + else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Verify payer is signer + if !payer.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Load and verify quoter registration + let registration = load_account::(quoter_registration_account, program_id)?; + + if registration.quoter_address != quoter_address { + return Err(ExecutorQuoterRouterError::QuoterNotRegistered.into()); + } + + if quoter_program.key() != ®istration.implementation_program_id { + return Err(ExecutorQuoterRouterError::QuoterNotRegistered.into()); + } + + // Step 1: CPI to quoter's RequestExecutionQuote (zero-copy) + let quoter_cpi_instruction = pinocchio::instruction::Instruction { + program_id: ®istration.implementation_program_id, + accounts: &[ + pinocchio::instruction::AccountMeta { + pubkey: quoter_config.key(), + is_signer: false, + is_writable: false, + }, + pinocchio::instruction::AccountMeta { + pubkey: quoter_chain_info.key(), + is_signer: false, + is_writable: false, + }, + pinocchio::instruction::AccountMeta { + pubkey: quoter_quote_body.key(), + is_signer: false, + is_writable: false, + }, + pinocchio::instruction::AccountMeta { + pubkey: event_cpi.key(), + is_signer: false, + is_writable: false, + }, + ], + data: cpi_data, + }; + + pinocchio::cpi::invoke( + "er_cpi_instruction, + &[ + quoter_config, + quoter_chain_info, + quoter_quote_body, + event_cpi, + ], + )?; + + // Get return data from quoter: (required_payment, payee_address, quote_body) + // Layout: 8 bytes payment + 32 bytes payee + 32 bytes quote_body = 72 bytes + let return_data = + pinocchio::cpi::get_return_data().ok_or(ExecutorQuoterRouterError::InvalidReturnData)?; + + if return_data.len() < 72 { + return Err(ExecutorQuoterRouterError::InvalidReturnData.into()); + } + + let mut required_payment_bytes = [0u8; 8]; + required_payment_bytes.copy_from_slice(&return_data[0..8]); + let required_payment = u64::from_be_bytes(required_payment_bytes); + + let mut payee_address = [0u8; 32]; + payee_address.copy_from_slice(&return_data[8..40]); + + let mut quote_body = [0u8; 32]; + quote_body.copy_from_slice(&return_data[40..72]); + + // Step 2: Handle payment + if amount < required_payment { + return Err(ExecutorQuoterRouterError::Underpaid.into()); + } + + // Step 3: Construct EQ02 signed quote + let signed_quote = make_signed_quote_eq02( + "er_address, + &payee_address, + OUR_CHAIN, + dst_chain, + EXPIRY_TIME_MAX, + "e_body, + ); + + // Step 4: CPI to Executor's request_for_execution + // Note: This still requires allocation due to signed_quote being constructed on-chain + let executor_ix_data = make_executor_request_for_execution_ix( + amount, + dst_chain, + &dst_addr, + &refund_addr_bytes, + &signed_quote, + request_bytes, + relay_instructions, + ); + + let executor_cpi_instruction = pinocchio::instruction::Instruction { + program_id: &EXECUTOR_PROGRAM_ID, + accounts: &[ + pinocchio::instruction::AccountMeta { + pubkey: payer.key(), + is_signer: true, + is_writable: true, + }, + pinocchio::instruction::AccountMeta { + pubkey: payee.key(), + is_signer: false, + is_writable: true, + }, + pinocchio::instruction::AccountMeta { + pubkey: &pinocchio_system::ID, + is_signer: false, + is_writable: false, + }, + ], + data: &executor_ix_data, + }; + + pinocchio::cpi::invoke(&executor_cpi_instruction, &[payer, payee, system_program])?; + + Ok(()) +} diff --git a/svm/pinocchio/programs/executor-quoter-router/src/instructions/serialization.rs b/svm/pinocchio/programs/executor-quoter-router/src/instructions/serialization.rs new file mode 100644 index 0000000..e4745d9 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/src/instructions/serialization.rs @@ -0,0 +1,122 @@ +//! Router-specific serialization for signed quotes (EQ02) and governance messages (EG01). +//! These are only used by the router program, not shared with other programs/clients. + +use pinocchio::program_error::ProgramError; + +use crate::error::ExecutorQuoterRouterError; + +/// EQ02 signed quote prefix +pub const QUOTE_PREFIX_EQ02: &[u8; 4] = b"EQ02"; + +/// EG01 governance message prefix +pub const GOV_PREFIX_EG01: &[u8; 4] = b"EG01"; + +/// Constructs an EQ02 signed quote. +/// +/// Layout (100 bytes): +/// - bytes 0-3: prefix "EQ02" (4 bytes) +/// - bytes 4-23: quoter_address (20 bytes, Ethereum address) +/// - bytes 24-55: payee_address (32 bytes, universal address) +/// - bytes 56-57: src_chain (u16 be) +/// - bytes 58-59: dst_chain (u16 be) +/// - bytes 60-67: expiry_time (u64 be) +/// - bytes 68-99: quote_body (32 bytes, EQ01 format) +pub fn make_signed_quote_eq02( + quoter_address: &[u8; 20], + payee_address: &[u8; 32], + src_chain: u16, + dst_chain: u16, + expiry_time: u64, + quote_body: &[u8; 32], +) -> [u8; 100] { + let mut out = [0u8; 100]; + out[0..4].copy_from_slice(QUOTE_PREFIX_EQ02); + out[4..24].copy_from_slice(quoter_address); + out[24..56].copy_from_slice(payee_address); + out[56..58].copy_from_slice(&src_chain.to_be_bytes()); + out[58..60].copy_from_slice(&dst_chain.to_be_bytes()); + out[60..68].copy_from_slice(&expiry_time.to_be_bytes()); + out[68..100].copy_from_slice(quote_body); + out +} + +/// Parsed governance message for UpdateQuoterContract. +/// +/// Layout (163 bytes): +/// - bytes 0-3: prefix "EG01" (4 bytes) +/// - bytes 4-5: chain_id (u16 be) +/// - bytes 6-25: quoter_address (20 bytes, Ethereum address) +/// - bytes 26-57: universal_contract_address (32 bytes) +/// - bytes 58-89: universal_sender_address (32 bytes) +/// - bytes 90-97: expiry_time (u64 be) +/// - bytes 98-129: signature_r (32 bytes) +/// - bytes 130-161: signature_s (32 bytes) +/// - byte 162: signature_v (1 byte) +#[derive(Debug, Clone, Copy)] +pub struct GovernanceMessage { + pub chain_id: u16, + pub quoter_address: [u8; 20], + pub universal_contract_address: [u8; 32], + pub universal_sender_address: [u8; 32], + pub expiry_time: u64, + pub signature_r: [u8; 32], + pub signature_s: [u8; 32], + pub signature_v: u8, +} + +impl GovernanceMessage { + pub const LEN: usize = 163; + + /// Parse a governance message from bytes. + pub fn parse(data: &[u8]) -> Result { + if data.len() < Self::LEN { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + // Check prefix + if &data[0..4] != GOV_PREFIX_EG01 { + return Err(ExecutorQuoterRouterError::InvalidGovernancePrefix.into()); + } + + let mut chain_id_bytes = [0u8; 2]; + chain_id_bytes.copy_from_slice(&data[4..6]); + let chain_id = u16::from_be_bytes(chain_id_bytes); + + let mut quoter_address = [0u8; 20]; + quoter_address.copy_from_slice(&data[6..26]); + + let mut universal_contract_address = [0u8; 32]; + universal_contract_address.copy_from_slice(&data[26..58]); + + let mut universal_sender_address = [0u8; 32]; + universal_sender_address.copy_from_slice(&data[58..90]); + + let mut expiry_time_bytes = [0u8; 8]; + expiry_time_bytes.copy_from_slice(&data[90..98]); + let expiry_time = u64::from_be_bytes(expiry_time_bytes); + + let mut signature_r = [0u8; 32]; + signature_r.copy_from_slice(&data[98..130]); + + let mut signature_s = [0u8; 32]; + signature_s.copy_from_slice(&data[130..162]); + + let signature_v = data[162]; + + Ok(Self { + chain_id, + quoter_address, + universal_contract_address, + universal_sender_address, + expiry_time, + signature_r, + signature_s, + signature_v, + }) + } + + /// Get the message bytes that were signed (bytes 0-98). + pub fn signed_message<'a>(&self, original_data: &'a [u8]) -> &'a [u8] { + &original_data[0..98] + } +} diff --git a/svm/pinocchio/programs/executor-quoter-router/src/instructions/update_quoter_contract.rs b/svm/pinocchio/programs/executor-quoter-router/src/instructions/update_quoter_contract.rs new file mode 100644 index 0000000..de98837 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/src/instructions/update_quoter_contract.rs @@ -0,0 +1,258 @@ +//! UpdateQuoterContract instruction for the ExecutorQuoterRouter. +//! +//! Registers or updates a quoter's implementation mapping using a signed governance message. +//! Uses secp256k1 ecrecover for signature verification to maintain EVM compatibility. + +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, + pubkey::{find_program_address, Pubkey}, + sysvars::{clock::Clock, Sysvar}, + ProgramResult, +}; + +use pinocchio::syscalls::{sol_keccak256, sol_secp256k1_recover}; + +use crate::{ + error::ExecutorQuoterRouterError, + state::{QuoterRegistration, QUOTER_REGISTRATION_DISCRIMINATOR, QUOTER_REGISTRATION_SEED}, + OUR_CHAIN, +}; + +use super::serialization::GovernanceMessage; + +/// Secp256k1 public key length (uncompressed, without 0x04 prefix) +const SECP256K1_PUBKEY_LEN: usize = 64; + +/// Keccak256 hash length +const KECCAK256_HASH_LEN: usize = 32; + +/// Verifies a secp256k1 signature and recovers the Ethereum address of the signer. +/// +/// This mirrors the shim contract ecrecover behavior (https://github.com/wormhole-foundation/wormhole/blob/main/svm/wormhole-core-shims/programs/verify-vaa/src/lib.rs): +/// 1. Hash the message with keccak256 +/// 2. Recover the public key using secp256k1_recover +/// 3. Derive the Ethereum address: keccak256(pubkey)[12:32] +/// +/// Returns the 20-byte Ethereum address of the signer. +fn ecrecover( + message: &[u8], + signature_r: &[u8; 32], + signature_s: &[u8; 32], + signature_v: u8, +) -> Result<[u8; 20], ProgramError> { + // Step 1: Compute keccak256 hash of the message + // Docs for this are here: https://github.com/solana-labs/solana/blob/master/programs/bpf_loader/src/syscalls/mod.rs + // The syscall expects a slice format: [ptr, len] pairs + let mut digest = [0u8; KECCAK256_HASH_LEN]; + + // Build the input format for sol_keccak256: array of (ptr, len) pairs + let message_ptr = message.as_ptr() as u64; + let message_len = message.len() as u64; + + // Create slice descriptor: [ptr (8 bytes), len (8 bytes)] + let slice_desc: [u64; 2] = [message_ptr, message_len]; + + unsafe { + let result = sol_keccak256( + slice_desc.as_ptr() as *const u8, + 1, // number of slices + digest.as_mut_ptr(), + ); + if result != 0 { + return Err(ExecutorQuoterRouterError::InvalidSignature.into()); + } + } + + // Step 2: Recover public key using secp256k1_recover + // Signature format: r (32 bytes) || s (32 bytes) + let mut signature_rs = [0u8; 64]; + signature_rs[0..32].copy_from_slice(signature_r); + signature_rs[32..64].copy_from_slice(signature_s); + + // Recovery ID: v - 27 (EVM uses 27/28, Solana uses 0/1) + let recovery_id = if signature_v >= 27 { + (signature_v - 27) as u64 + } else { + signature_v as u64 + }; + + let mut recovered_pubkey = [0u8; SECP256K1_PUBKEY_LEN]; + + unsafe { + let result = sol_secp256k1_recover( + digest.as_ptr(), + recovery_id, + signature_rs.as_ptr(), + recovered_pubkey.as_mut_ptr(), + ); + if result != 0 { + return Err(ExecutorQuoterRouterError::InvalidSignature.into()); + } + } + + // Step 3: Derive Ethereum address from recovered public key + // Ethereum address = keccak256(pubkey)[12:32] + let mut pubkey_hash = [0u8; KECCAK256_HASH_LEN]; + let pubkey_ptr = recovered_pubkey.as_ptr() as u64; + let pubkey_len = SECP256K1_PUBKEY_LEN as u64; + let pubkey_slice_desc: [u64; 2] = [pubkey_ptr, pubkey_len]; + + unsafe { + let result = sol_keccak256( + pubkey_slice_desc.as_ptr() as *const u8, + 1, + pubkey_hash.as_mut_ptr(), + ); + if result != 0 { + return Err(ExecutorQuoterRouterError::InvalidSignature.into()); + } + } + + // Take last 20 bytes as Ethereum address + let mut eth_address = [0u8; 20]; + eth_address.copy_from_slice(&pubkey_hash[12..32]); + + Ok(eth_address) +} + +/// UpdateQuoterContract instruction. +/// +/// Accounts: +/// 0. `[signer, writable]` payer - Pays for account creation +/// 1. `[signer]` sender - Must match universal_sender_address in governance message +/// 2. `[]` _config - reserved for integrator implementations +/// 3. `[writable]` quoter_registration - QuoterRegistration PDA (created if needed) +/// 4. `[]` system_program +/// +/// Instruction data layout: +/// - governance_message: [u8; 163] - The full EG01 governance message +pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + // Minimum data: 163 byte governance message + if data.len() < 163 { + return Err(ExecutorQuoterRouterError::InvalidInstructionData.into()); + } + + let gov_data = data; + + // Parse accounts + let [payer, sender, _config, quoter_registration_account, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Verify signers + if !payer.is_signer() || !sender.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Parse governance message + let gov_msg = GovernanceMessage::parse(gov_data)?; + + // Verify chain ID against hardcoded constant + if gov_msg.chain_id != OUR_CHAIN { + return Err(ExecutorQuoterRouterError::ChainIdMismatch.into()); + } + + // Verify sender matches universal_sender_address + // On SVM, the sender is a full 32-byte Solana pubkey. + // Note: On EVM, this would validate that upper 12 bytes are zero (NotAnEvmAddress check). + // On SVM, we use the full 32 bytes as Solana pubkeys, so no upper byte check is needed. + let sender_key = sender.key(); + if gov_msg.universal_sender_address != *sender_key { + return Err(ExecutorQuoterRouterError::InvalidSender.into()); + } + + // Verify expiry time + let clock = Clock::get()?; + if gov_msg.expiry_time <= clock.unix_timestamp as u64 { + return Err(ExecutorQuoterRouterError::GovernanceExpired.into()); + } + + // Verify secp256k1 signature + // The signed message is bytes 0-98 of the governance message (before signature) + // This mirrors EVM: bytes32 hash = keccak256(gov[0:98]); + { + let signed_message = gov_msg.signed_message(gov_data); + let recovered_address = ecrecover( + signed_message, + &gov_msg.signature_r, + &gov_msg.signature_s, + gov_msg.signature_v, + )?; + + // Verify the signer matches the quoter address + // This mirrors EVM: if (signer != quoterAddr) revert InvalidSignature(); + if recovered_address != gov_msg.quoter_address { + return Err(ExecutorQuoterRouterError::InvalidSignature.into()); + } + } + + // Extract implementation program ID from universal_contract_address + // For SVM, this is just the full 32 bytes (a Solana pubkey) + let implementation_program_id: Pubkey = gov_msg.universal_contract_address; + + // Check if account needs to be created + if quoter_registration_account.data_is_empty() { + // Derive canonical PDA and bump using find_program_address + let (expected_pda, canonical_bump) = find_program_address( + &[QUOTER_REGISTRATION_SEED, &gov_msg.quoter_address[..]], + program_id, + ); + + // Verify passed account matches expected PDA + if quoter_registration_account.key() != &expected_pda { + return Err(ProgramError::InvalidSeeds); + } + + // Create signer seeds with canonical bump + let bump_seed = [canonical_bump]; + let signer_seeds = [ + Seed::from(QUOTER_REGISTRATION_SEED), + Seed::from(&gov_msg.quoter_address[..]), + Seed::from(&bump_seed), + ]; + let signers = [Signer::from(&signer_seeds[..])]; + + // Create account via CPI (handles pre-funded accounts to prevent griefing) + pinocchio_system::create_account_with_minimum_balance_signed( + quoter_registration_account, + QuoterRegistration::LEN, + program_id, + payer, + None, + &signers, + )?; + + // Initialize registration data + let registration = QuoterRegistration { + discriminator: QUOTER_REGISTRATION_DISCRIMINATOR, + bump: canonical_bump, + quoter_address: gov_msg.quoter_address, + implementation_program_id, + }; + quoter_registration_account + .try_borrow_mut_data()? + .copy_from_slice(bytemuck::bytes_of(®istration)); + } else { + // Account exists - verify ownership + if quoter_registration_account.owner() != program_id { + return Err(ExecutorQuoterRouterError::InvalidOwner.into()); + } + + // Update registration data. + // Safety: owner check above guarantees correct size (only our program can + // create accounts it owns, and we always use QuoterRegistration::LEN). + let mut reg_data = quoter_registration_account.try_borrow_mut_data()?; + + // Verify discriminator + if reg_data[0] != QUOTER_REGISTRATION_DISCRIMINATOR { + return Err(ExecutorQuoterRouterError::InvalidDiscriminator.into()); + } + + // Only update mutable field (implementation program ID) + reg_data[22..54].copy_from_slice(&implementation_program_id); + } + + Ok(()) +} diff --git a/svm/pinocchio/programs/executor-quoter-router/src/lib.rs b/svm/pinocchio/programs/executor-quoter-router/src/lib.rs new file mode 100644 index 0000000..31c9e7f --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/src/lib.rs @@ -0,0 +1,86 @@ +#![no_std] + +use pinocchio::{ + account_info::AccountInfo, default_allocator, nostd_panic_handler, program_entrypoint, + program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +program_entrypoint!(process_instruction); +default_allocator!(); +nostd_panic_handler!(); + +pub mod error; +pub mod instructions; +pub mod state; + +use instructions::*; + +/// Program ID - replace with actual deployed address +pub static ID: Pubkey = [ + 0x0e, 0xf8, 0xc4, 0xd6, 0x7b, 0x42, 0x89, 0xd6, 0x3e, 0xf0, 0x63, 0x1b, 0x5d, 0x0c, 0x39, 0x18, + 0x2e, 0x8c, 0x9a, 0x4f, 0x7f, 0x9d, 0x8a, 0x3b, 0x6c, 0x5e, 0x4d, 0x3c, 0x2b, 0x1a, 0x09, 0xf9, +]; + +// ============================================================================= +// Hardcoded Configuration +// TODO: Replace with env variables at build time +// ============================================================================= + +/// Solana Wormhole chain ID +pub const OUR_CHAIN: u16 = 1; + +/// Executor program ID: execXUrAsMnqMmTHj5m7N1YQgsDz3cwGLYCYyuDRciV +pub const EXECUTOR_PROGRAM_ID: Pubkey = [ + 0x09, 0xb9, 0x69, 0x71, 0x58, 0x3b, 0x59, 0x03, 0xe0, 0x28, 0x1d, 0xa9, 0x65, 0x48, 0xd5, 0xd2, + 0x3c, 0x65, 0x1f, 0x7a, 0x9c, 0xcd, 0xe3, 0xea, 0xd5, 0x2b, 0x42, 0xf6, 0xb7, 0xda, 0xc2, 0xd2, +]; + +/// Instruction discriminators +#[repr(u8)] +pub enum Instruction { + /// Register or update a quoter's implementation mapping + /// Accounts: [payer, sender, _config, quoter_registration, system_program] + UpdateQuoterContract = 0, + + /// Get a quote from a registered quoter (read-only CPI) + /// Accounts: [_config, quoter_registration, quoter_program, ...quoter_accounts] + QuoteExecution = 1, + + /// Request execution through the router + /// Accounts: [payer, _config, quoter_registration, quoter_program, executor_program, payee, refund_addr, system_program, ...quoter_accounts] + RequestExecution = 2, +} + +impl TryFrom for Instruction { + type Error = ProgramError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Instruction::UpdateQuoterContract), + 1 => Ok(Instruction::QuoteExecution), + 2 => Ok(Instruction::RequestExecution), + _ => Err(ProgramError::InvalidInstructionData), + } + } +} + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if instruction_data.is_empty() { + return Err(ProgramError::InvalidInstructionData); + } + + let (instruction_discriminator, data) = instruction_data.split_at(1); + let instruction = Instruction::try_from(instruction_discriminator[0])?; + + match instruction { + Instruction::UpdateQuoterContract => { + update_quoter_contract::process(program_id, accounts, data) + } + Instruction::QuoteExecution => quote_execution::process(program_id, accounts, data), + Instruction::RequestExecution => request_execution::process(program_id, accounts, data), + } +} diff --git a/svm/pinocchio/programs/executor-quoter-router/src/state.rs b/svm/pinocchio/programs/executor-quoter-router/src/state.rs new file mode 100644 index 0000000..069dc9f --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter-router/src/state.rs @@ -0,0 +1,68 @@ +use bytemuck::{Pod, Zeroable}; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +use crate::error::ExecutorQuoterRouterError; + +/// Account discriminators for type safety +pub const QUOTER_REGISTRATION_DISCRIMINATOR: u8 = 1; + +/// PDA seed prefixes +pub const QUOTER_REGISTRATION_SEED: &[u8] = b"quoter_registration"; + +/// Expiry time constant - u64::MAX means no expiry +pub const EXPIRY_TIME_MAX: u64 = u64::MAX; + +/// Trait for accounts with a discriminator byte at offset 0. +pub trait Discriminator { + const DISCRIMINATOR: u8; +} + +/// Registration mapping a quoter address to its implementation program. +/// PDA seeds: ["quoter_registration", quoter_address (20 bytes)] +/// +/// This mirrors the EVM `mapping(address => IExecutorQuoter) quoterContract`. +#[repr(C)] +#[derive(Pod, Zeroable, Clone, Copy, Debug, PartialEq)] +pub struct QuoterRegistration { + /// Discriminator for account type validation + pub discriminator: u8, + /// PDA bump seed + pub bump: u8, + /// The quoter's Ethereum address (20 bytes) - used as the identity/key + pub quoter_address: [u8; 20], + /// The program ID of the quoter implementation to CPI into + pub implementation_program_id: Pubkey, +} + +impl Discriminator for QuoterRegistration { + const DISCRIMINATOR: u8 = QUOTER_REGISTRATION_DISCRIMINATOR; +} + +impl QuoterRegistration { + pub const LEN: usize = core::mem::size_of::(); +} + +/// Load a typed account from AccountInfo, validating ownership and discriminator. +/// Returns a copy of the account data. +pub fn load_account( + account: &AccountInfo, + program_id: &Pubkey, +) -> Result { + if account.owner() != program_id { + return Err(ExecutorQuoterRouterError::InvalidOwner.into()); + } + + let data = account.try_borrow_data()?; + if data.len() < core::mem::size_of::() { + return Err(ProgramError::InvalidAccountData); + } + + // Check discriminator (first byte) + if data[0] != T::DISCRIMINATOR { + return Err(ExecutorQuoterRouterError::InvalidDiscriminator.into()); + } + + let account = bytemuck::try_from_bytes::(&data[..core::mem::size_of::()]) + .map_err(|_| ProgramError::InvalidAccountData)?; + Ok(*account) +} diff --git a/svm/pinocchio/programs/executor-quoter/Cargo.toml b/svm/pinocchio/programs/executor-quoter/Cargo.toml new file mode 100644 index 0000000..0baa73d --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "executor-quoter" +version = "0.1.0" +description = "Executor Quoter - provides on-chain quote generation for cross-chain execution" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] +name = "executor_quoter" + +[features] +default = [] +no-entrypoint = [] +custom-heap = [] +custom-panic = [] + +[dependencies] +pinocchio = "0.9.2" +pinocchio-system = "0.4" +bytemuck = { version = "1.14", features = ["derive"] } +executor-requests = { path = "../../../modules/executor-requests" } + +[build-dependencies] +bs58 = "0.5" diff --git a/svm/pinocchio/programs/executor-quoter/README.md b/svm/pinocchio/programs/executor-quoter/README.md new file mode 100644 index 0000000..f0beccf --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/README.md @@ -0,0 +1,63 @@ +# Executor Quoter + +Example quoter program for the Wormhole executor system. This implementation serves as a reference for integrators building their own quoter logic. + +## Interface Specification + +The quoter interface is defined by CPI calls from the `executor-quoter-router` program. Integrators must implement two required instructions with specific discriminators: + +### Required Instructions + +**RequestQuote (discriminator: 2)** + +- Returns a quote for cross-chain execution +- Accounts: `[config, chain_info, quote_body]` +- Returns: `u64` payment amount (8 bytes, big-endian) + +**RequestExecutionQuote (discriminator: 3)** + +- Returns full execution details including payment, payee, and quote body +- Accounts: `[config, chain_info, quote_body, event_cpi]` +- Returns: 72 bytes (`u64` payment + 32-byte payee address + 32-byte quote body) + +Both instructions support up to 8-byte discriminators for Anchor compatibility (byte 0 = instruction ID, bytes 1-7 = padding zeros). + +### Instruction Data Layout + +Both instructions share the same input format (after discriminator): + +- `dst_chain`: u16 (LE) +- `dst_addr`: [u8; 32] +- `refund_addr`: [u8; 32] +- `request_bytes_len`: u32 (LE) +- `request_bytes`: variable +- `relay_instructions_len`: u32 (LE) +- `relay_instructions`: variable + +This is compatible with Borsh serialization: + +```rust +#[derive(borsh::BorshSerialize, borsh::BorshDeserialize)] +pub struct RequestQuoteData { + pub dst_chain: u16, + pub dst_addr: [u8; 32], + pub refund_addr: [u8; 32], + pub request_bytes: Vec, + pub relay_instructions: Vec, +} +``` + +## Optional Instructions + +The following instructions have no inherent spec and are left to integrator discretion: + +- **UpdateChainInfo (discriminator: 0)**: Configure per-chain parameters +- **UpdateQuote (discriminator: 1)**: Update pricing data +- Any additional instructions one may wish to add + +This example uses 1-byte discriminators for admin instructions, but this is by no means necessary. + +## Reserved Accounts + +- **config**: First account in CPI calls. Unused in this example but available for integrator-specific configuration state. +- **event_cpi**: Fourth account in `RequestExecutionQuote`. Reserved for integrators who want to emit events during quote execution. diff --git a/svm/programs/executor/Xargo.toml b/svm/pinocchio/programs/executor-quoter/Xargo.toml similarity index 100% rename from svm/programs/executor/Xargo.toml rename to svm/pinocchio/programs/executor-quoter/Xargo.toml diff --git a/svm/pinocchio/programs/executor-quoter/build.rs b/svm/pinocchio/programs/executor-quoter/build.rs new file mode 100644 index 0000000..6da2b5c --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/build.rs @@ -0,0 +1,68 @@ +use std::env; +use std::fs; +use std::path::Path; + +fn main() { + let out_dir = env::var("OUT_DIR").unwrap(); + let out_path = Path::new(&out_dir); + + // Parse QUOTER_UPDATER_PUBKEY env var (base58 pubkey string) + // Falls back to a default test pubkey if not set + let updater_pubkey = + env::var("QUOTER_UPDATER_PUBKEY").expect("Updater pubkey must be set at build time"); + + // Parse QUOTER_PAYEE_PUBKEY env var (base58 pubkey string) + // Falls back to same as updater if not set + let payee_pubkey = env::var("QUOTER_PAYEE_PUBKEY").unwrap_or_else(|_| updater_pubkey.clone()); + + // Decode base58 to bytes + let updater_bytes = bs58::decode(&updater_pubkey) + .into_vec() + .unwrap_or_else(|e| { + panic!( + "QUOTER_UPDATER_PUBKEY '{}' is not valid base58: {}", + updater_pubkey, e + ) + }); + let payee_bytes = bs58::decode(&payee_pubkey).into_vec().unwrap_or_else(|e| { + panic!( + "QUOTER_PAYEE_PUBKEY '{}' is not valid base58: {}", + payee_pubkey, e + ) + }); + + assert_eq!( + updater_bytes.len(), + 32, + "QUOTER_UPDATER_PUBKEY must decode to 32 bytes, got {}", + updater_bytes.len() + ); + assert_eq!( + payee_bytes.len(), + 32, + "QUOTER_PAYEE_PUBKEY must decode to 32 bytes, got {}", + payee_bytes.len() + ); + + // Write the byte arrays as includable Rust files + fs::write( + out_path.join("updater_address.rs"), + format_byte_array(&updater_bytes), + ) + .expect("Failed to write updater_address.rs"); + + fs::write( + out_path.join("payee_address.rs"), + format_byte_array(&payee_bytes), + ) + .expect("Failed to write payee_address.rs"); + + // Rerun if env vars change + println!("cargo:rerun-if-env-changed=QUOTER_UPDATER_PUBKEY"); + println!("cargo:rerun-if-env-changed=QUOTER_PAYEE_PUBKEY"); +} + +fn format_byte_array(bytes: &[u8]) -> String { + let hex_bytes: Vec = bytes.iter().map(|b| format!("0x{:02x}", b)).collect(); + format!("[{}]", hex_bytes.join(", ")) +} diff --git a/svm/pinocchio/programs/executor-quoter/src/error.rs b/svm/pinocchio/programs/executor-quoter/src/error.rs new file mode 100644 index 0000000..1c2e838 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/src/error.rs @@ -0,0 +1,55 @@ +use pinocchio::program_error::ProgramError; + +/// Custom error codes for ExecutorQuoter program. +/// Error codes start at 0x1000 to avoid collision with built-in errors. +#[repr(u32)] +pub enum ExecutorQuoterError { + /// Caller is not the authorized updater + InvalidUpdater = 0x1000, + /// Destination chain is not enabled + ChainDisabled = 0x1001, + /// Unsupported relay instruction type + UnsupportedInstruction = 0x1002, + /// Only one drop-off instruction is allowed + MoreThanOneDropOff = 0x1003, + /// Arithmetic overflow in quote calculation + MathOverflow = 0x1004, + /// Invalid relay instruction data + InvalidRelayInstructions = 0x1005, + /// Invalid PDA derivation + InvalidPda = 0x1006, + /// Account already initialized + AlreadyInitialized = 0x1007, + /// Account not initialized + NotInitialized = 0x1008, + /// Invalid account owner + InvalidOwner = 0x1009, + /// Invalid instruction data + InvalidInstructionData = 0x100A, + /// Invalid account discriminator + InvalidDiscriminator = 0x100B, + /// Chain ID in instruction does not match account + ChainIdMismatch = 0x100C, +} + +impl From for ProgramError { + fn from(e: ExecutorQuoterError) -> Self { + ProgramError::Custom(e as u32) + } +} + +/// Base error code for relay parsing errors. +/// Aligns with UnsupportedInstruction (0x1002) as the first relay-specific error. +pub const RELAY_PARSE_ERROR_BASE: u32 = 0x1002; + +/// Converts a relay parse error code to a ProgramError. +/// +/// Direct arithmetic conversion: +/// - UnsupportedType(0) -> 0x1002 (UnsupportedInstruction) +/// - MultipleDropoff(1) -> 0x1003 (MoreThanOneDropOff) +/// - Overflow(2) -> 0x1004 (MathOverflow) +/// - Truncated(3) -> 0x1005 (InvalidRelayInstructions) +#[inline] +pub fn relay_parse_error_to_program_error(e: executor_requests::RelayParseError) -> ProgramError { + ProgramError::Custom(RELAY_PARSE_ERROR_BASE + e as u32) +} diff --git a/svm/pinocchio/programs/executor-quoter/src/instructions/get_quote.rs b/svm/pinocchio/programs/executor-quoter/src/instructions/get_quote.rs new file mode 100644 index 0000000..cadb7fb --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/src/instructions/get_quote.rs @@ -0,0 +1,159 @@ +use executor_requests::parse_relay_instructions; +use pinocchio::{ + account_info::AccountInfo, cpi::set_return_data, program_error::ProgramError, pubkey::Pubkey, + ProgramResult, +}; + +use crate::{ + error::{relay_parse_error_to_program_error, ExecutorQuoterError}, + math, + state::{load_account, ChainInfo, QuoteBody}, + PAYEE_ADDRESS, +}; + +/// Process RequestQuote instruction. +/// Returns the required payment amount for cross-chain execution. +/// +/// Accounts: +/// 0. `[]` config - Config PDA +/// 1. `[]` chain_info - ChainInfo PDA for destination chain +/// 2. `[]` quote_body - QuoteBody PDA for destination chain +/// +/// Instruction data layout: +/// - dst_chain: u16 (offset 0) +/// - dst_addr: [u8; 32] (offset 2) +/// - refund_addr: [u8; 32] (offset 34) +/// - request_bytes_len: u32 (offset 66) +/// - request_bytes: [u8; request_bytes_len] (offset 70) +/// - relay_instructions_len: u32 (offset 70 + request_bytes_len) +/// - relay_instructions: [u8; relay_instructions_len] +pub fn process_request_quote( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + // Parse accounts + let [_config, chain_info_account, quote_body_account] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Load accounts (discriminator checked inside load_account) + let chain_info = load_account::(chain_info_account, program_id)?; + if !chain_info.is_enabled() { + return Err(ExecutorQuoterError::ChainDisabled.into()); + } + + let quote_body = load_account::(quote_body_account, program_id)?; + + // Parse instruction data to get relay_instructions + // Skip: dst_chain (2) + dst_addr (32) + refund_addr (32) = 66 bytes + if data.len() < 70 { + return Err(ExecutorQuoterError::InvalidInstructionData.into()); + } + + // Skip request_bytes + let mut len_bytes = [0u8; 4]; + len_bytes.copy_from_slice(&data[66..70]); + let request_bytes_len = u32::from_le_bytes(len_bytes) as usize; + let relay_start = 70 + request_bytes_len; + + if data.len() < relay_start + 4 { + return Err(ExecutorQuoterError::InvalidInstructionData.into()); + } + + let mut relay_len_bytes = [0u8; 4]; + relay_len_bytes.copy_from_slice(&data[relay_start..relay_start + 4]); + let relay_instructions_len = u32::from_le_bytes(relay_len_bytes) as usize; + + let relay_data_start = relay_start + 4; + if data.len() < relay_data_start + relay_instructions_len { + return Err(ExecutorQuoterError::InvalidInstructionData.into()); + } + + let relay_instructions = &data[relay_data_start..relay_data_start + relay_instructions_len]; + + // Parse relay instructions + let (gas_limit, msg_value) = + parse_relay_instructions(relay_instructions).map_err(relay_parse_error_to_program_error)?; + + // Calculate quote - returns u64 in SVM native decimals (lamports) + let required_payment = math::estimate_quote("e_body, &chain_info, gas_limit, msg_value)?; + + // Return the quote as u64 (8 bytes, big-endian) via set_return_data. + set_return_data(&required_payment.to_be_bytes()); + + Ok(()) +} + +/// Process RequestExecutionQuote instruction. +/// Returns the required payment, payee address, and quote body. +/// +/// Accounts: +/// 0. `[]` config - Config PDA +/// 1. `[]` chain_info - ChainInfo PDA for destination chain +/// 2. `[]` quote_body - QuoteBody PDA for destination chain +/// 3. `[]` event_cpi - Account for event CPI (unused in this implementation, but required for interface compatibility) +pub fn process_request_execution_quote( + program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + // Parse accounts - _config and event_cpi are required but unused in this implementation. + // Future quoter implementations may use them. + let [_config, chain_info_account, quote_body_account, _event_cpi] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Load accounts (discriminator checked inside load_account) + let chain_info = load_account::(chain_info_account, program_id)?; + if !chain_info.is_enabled() { + return Err(ExecutorQuoterError::ChainDisabled.into()); + } + + let quote_body = load_account::(quote_body_account, program_id)?; + + // Parse instruction data to get relay_instructions + if data.len() < 70 { + return Err(ExecutorQuoterError::InvalidInstructionData.into()); + } + + let mut len_bytes = [0u8; 4]; + len_bytes.copy_from_slice(&data[66..70]); + let request_bytes_len = u32::from_le_bytes(len_bytes) as usize; + let relay_start = 70 + request_bytes_len; + + if data.len() < relay_start + 4 { + return Err(ExecutorQuoterError::InvalidInstructionData.into()); + } + + let mut relay_len_bytes = [0u8; 4]; + relay_len_bytes.copy_from_slice(&data[relay_start..relay_start + 4]); + let relay_instructions_len = u32::from_le_bytes(relay_len_bytes) as usize; + + let relay_data_start = relay_start + 4; + if data.len() < relay_data_start + relay_instructions_len { + return Err(ExecutorQuoterError::InvalidInstructionData.into()); + } + + let relay_instructions = &data[relay_data_start..relay_data_start + relay_instructions_len]; + + // Parse relay instructions + let (gas_limit, msg_value) = + parse_relay_instructions(relay_instructions).map_err(relay_parse_error_to_program_error)?; + + // Calculate quote - returns u64 in SVM native decimals (lamports) + let required_payment = math::estimate_quote("e_body, &chain_info, gas_limit, msg_value)?; + + // Return data layout (72 bytes, all big-endian): + // - bytes 0-7: required_payment (u64) + // - bytes 8-39: payee_address (32 bytes) + // - bytes 40-71: quote_body (32 bytes, EQ01 format) + let mut return_data = [0u8; 72]; + return_data[0..8].copy_from_slice(&required_payment.to_be_bytes()); + return_data[8..40].copy_from_slice(&PAYEE_ADDRESS); + return_data[40..72].copy_from_slice("e_body.to_bytes32()); + + set_return_data(&return_data); + + Ok(()) +} diff --git a/svm/pinocchio/programs/executor-quoter/src/instructions/mod.rs b/svm/pinocchio/programs/executor-quoter/src/instructions/mod.rs new file mode 100644 index 0000000..ce05423 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/src/instructions/mod.rs @@ -0,0 +1,5 @@ +//! Instruction handlers for the ExecutorQuoter program. + +pub mod get_quote; +pub mod update_chain_info; +pub mod update_quote; diff --git a/svm/pinocchio/programs/executor-quoter/src/instructions/update_chain_info.rs b/svm/pinocchio/programs/executor-quoter/src/instructions/update_chain_info.rs new file mode 100644 index 0000000..e651ab3 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/src/instructions/update_chain_info.rs @@ -0,0 +1,137 @@ +use bytemuck::{Pod, Zeroable}; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, + pubkey::{find_program_address, Pubkey}, + ProgramResult, +}; + +use crate::{ + error::ExecutorQuoterError, + state::{ChainInfo, CHAIN_INFO_DISCRIMINATOR, CHAIN_INFO_SEED}, + UPDATER_ADDRESS, +}; + +/// Instruction data for UpdateChainInfo. +/// Field order matches ChainInfo account bytes 2-6 for direct copy_from_slice on updates. +#[repr(C)] +#[derive(Pod, Zeroable, Clone, Copy)] +pub struct UpdateChainInfoData { + pub chain_id: u16, + pub enabled: u8, + pub gas_price_decimals: u8, + pub native_decimals: u8, + pub _padding: u8, +} + +impl UpdateChainInfoData { + pub const LEN: usize = core::mem::size_of::(); +} + +/// Process the UpdateChainInfo instruction. +/// Creates or updates the ChainInfo PDA for a destination chain. +/// +/// Accounts: +/// 0. `[signer, writable]` payer - pays for account creation if needed +/// 1. `[signer]` updater - must match UPDATER_ADDRESS constant +/// 2. `[writable]` chain_info - ChainInfo PDA to create/update +/// 3. `[]` system_program - System program for account creation +pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + // Parse accounts + let [payer, updater, chain_info_account, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Validate signers + if !payer.is_signer() || !updater.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Validate instruction data length + if data.len() < UpdateChainInfoData::LEN { + return Err(ExecutorQuoterError::InvalidInstructionData.into()); + } + + // Validate updater against hardcoded address + if UPDATER_ADDRESS != *updater.key() { + return Err(ExecutorQuoterError::InvalidUpdater.into()); + } + + // Check if account needs to be created + if chain_info_account.data_is_empty() { + // Parse instruction data (only needed for creation) + let ix_data: UpdateChainInfoData = + bytemuck::try_pod_read_unaligned(&data[..UpdateChainInfoData::LEN]) + .map_err(|_| ExecutorQuoterError::InvalidInstructionData)?; + + // Derive canonical PDA and bump + let chain_id_bytes = ix_data.chain_id.to_le_bytes(); + let (expected_pda, canonical_bump) = + find_program_address(&[CHAIN_INFO_SEED, &chain_id_bytes], program_id); + + // Verify passed account matches expected PDA + if *chain_info_account.key() != expected_pda { + return Err(ExecutorQuoterError::InvalidPda.into()); + } + + // Create signer seeds with canonical bump + let bump_seed = [canonical_bump]; + let signer_seeds = [ + Seed::from(CHAIN_INFO_SEED), + Seed::from(chain_id_bytes.as_slice()), + Seed::from(&bump_seed), + ]; + let signers = [Signer::from(&signer_seeds[..])]; + + // Create account via CPI (handles pre-funded accounts to prevent griefing) + pinocchio_system::create_account_with_minimum_balance_signed( + chain_info_account, + ChainInfo::LEN, + program_id, + payer, + None, + &signers, + )?; + + // Initialize account data + let chain_info = ChainInfo { + discriminator: CHAIN_INFO_DISCRIMINATOR, + bump: canonical_bump, + chain_id: ix_data.chain_id, + enabled: ix_data.enabled, + gas_price_decimals: ix_data.gas_price_decimals, + native_decimals: ix_data.native_decimals, + _padding: 0, + }; + chain_info_account + .try_borrow_mut_data()? + .copy_from_slice(bytemuck::bytes_of(&chain_info)); + } else { + // Account exists - verify ownership + if chain_info_account.owner() != program_id { + return Err(ExecutorQuoterError::InvalidOwner.into()); + } + + // Update mutable fields directly via slice copy. + // Layout: enabled, gas_price_decimals, native_decimals are at + // bytes 4-7 in account data and bytes 2-5 in instruction data. + // Safety: owner check above guarantees correct size (only our program can + // create accounts it owns, and we always use ChainInfo::LEN). + let mut account_data = chain_info_account.try_borrow_mut_data()?; + + // Verify discriminator (first byte) + if account_data[0] != CHAIN_INFO_DISCRIMINATOR { + return Err(ExecutorQuoterError::InvalidDiscriminator.into()); + } + + // Verify chain_id matches (cannot change which chain this account is for) + if account_data[2..4] != data[0..2] { + return Err(ExecutorQuoterError::ChainIdMismatch.into()); + } + + account_data[4..7].copy_from_slice(&data[2..5]); + } + + Ok(()) +} diff --git a/svm/pinocchio/programs/executor-quoter/src/instructions/update_quote.rs b/svm/pinocchio/programs/executor-quoter/src/instructions/update_quote.rs new file mode 100644 index 0000000..89f0789 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/src/instructions/update_quote.rs @@ -0,0 +1,138 @@ +use bytemuck::{Pod, Zeroable}; +use pinocchio::{ + account_info::AccountInfo, + instruction::{Seed, Signer}, + program_error::ProgramError, + pubkey::{find_program_address, Pubkey}, + ProgramResult, +}; + +use crate::{ + error::ExecutorQuoterError, + state::{QuoteBody, QUOTE_BODY_DISCRIMINATOR, QUOTE_SEED}, + UPDATER_ADDRESS, +}; + +/// Instruction data for UpdateQuote +#[repr(C)] +#[derive(Pod, Zeroable, Clone, Copy)] +pub struct UpdateQuoteData { + pub chain_id: u16, + pub _padding: [u8; 6], + pub dst_price: u64, + pub src_price: u64, + pub dst_gas_price: u64, + pub base_fee: u64, +} + +impl UpdateQuoteData { + pub const LEN: usize = core::mem::size_of::(); +} + +/// Process the UpdateQuote instruction. +/// Creates or updates the QuoteBody PDA for a destination chain. +/// +/// Accounts: +/// 0. `[signer, writable]` payer - pays for account creation if needed +/// 1. `[signer]` updater - must match UPDATER_ADDRESS constant +/// 2. `[writable]` quote_body - QuoteBody PDA to create/update +/// 3. `[]` system_program - System program for account creation +pub fn process(program_id: &Pubkey, accounts: &[AccountInfo], data: &[u8]) -> ProgramResult { + // Parse accounts + let [payer, updater, quote_body_account, _system_program] = accounts else { + return Err(ProgramError::NotEnoughAccountKeys); + }; + + // Validate signers + if !payer.is_signer() || !updater.is_signer() { + return Err(ProgramError::MissingRequiredSignature); + } + + // Validate instruction data length + if data.len() < UpdateQuoteData::LEN { + return Err(ExecutorQuoterError::InvalidInstructionData.into()); + } + + // Validate updater against hardcoded address + if UPDATER_ADDRESS != *updater.key() { + return Err(ExecutorQuoterError::InvalidUpdater.into()); + } + + // Check if account needs to be created + if quote_body_account.data_is_empty() { + // Parse instruction data (only needed for creation) + let ix_data: UpdateQuoteData = + bytemuck::try_pod_read_unaligned(&data[..UpdateQuoteData::LEN]) + .map_err(|_| ExecutorQuoterError::InvalidInstructionData)?; + + // Derive canonical PDA and bump + let chain_id_bytes = ix_data.chain_id.to_le_bytes(); + let (expected_pda, canonical_bump) = + find_program_address(&[QUOTE_SEED, &chain_id_bytes], program_id); + + // Verify passed account matches expected PDA + if *quote_body_account.key() != expected_pda { + return Err(ExecutorQuoterError::InvalidPda.into()); + } + + // Create signer seeds with canonical bump + let bump_seed = [canonical_bump]; + let signer_seeds = [ + Seed::from(QUOTE_SEED), + Seed::from(chain_id_bytes.as_slice()), + Seed::from(&bump_seed), + ]; + let signers = [Signer::from(&signer_seeds[..])]; + + // Create account via CPI (handles pre-funded accounts to prevent griefing) + pinocchio_system::create_account_with_minimum_balance_signed( + quote_body_account, + QuoteBody::LEN, + program_id, + payer, + None, + &signers, + )?; + + // Initialize account data + let quote_body = QuoteBody { + discriminator: QUOTE_BODY_DISCRIMINATOR, + bump: canonical_bump, + chain_id: ix_data.chain_id, + _padding: [0u8; 4], + dst_price: ix_data.dst_price, + src_price: ix_data.src_price, + dst_gas_price: ix_data.dst_gas_price, + base_fee: ix_data.base_fee, + }; + quote_body_account + .try_borrow_mut_data()? + .copy_from_slice(bytemuck::bytes_of("e_body)); + } else { + // Account exists - verify ownership + if quote_body_account.owner() != program_id { + return Err(ExecutorQuoterError::InvalidOwner.into()); + } + + // Update pricing fields directly via slice copy. + // Layout: dst_price, src_price, dst_gas_price, base_fee are at bytes 8-40 + // in both instruction data and account data. + // Safety: owner check above guarantees correct size (only our program can + // create accounts it owns, and we always use QuoteBody::LEN). + let mut account_data = quote_body_account.try_borrow_mut_data()?; + + // Verify discriminator (first byte) + if account_data[0] != QUOTE_BODY_DISCRIMINATOR { + return Err(ExecutorQuoterError::InvalidDiscriminator.into()); + } + + // Verify chain_id matches (instruction bytes 0..2 vs account bytes 2..4) + if data[0..2] != account_data[2..4] { + return Err(ExecutorQuoterError::ChainIdMismatch.into()); + } + + account_data[8..40].copy_from_slice(&data[8..40]); + } + + Ok(()) +} diff --git a/svm/pinocchio/programs/executor-quoter/src/lib.rs b/svm/pinocchio/programs/executor-quoter/src/lib.rs new file mode 100644 index 0000000..d5b15a9 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/src/lib.rs @@ -0,0 +1,108 @@ +#![no_std] + +use pinocchio::{ + account_info::AccountInfo, default_allocator, nostd_panic_handler, program_entrypoint, + program_error::ProgramError, pubkey::Pubkey, ProgramResult, +}; + +// Use program_entrypoint to declare the entrypoint +// The MAX_TX_ACCOUNTS default handles any account count +program_entrypoint!(process_instruction); +default_allocator!(); +nostd_panic_handler!(); + +pub mod error; +pub mod instructions; +pub mod math; +pub mod state; + +use instructions::*; + +/// Program ID - replace with actual deployed address +pub static ID: Pubkey = [ + 0x0d, 0xf8, 0xc4, 0xd6, 0x7b, 0x42, 0x89, 0xd6, 0x3e, 0xf0, 0x63, 0x1b, 0x5d, 0x0c, 0x39, 0x18, + 0x2e, 0x8c, 0x9a, 0x4f, 0x7f, 0x9d, 0x8a, 0x3b, 0x6c, 0x5e, 0x4d, 0x3c, 0x2b, 0x1a, 0x09, 0xf8, +]; + +// ============================================================================= +// Build-time Configuration +// Set via environment variables: QUOTER_UPDATER_PUBKEY, QUOTER_PAYEE_PUBKEY +// ============================================================================= + +/// Decimals of the source chain native token (SOL = 9) +pub const SRC_TOKEN_DECIMALS: u8 = 9; + +/// Address authorized to update quotes and chain info. +/// Set at build time via QUOTER_UPDATER_PUBKEY env var (base58 pubkey). +pub const UPDATER_ADDRESS: Pubkey = include!(concat!(env!("OUT_DIR"), "/updater_address.rs")); + +/// Universal address for the payee (receives execution fees). +/// Set at build time via QUOTER_PAYEE_PUBKEY env var (base58 pubkey). +pub const PAYEE_ADDRESS: [u8; 32] = include!(concat!(env!("OUT_DIR"), "/payee_address.rs")); + +/// Instruction discriminators +#[repr(u8)] +pub enum Instruction { + /// Update chain info for a destination chain + /// Accounts: [payer, updater, chain_info, system_program] + UpdateChainInfo = 0, + /// Update quote for a destination chain + /// Accounts: [payer, updater, quote_body, system_program] + UpdateQuote = 1, + /// Request a quote for cross-chain execution + /// Accounts: [_config, chain_info, quote_body] + RequestQuote = 2, + /// Request execution quote with full details + /// Accounts: [_config, chain_info, quote_body, event_cpi] + RequestExecutionQuote = 3, +} + +impl TryFrom for Instruction { + type Error = ProgramError; + + fn try_from(value: u8) -> Result { + match value { + 0 => Ok(Instruction::UpdateChainInfo), + 1 => Ok(Instruction::UpdateQuote), + 2 => Ok(Instruction::RequestQuote), + 3 => Ok(Instruction::RequestExecutionQuote), + _ => Err(ProgramError::InvalidInstructionData), + } + } +} + +pub fn process_instruction( + program_id: &Pubkey, + accounts: &[AccountInfo], + instruction_data: &[u8], +) -> ProgramResult { + if instruction_data.is_empty() { + return Err(ProgramError::InvalidInstructionData); + } + + let instruction_id = instruction_data[0]; + + // Admin instructions (0, 1): 1-byte discriminator for minimal tx size + // CPI instructions (2, 3): 8-byte discriminator for Anchor compatibility + let data = match instruction_id { + 0 | 1 => &instruction_data[1..], + 2 | 3 => { + if instruction_data.len() < 8 { + return Err(ProgramError::InvalidInstructionData); + } + &instruction_data[8..] + } + _ => return Err(ProgramError::InvalidInstructionData), + }; + + let instruction = Instruction::try_from(instruction_id)?; + + match instruction { + Instruction::UpdateChainInfo => update_chain_info::process(program_id, accounts, data), + Instruction::UpdateQuote => update_quote::process(program_id, accounts, data), + Instruction::RequestQuote => get_quote::process_request_quote(program_id, accounts, data), + Instruction::RequestExecutionQuote => { + get_quote::process_request_execution_quote(program_id, accounts, data) + } + } +} diff --git a/svm/pinocchio/programs/executor-quoter/src/math/mod.rs b/svm/pinocchio/programs/executor-quoter/src/math/mod.rs new file mode 100644 index 0000000..597c3b9 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/src/math/mod.rs @@ -0,0 +1,351 @@ +//! Math utilities for quote calculation. +//! +//! This module provides U256 arithmetic and decimal normalization functions +//! needed to calculate cross-chain execution quotes. + +mod u256; + +pub use u256::{hi_lo, LoHi, U256}; + +use crate::error::ExecutorQuoterError; +use crate::state::{ChainInfo, QuoteBody}; +use pinocchio::program_error::ProgramError; + +/// Quote decimals (prices stored with 10^10 precision) +pub const QUOTE_DECIMALS: u8 = 10; + +/// SVM decimal resolution for output (SOL = 9 decimals) +pub const SVM_DECIMAL_RESOLUTION: u8 = 9; + +/// EVM decimal resolution for intermediate calculations (10^18) +pub const EVM_DECIMAL_RESOLUTION: u8 = 18; + +/// Precomputed powers of 10 for efficiency. +/// Index i contains 10^i. Supports up to 10^32 for max decimal precision. +const POW10: [u128; 33] = [ + 1, + 10, + 100, + 1_000, + 10_000, + 100_000, + 1_000_000, + 10_000_000, + 100_000_000, + 1_000_000_000, + 10_000_000_000, + 100_000_000_000, + 1_000_000_000_000, + 10_000_000_000_000, + 100_000_000_000_000, + 1_000_000_000_000_000, + 10_000_000_000_000_000, + 100_000_000_000_000_000, + 1_000_000_000_000_000_000, + 10_000_000_000_000_000_000, + 100_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000, + 10_000_000_000_000_000_000_000, + 100_000_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000, + 10_000_000_000_000_000_000_000_000, + 100_000_000_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000_000, + 10_000_000_000_000_000_000_000_000_000, + 100_000_000_000_000_000_000_000_000_000, + 1_000_000_000_000_000_000_000_000_000_000, + 10_000_000_000_000_000_000_000_000_000_000, + 100_000_000_000_000_000_000_000_000_000_000, +]; + +/// Returns 10^exp as U256. +/// Max supported exp is 32. +#[inline] +pub fn pow10(exp: u8) -> U256 { + debug_assert!(exp <= 32, "pow10: exp must be <= 32"); + U256::from_u128(POW10[exp as usize]) +} + +/// Normalize an amount from one decimal precision to another. +/// Equivalent to EVM: `normalize(amount, from, to)` +/// +/// If `from > to`: divides by 10^(from-to) (truncates) +/// If `from < to`: multiplies by 10^(to-from) +/// If `from == to`: returns amount unchanged +/// +/// Returns None on overflow. +#[inline] +pub fn normalize(amount: U256, from: u8, to: u8) -> Option { + match from.cmp(&to) { + core::cmp::Ordering::Greater => { + let divisor = pow10(from - to); + amount.checked_div(divisor) + } + core::cmp::Ordering::Less => { + let multiplier = pow10(to - from); + amount.checked_mul(multiplier) + } + core::cmp::Ordering::Equal => Some(amount), + } +} + +/// Multiply two values and divide by 10^decimals (truncates). +/// Equivalent to EVM: `mul(a, b, decimals) = (a * b) / 10^decimals` +/// +/// Returns None on overflow. +#[inline] +pub fn mul_decimals(a: U256, b: U256, decimals: u8) -> Option { + let product = a.checked_mul(b)?; + let divisor = pow10(decimals); + product.checked_div(divisor) +} + +/// Divide a by b with decimal scaling (truncates). +/// Equivalent to EVM: `div(a, b, decimals) = (a * 10^decimals) / b` +/// +/// Returns None on overflow or division by zero. +#[inline] +pub fn div_decimals(a: U256, b: U256, decimals: u8) -> Option { + let scaled = a.checked_mul(pow10(decimals))?; + scaled.checked_div(b) +} + +/// Estimate the quote for cross-chain execution. +/// Returns the required payment in SVM native token decimals (lamports for SOL). +/// +/// # Arguments +/// * `quote_body` - Quote body containing prices and fees +/// * `chain_info` - Chain info containing decimal configurations +/// * `gas_limit` - Total gas limit from relay instructions +/// * `msg_value` - Total message value from relay instructions +/// +/// # Returns +/// The required payment as u64 in SVM native decimals (lamports). +/// +/// # Errors +/// Returns `MathOverflow` on arithmetic overflow or division by zero. +pub fn estimate_quote( + quote_body: &QuoteBody, + chain_info: &ChainInfo, + gas_limit: u128, + msg_value: u128, +) -> Result { + let overflow_err = || -> ProgramError { ExecutorQuoterError::MathOverflow.into() }; + + let total_u256 = estimate_quote_u256(quote_body, chain_info, gas_limit, msg_value)?; + + // Convert from EVM_DECIMAL_RESOLUTION to SVM_DECIMAL_RESOLUTION + let result = normalize(total_u256, EVM_DECIMAL_RESOLUTION, SVM_DECIMAL_RESOLUTION) + .ok_or_else(overflow_err)?; + + // Convert to u64 (should fit for reasonable quote values) + result + .try_into_u64() + .ok_or_else(|| ExecutorQuoterError::MathOverflow.into()) +} + +fn estimate_quote_u256( + quote_body: &QuoteBody, + chain_info: &ChainInfo, + gas_limit: u128, + msg_value: u128, +) -> Result { + let base_fee = quote_body.base_fee; + let src_price = quote_body.src_price; + let dst_price = quote_body.dst_price; + let dst_gas_price = quote_body.dst_gas_price; + let dst_gas_price_decimals = chain_info.gas_price_decimals; + let dst_native_decimals = chain_info.native_decimals; + let overflow_err = || -> ProgramError { ExecutorQuoterError::MathOverflow.into() }; + + // 1. Base fee conversion: normalize from QUOTE_DECIMALS to EVM_DECIMAL_RESOLUTION + let src_chain_value_for_base_fee = normalize( + U256::from_u64(base_fee), + QUOTE_DECIMALS, + EVM_DECIMAL_RESOLUTION, + ) + .ok_or_else(overflow_err)?; + + // 2. Price ratio calculation + let n_src_price = normalize( + U256::from_u64(src_price), + QUOTE_DECIMALS, + EVM_DECIMAL_RESOLUTION, + ) + .ok_or_else(overflow_err)?; + + let n_dst_price = normalize( + U256::from_u64(dst_price), + QUOTE_DECIMALS, + EVM_DECIMAL_RESOLUTION, + ) + .ok_or_else(overflow_err)?; + + // Avoid division by zero + if n_src_price.is_zero() { + return Err(ExecutorQuoterError::MathOverflow.into()); + } + + let scaled_conversion = + div_decimals(n_dst_price, n_src_price, EVM_DECIMAL_RESOLUTION).ok_or_else(overflow_err)?; + + // 3. Gas limit cost calculation + let gas_cost = U256::from_u128(gas_limit) + .checked_mul(U256::from_u64(dst_gas_price)) + .ok_or_else(overflow_err)?; + let n_gas_limit_cost = normalize(gas_cost, dst_gas_price_decimals, EVM_DECIMAL_RESOLUTION) + .ok_or_else(overflow_err)?; + let src_chain_value_for_gas_limit = + mul_decimals(n_gas_limit_cost, scaled_conversion, EVM_DECIMAL_RESOLUTION) + .ok_or_else(overflow_err)?; + + // 4. Message value conversion + let n_msg_value = normalize( + U256::from_u128(msg_value), + dst_native_decimals, + EVM_DECIMAL_RESOLUTION, + ) + .ok_or_else(overflow_err)?; + let src_chain_value_for_msg_value = + mul_decimals(n_msg_value, scaled_conversion, EVM_DECIMAL_RESOLUTION) + .ok_or_else(overflow_err)?; + + // 5. Sum all components (all in EVM_DECIMAL_RESOLUTION scale) + src_chain_value_for_base_fee + .checked_add(src_chain_value_for_gas_limit) + .ok_or_else(overflow_err)? + .checked_add(src_chain_value_for_msg_value) + .ok_or_else(overflow_err) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_quote_body( + base_fee: u64, + src_price: u64, + dst_price: u64, + dst_gas_price: u64, + ) -> QuoteBody { + QuoteBody { + discriminator: 0, + bump: 0, + chain_id: 1, + _padding: [0; 4], + dst_price, + src_price, + dst_gas_price, + base_fee, + } + } + + fn make_chain_info(gas_price_decimals: u8, native_decimals: u8) -> ChainInfo { + ChainInfo { + discriminator: 0, + enabled: 1, + chain_id: 1, + gas_price_decimals, + native_decimals, + bump: 0, + _padding: 0, + } + } + + #[test] + fn test_estimate_quote_eth_to_sol() { + let quote_body = make_quote_body( + 100, // base_fee + 2650000000, // src_price (SOL ~$265) + 160000000, // dst_price (ETH ~$16 - test values) + 399146, // dst_gas_price + ); + let chain_info = make_chain_info(15, 18); // gas_price_decimals, native_decimals (ETH) + + let result_18 = estimate_quote_u256("e_body, &chain_info, 250000, 0).unwrap(); + + // Result is in lamports (9 decimals). Convert back to 18 decimals for comparison. + let expected_18 = U256::from_u64(6034845283018u64); + // Allow for truncation: result should be within 10^9 of expected + assert!(result_18.checked_sub(expected_18).is_some()); + assert!(result_18 + .checked_add(U256::from_u64(1_000_000_000 - 1)) + .unwrap() + .checked_sub(expected_18) + .is_some()); + } + + #[test] + fn test_estimate_quote_with_msg_value() { + let quote_body = make_quote_body(100, 2650000000, 160000000, 399146); + let chain_info = make_chain_info(15, 18); + + let result_18 = estimate_quote_u256( + "e_body, + &chain_info, + 250000, + 1_000_000_000_000_000_000, // 1 ETH in wei + ) + .unwrap(); + + let expected_18 = U256::from_u64(60383393335849055); + assert!(result_18.checked_sub(expected_18).is_some()); + assert!(result_18 + .checked_add(U256::from_u64(1_000_000_000 - 1)) + .unwrap() + .checked_sub(expected_18) + .is_some()); + } + + #[test] + fn test_estimate_quote_zero_gas_limit() { + let quote_body = make_quote_body(100, 2650000000, 160000000, 399146); + let chain_info = make_chain_info(15, 18); + + let result = estimate_quote("e_body, &chain_info, 0, 0).unwrap(); + + // base_fee = 100 at QUOTE_DECIMALS (10) + // Converted to 9 decimals = 10 lamports + assert_eq!(result, 10); + } + + #[test] + fn test_estimate_quote_zero_src_price() { + let quote_body = make_quote_body(100, 0, 160000000, 399146); // zero src_price + let chain_info = make_chain_info(15, 18); + + let result = estimate_quote("e_body, &chain_info, 250000, 0); + + assert!(result.is_err()); + } + + #[test] + fn test_estimate_quote_overflow_returns_error() { + let quote_body = make_quote_body( + u64::MAX, // max base_fee + 1, // tiny src_price (makes conversion huge) + u64::MAX, // max dst_price + u64::MAX, // max gas_price + ); + let chain_info = make_chain_info(0, 0); // no decimal scaling (makes values larger) + + let result = estimate_quote("e_body, &chain_info, u128::MAX, u128::MAX); + + assert!(result.is_err()); + } + + #[test] + fn test_u256_checked_operations_return_none_on_overflow() { + let max = U256::new(u128::MAX, u128::MAX); + let one = U256::from_u64(1); + + assert!(max.checked_add(one).is_none()); + + let zero = U256::from_u64(0); + assert!(zero.checked_sub(one).is_none()); + + assert!(max.checked_mul(U256::from_u64(2)).is_none()); + + assert!(one.checked_div(zero).is_none()); + } +} diff --git a/svm/pinocchio/programs/executor-quoter/src/math/u256.rs b/svm/pinocchio/programs/executor-quoter/src/math/u256.rs new file mode 100644 index 0000000..a1684e4 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/src/math/u256.rs @@ -0,0 +1,614 @@ +// cSpell:ignore Muldiv qhat rhat + +//! 256-bit unsigned integer implementation. +//! +//! Ported from Orca Whirlpool's U256Muldiv implementation. +//! Original: https://github.com/orca-so/whirlpools/blob/main/programs/whirlpool/src/math/u256_math.rs +//! +//! Modified for no_std compatibility. + +use core::cmp::Ordering; + +const NUM_WORDS: usize = 4; +const U64_MAX: u128 = u64::MAX as u128; +const U64_RESOLUTION: u32 = 64; + +/// A 256-bit unsigned integer represented as 4 x 64-bit words. +/// Words are stored in little-endian order (items[0] is least significant). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct U256 { + pub items: [u64; NUM_WORDS], +} + +impl U256 { + /// Creates a new U256 from two 128-bit halves. + /// `h` is the high 128 bits, `l` is the low 128 bits. + pub fn new(h: u128, l: u128) -> Self { + U256 { + items: [l.lo(), l.hi(), h.lo(), h.hi()], + } + } + + /// Creates a U256 from a u64 value. + #[inline] + pub fn from_u64(v: u64) -> Self { + Self::new(0, v as u128) + } + + /// Creates a U256 from a u128 value. + #[inline] + pub fn from_u128(v: u128) -> Self { + Self::new(0, v) + } + + fn copy(&self) -> Self { + let mut items: [u64; NUM_WORDS] = [0; NUM_WORDS]; + items.copy_from_slice(&self.items); + U256 { items } + } + + fn update_word(&mut self, index: usize, value: u64) { + self.items[index] = value; + } + + fn num_words(&self) -> usize { + for i in (0..self.items.len()).rev() { + if self.items[i] != 0 { + return i + 1; + } + } + 0 + } + + /// Gets the word at the given index. + pub fn get_word(&self, index: usize) -> u64 { + self.items[index] + } + + /// Gets the word at the given index as u128. + pub fn get_word_u128(&self, index: usize) -> u128 { + self.items[index] as u128 + } + + /// Converts to big-endian byte array (32 bytes). + /// Most significant byte first, matching EVM uint256 representation. + pub fn to_be_bytes(&self) -> [u8; 32] { + let mut result = [0u8; 32]; + // items[3] is most significant, items[0] is least significant + result[0..8].copy_from_slice(&self.items[3].to_be_bytes()); + result[8..16].copy_from_slice(&self.items[2].to_be_bytes()); + result[16..24].copy_from_slice(&self.items[1].to_be_bytes()); + result[24..32].copy_from_slice(&self.items[0].to_be_bytes()); + result + } + + /// Logical-left shift by one word (64 bits). + pub fn shift_word_left(&self) -> Self { + let mut result = U256::new(0, 0); + for i in (0..NUM_WORDS - 1).rev() { + result.items[i + 1] = self.items[i]; + } + result + } + + /// Logical-left shift by arbitrary amount. + pub fn shift_left(&self, mut shift_amount: u32) -> Self { + if shift_amount >= U64_RESOLUTION * (NUM_WORDS as u32) { + return U256::new(0, 0); + } + + let mut result = self.copy(); + + while shift_amount >= U64_RESOLUTION { + result = result.shift_word_left(); + shift_amount -= U64_RESOLUTION; + } + + if shift_amount == 0 { + return result; + } + + for i in (1..NUM_WORDS).rev() { + result.items[i] = (result.items[i] << shift_amount) + | (result.items[i - 1] >> (U64_RESOLUTION - shift_amount)); + } + + result.items[0] <<= shift_amount; + result + } + + /// Logical-right shift by one word (64 bits). + pub fn shift_word_right(&self) -> Self { + let mut result = U256::new(0, 0); + for i in 0..NUM_WORDS - 1 { + result.items[i] = self.items[i + 1] + } + result + } + + /// Logical-right shift by arbitrary amount. + pub fn shift_right(&self, mut shift_amount: u32) -> Self { + if shift_amount >= U64_RESOLUTION * (NUM_WORDS as u32) { + return U256::new(0, 0); + } + + let mut result = self.copy(); + + while shift_amount >= U64_RESOLUTION { + result = result.shift_word_right(); + shift_amount -= U64_RESOLUTION; + } + + if shift_amount == 0 { + return result; + } + + for i in 0..NUM_WORDS - 1 { + result.items[i] = (result.items[i] >> shift_amount) + | (result.items[i + 1] << (U64_RESOLUTION - shift_amount)); + } + + result.items[3] >>= shift_amount; + result + } + + /// Equality comparison. + #[allow(clippy::should_implement_trait)] + pub fn eq(&self, other: U256) -> bool { + for i in 0..self.items.len() { + if self.items[i] != other.items[i] { + return false; + } + } + true + } + + /// Less than comparison. + pub fn lt(&self, other: U256) -> bool { + for i in (0..self.items.len()).rev() { + match self.items[i].cmp(&other.items[i]) { + Ordering::Less => return true, + Ordering::Greater => return false, + Ordering::Equal => {} + } + } + false + } + + /// Greater than comparison. + pub fn gt(&self, other: U256) -> bool { + for i in (0..self.items.len()).rev() { + match self.items[i].cmp(&other.items[i]) { + Ordering::Less => return false, + Ordering::Greater => return true, + Ordering::Equal => {} + } + } + false + } + + /// Less than or equal comparison. + pub fn lte(&self, other: U256) -> bool { + for i in (0..self.items.len()).rev() { + match self.items[i].cmp(&other.items[i]) { + Ordering::Less => return true, + Ordering::Greater => return false, + Ordering::Equal => {} + } + } + true + } + + /// Greater than or equal comparison. + pub fn gte(&self, other: U256) -> bool { + for i in (0..self.items.len()).rev() { + match self.items[i].cmp(&other.items[i]) { + Ordering::Less => return false, + Ordering::Greater => return true, + Ordering::Equal => {} + } + } + true + } + + /// Try to convert to u128. Returns None if value exceeds u128::MAX. + pub fn try_into_u128(&self) -> Option { + if self.num_words() > 2 { + return None; + } + Some(((self.items[1] as u128) << U64_RESOLUTION) | (self.items[0] as u128)) + } + + /// Try to convert to u64. Returns None if value exceeds u64::MAX. + pub fn try_into_u64(&self) -> Option { + if self.num_words() > 1 { + return None; + } + Some(self.items[0]) + } + + /// Returns true if this value is zero. + pub fn is_zero(self) -> bool { + for i in 0..NUM_WORDS { + if self.items[i] != 0 { + return false; + } + } + true + } + + /// Checked addition. Returns None on overflow. + pub fn checked_add(&self, other: U256) -> Option { + let mut result = U256::new(0, 0); + + let mut carry = 0u128; + for i in 0..NUM_WORDS { + let x = self.get_word_u128(i); + let y = other.get_word_u128(i); + let t = x + y + carry; + result.update_word(i, t.lo()); + carry = t.hi_u128(); + } + + // If there's remaining carry, we overflowed + if carry != 0 { + return None; + } + + Some(result) + } + + /// Checked subtraction. Returns None on underflow. + pub fn checked_sub(&self, other: U256) -> Option { + // Check if self < other (would underflow) + if self.lt(other) { + return None; + } + + let mut result = U256::new(0, 0); + + let mut carry = 0u64; + for i in 0..NUM_WORDS { + let x = self.get_word(i); + let y = other.get_word(i); + let (t0, overflowing0) = x.overflowing_sub(y); + let (t1, overflowing1) = t0.overflowing_sub(carry); + result.update_word(i, t1); + carry = if overflowing0 || overflowing1 { 1 } else { 0 }; + } + + Some(result) + } + + /// Checked multiplication. Returns None on overflow. + pub fn checked_mul(&self, other: U256) -> Option { + let mut result = U256::new(0, 0); + + let m = self.num_words(); + let n = other.num_words(); + + // Quick overflow check: if sum of word counts > NUM_WORDS, likely overflow + // (not guaranteed, but catches obvious cases early) + if m + n > NUM_WORDS + 1 { + return None; + } + + for j in 0..n { + let mut k = 0u128; + for i in 0..m { + let x = self.get_word_u128(i); + let y = other.get_word_u128(j); + if i + j < NUM_WORDS { + let z = result.get_word_u128(i + j); + let t = x * y + z + k; + result.update_word(i + j, t.lo()); + k = t.hi_u128(); + } else if x * y != 0 { + // Would write beyond NUM_WORDS with non-zero value + return None; + } + } + + if j + m < NUM_WORDS { + result.update_word(j + m, k as u64); + } else if k != 0 { + // Carry would overflow + return None; + } + } + + Some(result) + } + + /// Checked division (truncates toward zero, like Solidity). + /// Returns None on division by zero. + pub fn checked_div(&self, mut divisor: U256) -> Option { + let mut dividend = self.copy(); + let mut quotient = U256::new(0, 0); + + let num_dividend_words = dividend.num_words(); + let num_divisor_words = divisor.num_words(); + + if num_divisor_words == 0 { + // Division by zero + return None; + } + + // Case 0: Dividend is 0 + if num_dividend_words == 0 { + return Some(U256::new(0, 0)); + } + + // Case 1: Dividend < divisor + if num_dividend_words < num_divisor_words { + return Some(U256::new(0, 0)); + } + + // Case 2: Both fit in u128 + if num_dividend_words < 3 { + let dividend_u128 = dividend.try_into_u128().unwrap(); + let divisor_u128 = divisor.try_into_u128().unwrap(); + let quotient_u128 = dividend_u128 / divisor_u128; + return Some(U256::new(0, quotient_u128)); + } + + // Case 3: Single-word divisor + if num_divisor_words == 1 { + let mut k = 0u128; + for j in (0..num_dividend_words).rev() { + let d1 = hi_lo(k.lo(), dividend.get_word(j)); + let d2 = divisor.get_word_u128(0); + let q = d1 / d2; + k = d1 - d2 * q; + quotient.update_word(j, q.lo()); + } + return Some(quotient); + } + + // Normalize the division by shifting left + let s = divisor.get_word(num_divisor_words - 1).leading_zeros(); + let b = dividend.get_word(num_dividend_words - 1).leading_zeros(); + + let mut dividend_carry_space: u64 = 0; + if num_dividend_words == NUM_WORDS && b < s { + dividend_carry_space = dividend.items[num_dividend_words - 1] >> (U64_RESOLUTION - s); + } + dividend = dividend.shift_left(s); + divisor = divisor.shift_left(s); + + for j in (0..num_dividend_words - num_divisor_words + 1).rev() { + let result = div_loop( + j, + num_divisor_words, + dividend, + &mut dividend_carry_space, + divisor, + quotient, + ); + quotient = result.0; + dividend = result.1; + } + + Some(quotient) + } +} + +/// Trait for extracting high/low parts of u128. +pub trait LoHi { + fn lo(self) -> u64; + fn hi(self) -> u64; + fn lo_u128(self) -> u128; + fn hi_u128(self) -> u128; +} + +impl LoHi for u128 { + #[inline] + fn lo(self) -> u64 { + (self & U64_MAX) as u64 + } + + #[inline] + fn lo_u128(self) -> u128 { + self & U64_MAX + } + + #[inline] + fn hi(self) -> u64 { + (self >> U64_RESOLUTION) as u64 + } + + #[inline] + fn hi_u128(self) -> u128 { + self >> U64_RESOLUTION + } +} + +/// Combines high and low u64 into u128. +#[inline] +pub fn hi_lo(hi: u64, lo: u64) -> u128 { + ((hi as u128) << U64_RESOLUTION) | (lo as u128) +} + +/// Helper function for the division algorithm. +fn div_loop( + index: usize, + num_divisor_words: usize, + mut dividend: U256, + dividend_carry_space: &mut u64, + divisor: U256, + mut quotient: U256, +) -> (U256, U256) { + let use_carry = (index + num_divisor_words) == NUM_WORDS; + let div_hi = if use_carry { + *dividend_carry_space + } else { + dividend.get_word(index + num_divisor_words) + }; + let d0 = hi_lo(div_hi, dividend.get_word(index + num_divisor_words - 1)); + let d1 = divisor.get_word_u128(num_divisor_words - 1); + + let mut qhat = d0 / d1; + let mut rhat = d0 - d1 * qhat; + + let d0_2 = dividend.get_word(index + num_divisor_words - 2); + let d1_2 = divisor.get_word_u128(num_divisor_words - 2); + + let mut cmp1 = hi_lo(rhat.lo(), d0_2); + let mut cmp2 = qhat.wrapping_mul(d1_2); + + while qhat.hi() != 0 || cmp2 > cmp1 { + qhat -= 1; + rhat += d1; + if rhat.hi() != 0 { + break; + } + + cmp1 = hi_lo(rhat.lo(), cmp1.lo()); + cmp2 -= d1_2; + } + + let mut k = 0; + let mut t; + for i in 0..num_divisor_words { + let p = qhat * (divisor.get_word_u128(i)); + t = (dividend.get_word_u128(index + i)) + .wrapping_sub(k) + .wrapping_sub(p.lo_u128()); + dividend.update_word(index + i, t.lo()); + k = ((p >> U64_RESOLUTION) as u64).wrapping_sub((t >> U64_RESOLUTION) as u64) as u128; + } + + let d_head = if use_carry { + *dividend_carry_space as u128 + } else { + dividend.get_word_u128(index + num_divisor_words) + }; + + t = d_head.wrapping_sub(k); + if use_carry { + *dividend_carry_space = t.lo(); + } else { + dividend.update_word(index + num_divisor_words, t.lo()); + } + + if k > d_head { + qhat -= 1; + k = 0; + for i in 0..num_divisor_words { + t = dividend + .get_word_u128(index + i) + .wrapping_add(divisor.get_word_u128(i)) + .wrapping_add(k); + dividend.update_word(index + i, t.lo()); + k = t >> U64_RESOLUTION; + } + + let new_carry = dividend + .get_word_u128(index + num_divisor_words) + .wrapping_add(k) + .lo(); + if use_carry { + *dividend_carry_space = new_carry + } else { + dividend.update_word( + index + num_divisor_words, + dividend + .get_word_u128(index + num_divisor_words) + .wrapping_add(k) + .lo(), + ); + } + } + + quotient.update_word(index, qhat.lo()); + + (quotient, dividend) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_basic_operations() { + let a = U256::from_u128(100); + let b = U256::from_u128(50); + + // Addition + let sum = a.checked_add(b).unwrap(); + assert_eq!(sum.try_into_u128(), Some(150)); + + // Subtraction + let diff = a.checked_sub(b).unwrap(); + assert_eq!(diff.try_into_u128(), Some(50)); + + // Multiplication + let prod = a.checked_mul(b).unwrap(); + assert_eq!(prod.try_into_u128(), Some(5000)); + + // Division + let quot = a.checked_div(b).unwrap(); + assert_eq!(quot.try_into_u128(), Some(2)); + } + + #[test] + fn test_large_multiplication() { + // Test u128 * u128 that exceeds u128 but fits in U256 + let a = U256::from_u128(u128::MAX); + let b = U256::from_u128(2); + let prod = a.checked_mul(b).unwrap(); + + // Result should be > u128::MAX but still valid U256 + assert!(prod.try_into_u128().is_none()); + assert!(prod.gt(U256::from_u128(u128::MAX))); + } + + #[test] + fn test_division_truncates() { + // Verify division truncates (rounds toward zero) like Solidity + let a = U256::from_u128(100); + let b = U256::from_u128(30); + + let quot = a.checked_div(b).unwrap(); + // 100 / 30 = 3.333... truncates to 3 + assert_eq!(quot.try_into_u128(), Some(3)); + } + + #[test] + fn test_division_by_zero() { + let a = U256::from_u128(100); + let b = U256::from_u128(0); + + assert!(a.checked_div(b).is_none()); + } + + #[test] + fn test_overflow_errors() { + let max = U256::new(u128::MAX, u128::MAX); + let one = U256::from_u64(1); + + // Addition overflow + assert!(max.checked_add(one).is_none()); + + // Subtraction underflow + let zero = U256::from_u64(0); + assert!(zero.checked_sub(one).is_none()); + + // Multiplication overflow + assert!(max.checked_mul(U256::from_u64(2)).is_none()); + } + + #[test] + fn test_pow10_values() { + use super::super::pow10; + + assert_eq!(pow10(0).try_into_u64(), Some(1)); + assert_eq!(pow10(1).try_into_u64(), Some(10)); + assert_eq!(pow10(9).try_into_u64(), Some(1_000_000_000)); + assert_eq!(pow10(18).try_into_u64(), Some(1_000_000_000_000_000_000)); + assert_eq!( + pow10(32).try_into_u128(), + Some(100_000_000_000_000_000_000_000_000_000_000) + ); + } +} diff --git a/svm/pinocchio/programs/executor-quoter/src/state.rs b/svm/pinocchio/programs/executor-quoter/src/state.rs new file mode 100644 index 0000000..c878ba9 --- /dev/null +++ b/svm/pinocchio/programs/executor-quoter/src/state.rs @@ -0,0 +1,126 @@ +use bytemuck::{Pod, Zeroable}; +use pinocchio::{account_info::AccountInfo, program_error::ProgramError, pubkey::Pubkey}; + +use crate::error::ExecutorQuoterError; + +/// Account discriminators for type safety. +pub const QUOTE_BODY_DISCRIMINATOR: u8 = 1; +pub const CHAIN_INFO_DISCRIMINATOR: u8 = 2; + +/// PDA seed prefixes +pub const QUOTE_SEED: &[u8] = b"quote"; +pub const CHAIN_INFO_SEED: &[u8] = b"chain_info"; + +/// Trait for accounts with a discriminator byte at offset 0. +pub trait Discriminator { + const DISCRIMINATOR: u8; +} + +/// On-chain quote body for a specific destination chain. +/// Mirrors the EVM OnChainQuoteBody struct. +/// PDA seeds: ["quote", chain_id (u16 le bytes)] +#[repr(C)] +#[derive(Pod, Zeroable, Clone, Copy, Debug, PartialEq)] +pub struct QuoteBody { + /// Discriminator for account type validation + pub discriminator: u8, + /// PDA bump seed + pub bump: u8, + /// The destination chain ID this quote applies to + pub chain_id: u16, + /// Padding + pub _padding: [u8; 4], + /// The USD price, in 10^10, of the destination chain native currency + pub dst_price: u64, + /// The USD price, in 10^10, of the source chain native currency + pub src_price: u64, + /// The current gas price on the destination chain + pub dst_gas_price: u64, + /// The base fee, in source chain native currency, required by the quoter + pub base_fee: u64, +} + +impl Discriminator for QuoteBody { + const DISCRIMINATOR: u8 = QUOTE_BODY_DISCRIMINATOR; +} + +impl QuoteBody { + pub const LEN: usize = core::mem::size_of::(); + + /// Pack the quote body into a bytes32 representation (EQ01 format). + /// Layout (32 bytes): + /// - bytes 0-7: base_fee (u64 be) + /// - bytes 8-15: dst_gas_price (u64 be) + /// - bytes 16-23: src_price (u64 be) + /// - bytes 24-31: dst_price (u64 be) + pub fn to_bytes32(&self) -> [u8; 32] { + let mut result = [0u8; 32]; + result[0..8].copy_from_slice(&self.base_fee.to_be_bytes()); + result[8..16].copy_from_slice(&self.dst_gas_price.to_be_bytes()); + result[16..24].copy_from_slice(&self.src_price.to_be_bytes()); + result[24..32].copy_from_slice(&self.dst_price.to_be_bytes()); + result + } +} + +/// Chain-specific configuration. +/// PDA seeds: ["chain_info", chain_id (u16 le bytes)] +/// +/// Field order is optimized for efficient updates: mutable fields (chain_id, +/// enabled, gas_price_decimals, native_decimals) are contiguous at bytes 2-6, +/// matching the instruction data layout for direct copy_from_slice. +#[repr(C)] +#[derive(Pod, Zeroable, Clone, Copy, Debug, PartialEq)] +pub struct ChainInfo { + /// Discriminator for account type validation + pub discriminator: u8, + /// PDA bump seed + pub bump: u8, + /// The chain ID this info applies to + pub chain_id: u16, + /// Whether this chain is enabled for quoting + pub enabled: u8, + /// Decimals used for gas price on this chain + pub gas_price_decimals: u8, + /// Decimals of the native token on this chain + pub native_decimals: u8, + /// Padding + pub _padding: u8, +} + +impl Discriminator for ChainInfo { + const DISCRIMINATOR: u8 = CHAIN_INFO_DISCRIMINATOR; +} + +impl ChainInfo { + pub const LEN: usize = core::mem::size_of::(); + + pub fn is_enabled(&self) -> bool { + self.enabled == 1 + } +} + +/// Load a typed account from AccountInfo, validating ownership and discriminator. +/// Returns a copy of the account data. +pub fn load_account( + account: &AccountInfo, + program_id: &Pubkey, +) -> Result { + if account.owner() != program_id { + return Err(ExecutorQuoterError::InvalidOwner.into()); + } + + let data = account.try_borrow_data()?; + if data.len() < core::mem::size_of::() { + return Err(ProgramError::InvalidAccountData); + } + + // Check discriminator (first byte) + if data[0] != T::DISCRIMINATOR { + return Err(ExecutorQuoterError::InvalidDiscriminator.into()); + } + + let account = bytemuck::try_from_bytes::(&data[..core::mem::size_of::()]) + .map_err(|_| ProgramError::InvalidAccountData)?; + Ok(*account) +} diff --git a/svm/pinocchio/tests/executor-quoter-router-tests/Cargo.toml b/svm/pinocchio/tests/executor-quoter-router-tests/Cargo.toml new file mode 100644 index 0000000..6cfc64a --- /dev/null +++ b/svm/pinocchio/tests/executor-quoter-router-tests/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "executor-quoter-router-tests" +version = "0.1.0" +description = "Integration tests and benchmarks for executor-quoter-router" +edition = "2021" + +[lib] +name = "executor_quoter_router_tests" + +[features] +default = [] + +[dev-dependencies] +solana-program-test = "1.18" +solana-sdk = "1.18" +libsecp256k1 = "0.7" +rand = "0.8" +mollusk-svm = "0.0.9-solana-1.18" +mollusk-svm-bencher = "0.0.9-solana-1.18" +executor-requests = { path = "../../../modules/executor-requests" } + +[[bench]] +name = "compute_units" +harness = false diff --git a/svm/pinocchio/tests/executor-quoter-router-tests/benches/compute_units.rs b/svm/pinocchio/tests/executor-quoter-router-tests/benches/compute_units.rs new file mode 100644 index 0000000..eba2fb5 --- /dev/null +++ b/svm/pinocchio/tests/executor-quoter-router-tests/benches/compute_units.rs @@ -0,0 +1,265 @@ +//! Compute unit benchmarks for executor-quoter-router using mollusk-svm. +//! +//! Run with: cargo bench -p executor-quoter-router-tests +//! Output: target/benches/executor_quoter_router_compute_units.md + +use libsecp256k1::{Message, PublicKey, SecretKey}; +use mollusk_svm::program::keyed_account_for_system_program; +use mollusk_svm::Mollusk; +use mollusk_svm_bencher::MolluskComputeUnitBencher; +use solana_sdk::{ + account::AccountSharedData, + instruction::{AccountMeta, Instruction}, + keccak, + pubkey::Pubkey, + rent::Rent, + system_program, +}; + +/// Router Program ID - FgDLrWZ9avy9A4hNDLCvVUyh7knK9r2Ry4KgHX1y2aKS +const ROUTER_PROGRAM_ID: Pubkey = Pubkey::new_from_array([ + 0xda, 0x0f, 0x39, 0x58, 0xba, 0x11, 0x3d, 0xfa, 0x31, 0xe1, 0xda, 0xc7, 0x67, 0xe7, 0x47, 0xce, + 0xc9, 0x03, 0xf4, 0x56, 0x9c, 0x89, 0x97, 0x1f, 0x47, 0x27, 0x2e, 0xb0, 0x7e, 0x3d, 0xd5, 0xf9, +]); + +/// Quoter Program ID (matching executor-quoter) +const QUOTER_PROGRAM_ID: Pubkey = Pubkey::new_from_array([ + 0x58, 0xce, 0x85, 0x6b, 0x53, 0xca, 0x8b, 0x7d, 0xc9, 0xa3, 0x84, 0x42, 0x1c, 0x5c, 0xaf, 0x30, + 0x63, 0xcf, 0x30, 0x96, 0x2b, 0x4c, 0xf6, 0x0d, 0xad, 0x51, 0x9d, 0x3d, 0xcd, 0xf3, 0x86, 0x58, +]); + +// Account discriminators +const QUOTER_REGISTRATION_DISCRIMINATOR: u8 = 1; + +// PDA seeds +const QUOTER_REGISTRATION_SEED: &[u8] = b"quoter_registration"; + +// Account sizes +const QUOTER_REGISTRATION_SIZE: usize = 54; // 1 + 1 + 20 + 32 + +// Instruction discriminators +const IX_UPDATE_QUOTER_CONTRACT: u8 = 0; + +// Wormhole chain ID for Solana +const SOLANA_CHAIN_ID: u16 = 1; + +/// Helper to derive quoter registration PDA +fn derive_quoter_registration_pda(quoter_address: &[u8; 20]) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[QUOTER_REGISTRATION_SEED, "er_address[..]], + &ROUTER_PROGRAM_ID, + ) +} + +/// Secp256k1 quoter identity for testing. +struct QuoterIdentity { + secret_key: SecretKey, + eth_address: [u8; 20], +} + +impl QuoterIdentity { + /// Create a quoter identity from a fixed seed for deterministic tests. + fn from_seed(seed: [u8; 32]) -> Self { + let secret_key = SecretKey::parse(&seed).expect("valid seed"); + let public_key = PublicKey::from_secret_key(&secret_key); + + // Derive Ethereum address: keccak256(pubkey)[12:32] + let pubkey_bytes = public_key.serialize(); + let pubkey_hash = keccak::hash(&pubkey_bytes[1..65]); + let mut eth_address = [0u8; 20]; + eth_address.copy_from_slice(&pubkey_hash.0[12..32]); + + Self { + secret_key, + eth_address, + } + } + + /// Sign a message and return (r, s, v). + fn sign(&self, message: &[u8]) -> ([u8; 32], [u8; 32], u8) { + let message_hash = keccak::hash(message); + let message = Message::parse_slice(&message_hash.0).expect("valid message hash"); + + let (signature, recovery_id) = libsecp256k1::sign(&message, &self.secret_key); + let sig_bytes = signature.serialize(); + + let mut r = [0u8; 32]; + let mut s = [0u8; 32]; + r.copy_from_slice(&sig_bytes[0..32]); + s.copy_from_slice(&sig_bytes[32..64]); + + // EVM uses v = 27 or 28, recovery_id is 0 or 1 + let v = recovery_id.serialize() + 27; + + (r, s, v) + } +} + +/// Build a valid EG01 governance message with proper signature. +fn build_signed_governance_message( + chain_id: u16, + quoter: &QuoterIdentity, + implementation_program_id: &Pubkey, + sender: &Pubkey, + expiry_time: u64, +) -> Vec { + // Build the message body (bytes 0-98) that will be signed + let mut body = Vec::with_capacity(98); + body.extend_from_slice(b"EG01"); + body.extend_from_slice(&chain_id.to_be_bytes()); + body.extend_from_slice("er.eth_address); + body.extend_from_slice(implementation_program_id.as_ref()); // universal_contract_address + body.extend_from_slice(sender.as_ref()); // universal_sender_address + body.extend_from_slice(&expiry_time.to_be_bytes()); + + // Sign the body + let (r, s, v) = quoter.sign(&body); + + // Build the full message + let mut data = Vec::with_capacity(163); + data.extend_from_slice(&body); + data.extend_from_slice(&r); + data.extend_from_slice(&s); + data.push(v); + + data +} + +/// Build UpdateQuoterContract instruction data with proper signature +fn build_signed_update_quoter_contract_data( + chain_id: u16, + quoter: &QuoterIdentity, + implementation_program_id: &Pubkey, + sender: &Pubkey, + expiry_time: u64, +) -> Vec { + let mut data = Vec::with_capacity(1 + 163); + data.push(IX_UPDATE_QUOTER_CONTRACT); + data.extend(build_signed_governance_message( + chain_id, + quoter, + implementation_program_id, + sender, + expiry_time, + )); + data +} + +/// Create a funded payer account +fn create_payer_account() -> AccountSharedData { + AccountSharedData::new(1_000_000_000, 0, &system_program::ID) +} + +/// Create a signer account +fn create_signer_account() -> AccountSharedData { + AccountSharedData::new(0, 0, &system_program::ID) +} + +/// Create a placeholder config account (config is not used, just required in account list) +fn create_config_account() -> AccountSharedData { + AccountSharedData::new(0, 0, &system_program::ID) +} + +/// Create an initialized QuoterRegistration account +fn create_quoter_registration_account( + bump: u8, + quoter_address: &[u8; 20], + implementation_program_id: &Pubkey, +) -> AccountSharedData { + let rent = Rent::default(); + let lamports = rent.minimum_balance(QUOTER_REGISTRATION_SIZE); + let mut data = vec![0u8; QUOTER_REGISTRATION_SIZE]; + + data[0] = QUOTER_REGISTRATION_DISCRIMINATOR; + data[1] = bump; + data[2..22].copy_from_slice(quoter_address); + data[22..54].copy_from_slice(implementation_program_id.as_ref()); + + let mut account = + AccountSharedData::new(lamports, QUOTER_REGISTRATION_SIZE, &ROUTER_PROGRAM_ID); + account.set_data_from_slice(&data); + account +} + +fn main() { + // Initialize Mollusk with the program + let mollusk = Mollusk::new(&ROUTER_PROGRAM_ID, "executor_quoter_router"); + + // Get the system program keyed account for CPI + let system_program_account = keyed_account_for_system_program(); + + // Set up accounts + let payer = Pubkey::new_unique(); + let sender = Pubkey::new_unique(); + let config_pda = Pubkey::new_unique(); // placeholder, not used + + // Create a deterministic quoter identity + let quoter = QuoterIdentity::from_seed([1u8; 32]); + let (quoter_registration_pda, quoter_registration_bump) = + derive_quoter_registration_pda("er.eth_address); + + // Far future expiry time + let expiry_time = u64::MAX; + + // Build UpdateQuoterContract instruction (create new registration) + let update_quoter_contract_create_ix = Instruction::new_with_bytes( + ROUTER_PROGRAM_ID, + &build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + "ER_PROGRAM_ID, + &sender, + expiry_time, + ), + vec![ + AccountMeta::new(payer, true), + AccountMeta::new_readonly(sender, true), + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + ); + + let update_quoter_contract_create_accounts = vec![ + (payer, create_payer_account()), + (sender, create_signer_account()), + (config_pda, create_config_account()), + ( + quoter_registration_pda, + AccountSharedData::new(0, 0, &system_program::ID), + ), + system_program_account.clone(), + ]; + + // Build UpdateQuoterContract instruction (update existing registration) + let update_quoter_contract_update_accounts = vec![ + (payer, create_payer_account()), + (sender, create_signer_account()), + (config_pda, create_config_account()), + ( + quoter_registration_pda, + create_quoter_registration_account( + quoter_registration_bump, + "er.eth_address, + "ER_PROGRAM_ID, + ), + ), + system_program_account, + ]; + + // Run benchmarks + MolluskComputeUnitBencher::new(mollusk) + .bench(( + "update_quoter_contract_create", + &update_quoter_contract_create_ix, + &update_quoter_contract_create_accounts, + )) + .bench(( + "update_quoter_contract_update", + &update_quoter_contract_create_ix, + &update_quoter_contract_update_accounts, + )) + .must_pass(true) + .out_dir("target/benches") + .execute(); +} diff --git a/svm/pinocchio/tests/executor-quoter-router-tests/src/lib.rs b/svm/pinocchio/tests/executor-quoter-router-tests/src/lib.rs new file mode 100644 index 0000000..494acaa --- /dev/null +++ b/svm/pinocchio/tests/executor-quoter-router-tests/src/lib.rs @@ -0,0 +1 @@ +// Placeholder lib for test crate diff --git a/svm/pinocchio/tests/executor-quoter-router-tests/tests/integration.rs b/svm/pinocchio/tests/executor-quoter-router-tests/tests/integration.rs new file mode 100644 index 0000000..e59d367 --- /dev/null +++ b/svm/pinocchio/tests/executor-quoter-router-tests/tests/integration.rs @@ -0,0 +1,2762 @@ +//! Integration tests for executor-quoter-router using solana-program-test with BPF. +//! +//! These tests require both the executor-quoter-router and executor-quoter BPF binaries. + +use libsecp256k1::{Message, PublicKey, SecretKey}; +use rand::rngs::OsRng; +use solana_program_test::{tokio, ProgramTest, ProgramTestBanksClientExt}; +use solana_sdk::{ + compute_budget::ComputeBudgetInstruction, + instruction::{AccountMeta, Instruction}, + keccak, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_program, + transaction::Transaction, +}; + +/// Router Program ID - FgDLrWZ9avy9A4hNDLCvVUyh7knK9r2Ry4KgHX1y2aKS +const ROUTER_PROGRAM_ID: Pubkey = Pubkey::new_from_array([ + 0xda, 0x0f, 0x39, 0x58, 0xba, 0x11, 0x3d, 0xfa, 0x31, 0xe1, 0xda, 0xc7, 0x67, 0xe7, 0x47, 0xce, + 0xc9, 0x03, 0xf4, 0x56, 0x9c, 0x89, 0x97, 0x1f, 0x47, 0x27, 0x2e, 0xb0, 0x7e, 0x3d, 0xd5, 0xf9, +]); + +/// Quoter Program ID (matching executor-quoter) +const QUOTER_PROGRAM_ID: Pubkey = Pubkey::new_from_array([ + 0x58, 0xce, 0x85, 0x6b, 0x53, 0xca, 0x8b, 0x7d, 0xc9, 0xa3, 0x84, 0x42, 0x1c, 0x5c, 0xaf, 0x30, + 0x63, 0xcf, 0x30, 0x96, 0x2b, 0x4c, 0xf6, 0x0d, 0xad, 0x51, 0x9d, 0x3d, 0xcd, 0xf3, 0x86, 0x58, +]); + +/// Executor Program ID - execXUrAsMnqMmTHj5m7N1YQgsDz3cwGLYCYyuDRciV +const EXECUTOR_PROGRAM_ID: Pubkey = Pubkey::new_from_array([ + 0x09, 0xb9, 0x69, 0x71, 0x58, 0x3b, 0x59, 0x03, 0xe0, 0x28, 0x1d, 0xa9, 0x65, 0x48, 0xd5, 0xd2, + 0x3c, 0x65, 0x1f, 0x7a, 0x9c, 0xcd, 0xe3, 0xea, 0xd5, 0x2b, 0x42, 0xf6, 0xb7, 0xda, 0xc2, 0xd2, +]); + +// Account discriminators (updated - no config) +const QUOTER_REGISTRATION_DISCRIMINATOR: u8 = 1; + +// PDA seeds +const QUOTER_REGISTRATION_SEED: &[u8] = b"quoter_registration"; + +// Account sizes +const QUOTER_REGISTRATION_SIZE: usize = 54; // 1 + 1 + 20 + 32 + +// Instruction discriminators (updated - no initialize) +const IX_UPDATE_QUOTER_CONTRACT: u8 = 0; +const IX_QUOTE_EXECUTION: u8 = 1; +const IX_REQUEST_EXECUTION: u8 = 2; + +// Wormhole chain ID for Solana +const SOLANA_CHAIN_ID: u16 = 1; + +/// Helper to get a dummy config pubkey (not used by program but required in instruction) +fn get_dummy_config_pubkey() -> Pubkey { + Pubkey::new_unique() +} + +/// Helper to derive quoter registration PDA +fn derive_quoter_registration_pda(quoter_address: &[u8; 20]) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[QUOTER_REGISTRATION_SEED, "er_address[..]], + &ROUTER_PROGRAM_ID, + ) +} + +// Note: build_initialize_data removed - program no longer has Initialize instruction + +/// Secp256k1 quoter identity for testing. +/// Contains the secret key and derived Ethereum address. +struct QuoterIdentity { + secret_key: SecretKey, + eth_address: [u8; 20], +} + +impl QuoterIdentity { + /// Create a new random quoter identity. + fn new() -> Self { + let secret_key = SecretKey::random(&mut OsRng); + let public_key = PublicKey::from_secret_key(&secret_key); + + // Derive Ethereum address: keccak256(pubkey)[12:32] + // libsecp256k1 public key is 65 bytes with 0x04 prefix, we need the 64 bytes after + let pubkey_bytes = public_key.serialize(); + let pubkey_hash = keccak::hash(&pubkey_bytes[1..65]); + let mut eth_address = [0u8; 20]; + eth_address.copy_from_slice(&pubkey_hash.0[12..32]); + + Self { + secret_key, + eth_address, + } + } + + /// Sign a message and return (r, s, v). + fn sign(&self, message: &[u8]) -> ([u8; 32], [u8; 32], u8) { + let message_hash = keccak::hash(message); + let message = Message::parse_slice(&message_hash.0).expect("valid message hash"); + + let (signature, recovery_id) = libsecp256k1::sign(&message, &self.secret_key); + let sig_bytes = signature.serialize(); + + let mut r = [0u8; 32]; + let mut s = [0u8; 32]; + r.copy_from_slice(&sig_bytes[0..32]); + s.copy_from_slice(&sig_bytes[32..64]); + + // EVM uses v = 27 or 28, recovery_id is 0 or 1 + let v = recovery_id.serialize() + 27; + + (r, s, v) + } +} + +/// Build a valid EG01 governance message with proper signature. +fn build_signed_governance_message( + chain_id: u16, + quoter: &QuoterIdentity, + implementation_program_id: &Pubkey, + sender: &Pubkey, + expiry_time: u64, +) -> Vec { + // Build the message body (bytes 0-98) that will be signed + let mut body = Vec::with_capacity(98); + body.extend_from_slice(b"EG01"); + body.extend_from_slice(&chain_id.to_be_bytes()); + body.extend_from_slice("er.eth_address); + body.extend_from_slice(implementation_program_id.as_ref()); // universal_contract_address + body.extend_from_slice(sender.as_ref()); // universal_sender_address + body.extend_from_slice(&expiry_time.to_be_bytes()); + + assert_eq!(body.len(), 98, "Governance body should be 98 bytes"); + + // Sign the body + let (r, s, v) = quoter.sign(&body); + + // Build the full message + let mut data = Vec::with_capacity(163); + data.extend_from_slice(&body); + data.extend_from_slice(&r); + data.extend_from_slice(&s); + data.push(v); + + assert_eq!(data.len(), 163, "Governance message should be 163 bytes"); + data +} + +/// Build a valid EG01 governance message for testing (unsigned, for negative tests) +fn build_governance_message( + chain_id: u16, + quoter_address: &[u8; 20], + implementation_program_id: &Pubkey, + sender: &Pubkey, + expiry_time: u64, +) -> Vec { + let mut data = Vec::with_capacity(163); + data.extend_from_slice(b"EG01"); + data.extend_from_slice(&chain_id.to_be_bytes()); + data.extend_from_slice(quoter_address); + data.extend_from_slice(implementation_program_id.as_ref()); // universal_contract_address + data.extend_from_slice(sender.as_ref()); // universal_sender_address + data.extend_from_slice(&expiry_time.to_be_bytes()); + // signature_r (32 bytes) + data.extend_from_slice(&[0u8; 32]); + // signature_s (32 bytes) + data.extend_from_slice(&[0u8; 32]); + // signature_v (1 byte) + data.push(0); + data +} + +/// Build UpdateQuoterContract instruction data with proper signature +fn build_signed_update_quoter_contract_data( + chain_id: u16, + quoter: &QuoterIdentity, + implementation_program_id: &Pubkey, + sender: &Pubkey, + expiry_time: u64, +) -> Vec { + let mut data = Vec::with_capacity(1 + 163); + data.push(IX_UPDATE_QUOTER_CONTRACT); + data.extend(build_signed_governance_message( + chain_id, + quoter, + implementation_program_id, + sender, + expiry_time, + )); + data +} + +/// Build UpdateQuoterContract instruction data (unsigned, for negative tests) +fn build_update_quoter_contract_data( + chain_id: u16, + quoter_address: &[u8; 20], + implementation_program_id: &Pubkey, + sender: &Pubkey, + expiry_time: u64, +) -> Vec { + let mut data = Vec::with_capacity(1 + 163); + data.push(IX_UPDATE_QUOTER_CONTRACT); + data.extend(build_governance_message( + chain_id, + quoter_address, + implementation_program_id, + sender, + expiry_time, + )); + data +} + +/// Setup ProgramTest with router program +fn setup_program_test() -> ProgramTest { + let mut pt = ProgramTest::default(); + + // Add router program + pt.add_program( + "executor_quoter_router", + ROUTER_PROGRAM_ID, + None, // BPF loaded from target/deploy + ); + + // Force BPF execution for pinocchio programs + pt.prefer_bpf(true); + + pt +} + +/// Setup ProgramTest with both router and quoter programs +#[allow(dead_code)] +fn setup_program_test_with_quoter() -> ProgramTest { + let mut pt = ProgramTest::default(); + + // Add router program + pt.add_program( + "executor_quoter_router", + ROUTER_PROGRAM_ID, + None, // BPF loaded from target/deploy + ); + + // Add quoter program for CPI testing + pt.add_program("executor_quoter", QUOTER_PROGRAM_ID, None); + + // Force BPF execution for pinocchio programs + pt.prefer_bpf(true); + + pt +} + +/// Setup ProgramTest with router, quoter, and executor programs +fn setup_program_test_full() -> ProgramTest { + let mut pt = ProgramTest::default(); + + // Add router program + pt.add_program("executor_quoter_router", ROUTER_PROGRAM_ID, None); + + // Add quoter program for CPI testing + pt.add_program("executor_quoter", QUOTER_PROGRAM_ID, None); + + // Add executor program for full flow testing + pt.add_program("executor", EXECUTOR_PROGRAM_ID, None); + + // Force BPF execution for pinocchio programs + pt.prefer_bpf(true); + + pt +} + +#[tokio::test] +async fn test_update_quoter_contract() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Create a secp256k1 quoter identity + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + + // Create sender keypair (must match universal_sender_address) + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Build signed UpdateQuoterContract instruction + let expiry_time = u64::MAX; + + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + "ER_PROGRAM_ID, + &sender.pubkey(), + expiry_time, + ); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(sender.pubkey(), true), // sender + AccountMeta::new_readonly(config_pubkey, false), // config + AccountMeta::new(quoter_registration_pda, false), // quoter_registration + AccountMeta::new_readonly(system_program::ID, false), // system_program + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!(result.is_ok(), "UpdateQuoterContract failed: {:?}", result); + + // Verify quoter registration was created + let registration_account = banks_client + .get_account(quoter_registration_pda) + .await + .unwrap(); + assert!( + registration_account.is_some(), + "QuoterRegistration account not created" + ); + + let reg_data = registration_account.unwrap().data; + assert_eq!(reg_data.len(), QUOTER_REGISTRATION_SIZE); + assert_eq!(reg_data[0], QUOTER_REGISTRATION_DISCRIMINATOR); + + // Verify quoter_address (at offset 4 after discriminator, bump, padding) + let stored_quoter_addr: [u8; 20] = reg_data[2..22].try_into().unwrap(); + assert_eq!(stored_quoter_addr, quoter.eth_address); + + // Verify implementation_program_id (at offset 24) + let stored_impl: [u8; 32] = reg_data[22..54].try_into().unwrap(); + assert_eq!(stored_impl, QUOTER_PROGRAM_ID.to_bytes()); +} + +#[tokio::test] +async fn test_update_quoter_contract_wrong_chain_fails() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Try to register quoter with wrong chain ID (Ethereum = 2) + let quoter_address: [u8; 20] = [0xAB; 20]; + let (quoter_registration_pda, quoter_bump) = derive_quoter_registration_pda("er_address); + + let sender = Keypair::new(); + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Use Ethereum chain ID (2) instead of Solana (1) + let wrong_chain_id: u16 = 2; + let ix_data = build_update_quoter_contract_data( + wrong_chain_id, + "er_address, + "ER_PROGRAM_ID, + &sender.pubkey(), + u64::MAX, + ); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!(result.is_err(), "Should fail with wrong chain ID"); +} + +#[tokio::test] +async fn test_update_quoter_contract_expired_fails() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Try to register quoter with expired timestamp + let quoter_address: [u8; 20] = [0xAB; 20]; + let (quoter_registration_pda, quoter_bump) = derive_quoter_registration_pda("er_address); + + let sender = Keypair::new(); + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Use an already-expired timestamp + let expired_time: u64 = 1; // Jan 1, 1970 + 1 second + let ix_data = build_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er_address, + "ER_PROGRAM_ID, + &sender.pubkey(), + expired_time, + ); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "Should fail with expired governance message" + ); +} + +#[tokio::test] +async fn test_update_quoter_contract_wrong_sender_fails() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Try to register with mismatched sender + let quoter_address: [u8; 20] = [0xAB; 20]; + let (quoter_registration_pda, quoter_bump) = derive_quoter_registration_pda("er_address); + + let sender = Keypair::new(); + let different_sender = Keypair::new(); + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Build message with different_sender's pubkey but sign with sender + let ix_data = build_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er_address, + "ER_PROGRAM_ID, + &different_sender.pubkey(), // Mismatch! + u64::MAX, + ); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), // Actual signer + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!(result.is_err(), "Should fail with sender mismatch"); +} + +#[tokio::test] +async fn test_update_quoter_contract_update_existing() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Create a secp256k1 quoter identity + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + "ER_PROGRAM_ID, + &sender.pubkey(), + u64::MAX, + ); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Verify first registration + let reg_account = banks_client + .get_account(quoter_registration_pda) + .await + .unwrap() + .unwrap(); + let stored_impl: [u8; 32] = reg_account.data[22..54].try_into().unwrap(); + assert_eq!(stored_impl, QUOTER_PROGRAM_ID.to_bytes()); + + // Now update to a different implementation (same quoter signs for different implementation) + let new_implementation = Pubkey::new_from_array([0x99; 32]); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + &new_implementation, + &sender.pubkey(), + u64::MAX, + ); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_ok(), + "Update existing registration failed: {:?}", + result + ); + + // Verify updated registration + let reg_account = banks_client + .get_account(quoter_registration_pda) + .await + .unwrap() + .unwrap(); + let stored_impl: [u8; 32] = reg_account.data[22..54].try_into().unwrap(); + assert_eq!(stored_impl, new_implementation.to_bytes()); +} + +#[tokio::test] +async fn test_update_quoter_contract_bad_signature() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Try with unsigned governance message (all zeros signature) + let quoter_address: [u8; 20] = [0xAB; 20]; + let (quoter_registration_pda, quoter_bump) = derive_quoter_registration_pda("er_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // This uses the unsigned version (zeros for r, s, v) + let ix_data = build_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er_address, + "ER_PROGRAM_ID, + &sender.pubkey(), + u64::MAX, + ); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!(result.is_err(), "Should fail with bad signature"); +} + +#[tokio::test] +async fn test_update_quoter_contract_quoter_mismatch() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Create two different quoter identities + let quoter_alice = QuoterIdentity::new(); + let quoter_bob = QuoterIdentity::new(); + + // Try to register Alice's address but sign with Bob's key + // We need to manually construct this since our helper uses the same quoter for address and signing + let sender = Keypair::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er_alice.eth_address); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Build message body with Alice's address + let mut body = Vec::with_capacity(98); + body.extend_from_slice(b"EG01"); + body.extend_from_slice(&SOLANA_CHAIN_ID.to_be_bytes()); + body.extend_from_slice("er_alice.eth_address); // Alice's address + body.extend_from_slice(QUOTER_PROGRAM_ID.as_ref()); + body.extend_from_slice(sender.pubkey().as_ref()); + body.extend_from_slice(&u64::MAX.to_be_bytes()); + + // Sign with Bob's key + let (r, s, v) = quoter_bob.sign(&body); + + let mut gov_data = Vec::with_capacity(163); + gov_data.extend_from_slice(&body); + gov_data.extend_from_slice(&r); + gov_data.extend_from_slice(&s); + gov_data.push(v); + + let mut ix_data = Vec::with_capacity(1 + 163); + ix_data.push(IX_UPDATE_QUOTER_CONTRACT); + ix_data.extend(gov_data); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "Should fail with quoter mismatch (Alice's address, Bob's signature)" + ); +} + +#[tokio::test] +async fn test_update_quoter_contract_invalid_governance_prefix() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Try to register with invalid governance prefix + let quoter_address: [u8; 20] = [0xAB; 20]; + let (quoter_registration_pda, quoter_bump) = derive_quoter_registration_pda("er_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Build governance message with wrong prefix + let mut ix_data = Vec::with_capacity(1 + 163); + ix_data.push(IX_UPDATE_QUOTER_CONTRACT); + ix_data.extend_from_slice(b"BAD!"); // Wrong prefix (should be "EG01") + ix_data.extend_from_slice(&SOLANA_CHAIN_ID.to_be_bytes()); + ix_data.extend_from_slice("er_address); + ix_data.extend_from_slice(QUOTER_PROGRAM_ID.as_ref()); + ix_data.extend_from_slice(sender.pubkey().as_ref()); + ix_data.extend_from_slice(&u64::MAX.to_be_bytes()); + ix_data.extend_from_slice(&[0u8; 65]); // signature + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "Should fail with invalid governance prefix" + ); +} + +// ============================================================================ +// Ecrecover Unit Tests +// ============================================================================ +// +// These tests verify the secp256k1 signature verification works correctly +// by testing various scenarios with known keys and signatures. + +/// Test that a valid signature from a known key is accepted. +#[tokio::test] +async fn test_ecrecover_valid_signature() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Create multiple quoter identities and verify each one works + for i in 0..3 { + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + "ER_PROGRAM_ID, + &sender.pubkey(), + u64::MAX, + ); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_ok(), + "Quoter {} with valid signature should succeed: {:?}", + i, + result + ); + + // Verify the registration exists with correct data + let reg_account = banks_client + .get_account(quoter_registration_pda) + .await + .unwrap() + .unwrap(); + let stored_quoter_addr: [u8; 20] = reg_account.data[2..22].try_into().unwrap(); + assert_eq!( + stored_quoter_addr, quoter.eth_address, + "Stored quoter address should match" + ); + } +} + +/// Test that the same quoter can sign different messages (different implementations). +#[tokio::test] +async fn test_ecrecover_same_key_different_messages() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Same quoter, different implementations + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + // First registration + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let impl1 = Pubkey::new_from_array([0x11; 32]); + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + &impl1, + &sender.pubkey(), + u64::MAX, + ); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Verify first implementation + let reg_account = banks_client + .get_account(quoter_registration_pda) + .await + .unwrap() + .unwrap(); + let stored_impl: [u8; 32] = reg_account.data[22..54].try_into().unwrap(); + assert_eq!(stored_impl, impl1.to_bytes()); + + // Update to second implementation with new signature + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let impl2 = Pubkey::new_from_array([0x22; 32]); + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + &impl2, + &sender.pubkey(), + u64::MAX, + ); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Verify second implementation + let reg_account = banks_client + .get_account(quoter_registration_pda) + .await + .unwrap() + .unwrap(); + let stored_impl: [u8; 32] = reg_account.data[22..54].try_into().unwrap(); + assert_eq!(stored_impl, impl2.to_bytes()); +} + +/// Test that recovery_id (v) must be correct - wrong v value should fail. +#[tokio::test] +async fn test_ecrecover_wrong_recovery_id() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Build governance message body + let mut body = Vec::with_capacity(98); + body.extend_from_slice(b"EG01"); + body.extend_from_slice(&SOLANA_CHAIN_ID.to_be_bytes()); + body.extend_from_slice("er.eth_address); + body.extend_from_slice(QUOTER_PROGRAM_ID.as_ref()); + body.extend_from_slice(sender.pubkey().as_ref()); + body.extend_from_slice(&u64::MAX.to_be_bytes()); + + // Sign correctly + let (r, s, v) = quoter.sign(&body); + + // Flip the v value (if 27, make it 28; if 28, make it 27) + let wrong_v = if v == 27 { 28 } else { 27 }; + + // Build message with wrong v + let mut gov_data = Vec::with_capacity(163); + gov_data.extend_from_slice(&body); + gov_data.extend_from_slice(&r); + gov_data.extend_from_slice(&s); + gov_data.push(wrong_v); + + let mut ix_data = Vec::with_capacity(1 + 163); + ix_data.push(IX_UPDATE_QUOTER_CONTRACT); + ix_data.extend(gov_data); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "Wrong recovery_id (v) should fail signature verification" + ); +} + +/// Test that corrupted r value fails signature verification. +#[tokio::test] +async fn test_ecrecover_corrupted_r() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Build and sign + let mut body = Vec::with_capacity(98); + body.extend_from_slice(b"EG01"); + body.extend_from_slice(&SOLANA_CHAIN_ID.to_be_bytes()); + body.extend_from_slice("er.eth_address); + body.extend_from_slice(QUOTER_PROGRAM_ID.as_ref()); + body.extend_from_slice(sender.pubkey().as_ref()); + body.extend_from_slice(&u64::MAX.to_be_bytes()); + + let (mut r, s, v) = quoter.sign(&body); + + // Corrupt r by flipping a bit + r[0] ^= 0x01; + + let mut gov_data = Vec::with_capacity(163); + gov_data.extend_from_slice(&body); + gov_data.extend_from_slice(&r); + gov_data.extend_from_slice(&s); + gov_data.push(v); + + let mut ix_data = Vec::with_capacity(1 + 163); + ix_data.push(IX_UPDATE_QUOTER_CONTRACT); + ix_data.extend(gov_data); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!(result.is_err(), "Corrupted r should fail"); +} + +/// Test that corrupted s value fails signature verification. +#[tokio::test] +async fn test_ecrecover_corrupted_s() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Build and sign + let mut body = Vec::with_capacity(98); + body.extend_from_slice(b"EG01"); + body.extend_from_slice(&SOLANA_CHAIN_ID.to_be_bytes()); + body.extend_from_slice("er.eth_address); + body.extend_from_slice(QUOTER_PROGRAM_ID.as_ref()); + body.extend_from_slice(sender.pubkey().as_ref()); + body.extend_from_slice(&u64::MAX.to_be_bytes()); + + let (r, mut s, v) = quoter.sign(&body); + + // Corrupt s by flipping a bit + s[15] ^= 0xFF; + + let mut gov_data = Vec::with_capacity(163); + gov_data.extend_from_slice(&body); + gov_data.extend_from_slice(&r); + gov_data.extend_from_slice(&s); + gov_data.push(v); + + let mut ix_data = Vec::with_capacity(1 + 163); + ix_data.push(IX_UPDATE_QUOTER_CONTRACT); + ix_data.extend(gov_data); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!(result.is_err(), "Corrupted s should fail"); +} + +/// Test that modified message body fails (signature no longer valid). +#[tokio::test] +async fn test_ecrecover_modified_message() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Build original message + let mut body = Vec::with_capacity(98); + body.extend_from_slice(b"EG01"); + body.extend_from_slice(&SOLANA_CHAIN_ID.to_be_bytes()); + body.extend_from_slice("er.eth_address); + body.extend_from_slice(QUOTER_PROGRAM_ID.as_ref()); + body.extend_from_slice(sender.pubkey().as_ref()); + body.extend_from_slice(&u64::MAX.to_be_bytes()); + + // Sign the original message + let (r, s, v) = quoter.sign(&body); + + // Modify the message body AFTER signing (change implementation) + let mut modified_body = body.clone(); + modified_body[26] ^= 0x01; // Flip a bit in the implementation address + + // Build governance data with modified body but original signature + let mut gov_data = Vec::with_capacity(163); + gov_data.extend_from_slice(&modified_body); + gov_data.extend_from_slice(&r); + gov_data.extend_from_slice(&s); + gov_data.push(v); + + let mut ix_data = Vec::with_capacity(1 + 163); + ix_data.push(IX_UPDATE_QUOTER_CONTRACT); + ix_data.extend(gov_data); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "Modified message with original signature should fail" + ); +} + +/// Test with a known test vector to verify EVM compatibility. +/// This uses a deterministic key to ensure reproducible results. +#[tokio::test] +async fn test_ecrecover_deterministic_key() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Create quoter from deterministic secret key + let secret_bytes: [u8; 32] = [ + 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, + 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, + 0x1f, 0x20, + ]; + let secret_key = SecretKey::parse(&secret_bytes).expect("valid secret key"); + let public_key = PublicKey::from_secret_key(&secret_key); + + // Derive Ethereum address + let pubkey_bytes = public_key.serialize(); + let pubkey_hash = keccak::hash(&pubkey_bytes[1..65]); + let mut eth_address = [0u8; 20]; + eth_address.copy_from_slice(&pubkey_hash.0[12..32]); + + // Log the derived address for verification + // This can be compared with EVM ecrecover using the same key + let hex_chars: Vec = eth_address.iter().map(|b| format!("{:02x}", b)).collect(); + println!( + "Deterministic key Ethereum address: 0x{}", + hex_chars.join("") + ); + + let (quoter_registration_pda, quoter_bump) = derive_quoter_registration_pda(ð_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Build and sign governance message + let mut body = Vec::with_capacity(98); + body.extend_from_slice(b"EG01"); + body.extend_from_slice(&SOLANA_CHAIN_ID.to_be_bytes()); + body.extend_from_slice(ð_address); + body.extend_from_slice(QUOTER_PROGRAM_ID.as_ref()); + body.extend_from_slice(sender.pubkey().as_ref()); + body.extend_from_slice(&u64::MAX.to_be_bytes()); + + // Sign + let message_hash = keccak::hash(&body); + let message = Message::parse_slice(&message_hash.0).expect("valid message hash"); + let (signature, recovery_id) = libsecp256k1::sign(&message, &secret_key); + let sig_bytes = signature.serialize(); + + let mut r = [0u8; 32]; + let mut s = [0u8; 32]; + r.copy_from_slice(&sig_bytes[0..32]); + s.copy_from_slice(&sig_bytes[32..64]); + let v = recovery_id.serialize() + 27; + + let mut gov_data = Vec::with_capacity(163); + gov_data.extend_from_slice(&body); + gov_data.extend_from_slice(&r); + gov_data.extend_from_slice(&s); + gov_data.push(v); + + let mut ix_data = Vec::with_capacity(1 + 163); + ix_data.push(IX_UPDATE_QUOTER_CONTRACT); + ix_data.extend(gov_data); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_ok(), + "Deterministic key signature should succeed: {:?}", + result + ); + + // Verify registration + let reg_account = banks_client + .get_account(quoter_registration_pda) + .await + .unwrap() + .unwrap(); + let stored_quoter_addr: [u8; 20] = reg_account.data[2..22].try_into().unwrap(); + assert_eq!(stored_quoter_addr, eth_address); +} + +// ============================================================================ +// QuoteExecution and RequestExecution Tests +// ============================================================================ + +// Quoter PDA seeds +const QUOTER_CHAIN_INFO_SEED: &[u8] = b"chain_info"; +const QUOTER_QUOTE_SEED: &[u8] = b"quote"; + +/// Derive quoter chain_info PDA +fn derive_quoter_chain_info_pda(chain_id: u16) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[QUOTER_CHAIN_INFO_SEED, &chain_id.to_le_bytes()], + "ER_PROGRAM_ID, + ) +} + +/// Derive quoter quote_body PDA +fn derive_quoter_quote_body_pda(chain_id: u16) -> (Pubkey, u8) { + Pubkey::find_program_address( + &[QUOTER_QUOTE_SEED, &chain_id.to_le_bytes()], + "ER_PROGRAM_ID, + ) +} + +/// Quoter updater address (hardcoded in quoter program) - 9r6q2iEg4MBevjC8reaLmQUDxueF3vabUoqDkZ2LoAYe +const QUOTER_UPDATER_ADDRESS: Pubkey = Pubkey::new_from_array([ + 0x83, 0x71, 0x8b, 0x7e, 0xc8, 0x96, 0x17, 0xb7, 0x04, 0x06, 0x85, 0xe0, 0x1b, 0xdc, 0xca, 0x03, + 0x21, 0x40, 0x22, 0x98, 0x0d, 0xaa, 0xe9, 0x13, 0x40, 0xe0, 0xc3, 0xf8, 0x40, 0xc0, 0x05, 0xef, +]); + +/// Helper to get a dummy config pubkey for the quoter program (not used by program) +fn get_quoter_dummy_config_pubkey() -> Pubkey { + Pubkey::new_unique() +} + +/// Get the authorized updater keypair for quoter program. +/// Reads from QUOTER_UPDATER_KEYPAIR_PATH env var (path to JSON keypair file). +fn get_quoter_updater_keypair() -> Keypair { + let keypair_path = std::env::var("QUOTER_UPDATER_KEYPAIR_PATH").expect( + "QUOTER_UPDATER_KEYPAIR_PATH env var must be set to path of updater keypair JSON file", + ); + solana_sdk::signature::read_keypair_file(&keypair_path) + .expect("Failed to read updater keypair from file") +} + +// Quoter instruction discriminators +// Admin instructions (0, 1): 1-byte discriminator for minimal tx size +const QUOTER_IX_UPDATE_CHAIN_INFO: u8 = 0; +const QUOTER_IX_UPDATE_QUOTE: u8 = 1; +// Note: QUOTER_IX_REQUEST_QUOTE and QUOTER_IX_REQUEST_EXECUTION_QUOTE +// are defined above as IX_QUOTER_REQUEST_QUOTE and IX_QUOTER_REQUEST_EXECUTION_QUOTE + +/// Build relay instructions with gas limit and msg value (Type 1) +fn build_relay_instructions_gas(gas_limit: u128, msg_value: u128) -> Vec { + let mut data = Vec::with_capacity(33); + data.push(1); // IX_TYPE_GAS + data.extend_from_slice(&gas_limit.to_be_bytes()); + data.extend_from_slice(&msg_value.to_be_bytes()); + data +} + +/// Quoter discriminator for RequestQuote (8 bytes, Anchor-compatible) +/// Byte 0 = instruction ID, bytes 1-7 = padding (zeros) +const IX_QUOTER_REQUEST_QUOTE: [u8; 8] = [2, 0, 0, 0, 0, 0, 0, 0]; +/// Quoter discriminator for RequestExecutionQuote (8 bytes, Anchor-compatible) +/// Byte 0 = instruction ID, bytes 1-7 = padding (zeros) +const IX_QUOTER_REQUEST_EXECUTION_QUOTE: [u8; 8] = [3, 0, 0, 0, 0, 0, 0, 0]; + +/// Build QuoteExecution instruction data (zero-copy layout). +/// +/// Layout: +/// - router_discriminator (1) +/// - quoter_address (20) - for registration lookup +/// - quoter CPI data: +/// - quoter_discriminator (8) - Anchor-compatible, byte 0 = RequestQuote (2) +/// - dst_chain (2) +/// - dst_addr (32) +/// - refund_addr (32) +/// - request_bytes_len (4) +/// - request_bytes +/// - relay_instructions_len (4) +/// - relay_instructions +fn build_quote_execution_data( + quoter_address: &[u8; 20], + dst_chain: u16, + dst_addr: &[u8; 32], + refund_addr: &[u8; 32], + request_bytes: &[u8], + relay_instructions: &[u8], +) -> Vec { + let mut data = Vec::new(); + // Router discriminator + data.push(IX_QUOTE_EXECUTION); + // Quoter address for registration lookup + data.extend_from_slice(quoter_address); + // Quoter CPI data (passed directly to quoter) + data.extend_from_slice(&IX_QUOTER_REQUEST_QUOTE); // 8-byte quoter discriminator + data.extend_from_slice(&dst_chain.to_le_bytes()); + data.extend_from_slice(dst_addr); + data.extend_from_slice(refund_addr); + data.extend_from_slice(&(request_bytes.len() as u32).to_le_bytes()); + data.extend_from_slice(request_bytes); + data.extend_from_slice(&(relay_instructions.len() as u32).to_le_bytes()); + data.extend_from_slice(relay_instructions); + data +} + +/// Build RequestExecution instruction data (zero-copy layout). +/// +/// Layout: +/// - router_discriminator (1) +/// - amount (8) - payment amount +/// - quoter_address (20) - for registration lookup +/// - quoter CPI data: +/// - quoter_discriminator (8) - Anchor-compatible, byte 0 = RequestExecutionQuote (3) +/// - dst_chain (2) +/// - dst_addr (32) +/// - refund_addr (32) +/// - request_bytes_len (4) +/// - request_bytes +/// - relay_instructions_len (4) +/// - relay_instructions +fn build_request_execution_data( + quoter_address: &[u8; 20], + amount: u64, + dst_chain: u16, + dst_addr: &[u8; 32], + refund_addr: &[u8; 32], + request_bytes: &[u8], + relay_instructions: &[u8], +) -> Vec { + let mut data = Vec::new(); + // Router discriminator + data.push(IX_REQUEST_EXECUTION); + // Amount for payment + data.extend_from_slice(&amount.to_le_bytes()); + // Quoter address for registration lookup + data.extend_from_slice(quoter_address); + // Quoter CPI data (passed directly to quoter) + data.extend_from_slice(&IX_QUOTER_REQUEST_EXECUTION_QUOTE); // 8-byte quoter discriminator + data.extend_from_slice(&dst_chain.to_le_bytes()); + data.extend_from_slice(dst_addr); + data.extend_from_slice(refund_addr); + data.extend_from_slice(&(request_bytes.len() as u32).to_le_bytes()); + data.extend_from_slice(request_bytes); + data.extend_from_slice(&(relay_instructions.len() as u32).to_le_bytes()); + data.extend_from_slice(relay_instructions); + data +} + +/// Test destination chain ID +const DST_CHAIN_ID: u16 = 2; // Ethereum + +#[tokio::test] +async fn test_quote_execution() { + let pt = setup_program_test_with_quoter(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Register quoter + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + "ER_PROGRAM_ID, + &sender.pubkey(), + u64::MAX, + ); + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Setup quoter accounts (config, chain_info, quote_body) + // Note: We need to initialize the quoter program's accounts + // For this test, we'll use the quoter's initialize instruction + + let quoter_config_pubkey = get_quoter_dummy_config_pubkey(); + let (quoter_chain_info_pda, _quoter_chain_info_bump) = + derive_quoter_chain_info_pda(DST_CHAIN_ID); + let (quoter_quote_body_pda, _quoter_quote_body_bump) = + derive_quoter_quote_body_pda(DST_CHAIN_ID); + let updater = get_quoter_updater_keypair(); + + // Update chain info + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let (_, chain_info_bump) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let mut chain_info_data = Vec::new(); + chain_info_data.push(QUOTER_IX_UPDATE_CHAIN_INFO); // 1-byte discriminator + chain_info_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); + chain_info_data.push(1); // enabled + chain_info_data.push(9); // gas_price_decimals + chain_info_data.push(18); // native_decimals + chain_info_data.push(chain_info_bump); + chain_info_data.extend_from_slice(&[0u8; 2]); // padding + + let chain_info_ix = Instruction { + program_id: QUOTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), // updater + AccountMeta::new(quoter_chain_info_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: chain_info_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, chain_info_ix], + Some(&payer.pubkey()), + &[&payer, &updater], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Update quote + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let (_, quote_body_bump) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let mut quote_data = Vec::new(); + quote_data.push(QUOTER_IX_UPDATE_QUOTE); // 1-byte discriminator + quote_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); + quote_data.push(quote_body_bump); + quote_data.extend_from_slice(&[0u8; 5]); // padding + quote_data.extend_from_slice(&2000_0000000000u64.to_le_bytes()); // dst_price + quote_data.extend_from_slice(&200_0000000000u64.to_le_bytes()); // src_price + quote_data.extend_from_slice(&50_000000000u64.to_le_bytes()); // dst_gas_price (50 Gwei) + quote_data.extend_from_slice(&1000000u64.to_le_bytes()); // base_fee + + let quote_ix = Instruction { + program_id: QUOTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), // updater + AccountMeta::new(quoter_quote_body_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: quote_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, quote_ix], + Some(&payer.pubkey()), + &[&payer, &updater], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Now call QuoteExecution through the router + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let dst_addr = [0x01u8; 32]; + let refund_addr = [0x02u8; 32]; + let relay_instructions = build_relay_instructions_gas(200000, 0); + + let quote_ix_data = build_quote_execution_data( + "er.eth_address, + DST_CHAIN_ID, + &dst_addr, + &refund_addr, + &[], + &relay_instructions, + ); + + let quote_ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(quoter_registration_pda, false), + AccountMeta::new_readonly(QUOTER_PROGRAM_ID, false), + AccountMeta::new_readonly(quoter_config_pubkey, false), + AccountMeta::new_readonly(quoter_chain_info_pda, false), + AccountMeta::new_readonly(quoter_quote_body_pda, false), + ], + data: quote_ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, quote_ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!(result.is_ok(), "QuoteExecution failed: {:?}", result); +} + +#[tokio::test] +async fn test_quote_execution_quoter_not_registered() { + let pt = setup_program_test_with_quoter(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Register quoter A + let quoter_a = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er_a.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er_a, + "ER_PROGRAM_ID, + &sender.pubkey(), + u64::MAX, + ); + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Setup quoter accounts + let quoter_config_pubkey = get_quoter_dummy_config_pubkey(); + let (quoter_chain_info_pda, _) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let (quoter_quote_body_pda, _) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + + // Try to call QuoteExecution with a different quoter address (quoter B) + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let quoter_b = QuoterIdentity::new(); + let dst_addr = [0x01u8; 32]; + let refund_addr = [0x02u8; 32]; + let relay_instructions = build_relay_instructions_gas(200000, 0); + + // Use quoter_b's address but quoter_a's registration PDA + let quote_ix_data = build_quote_execution_data( + "er_b.eth_address, // Different quoter address! + DST_CHAIN_ID, + &dst_addr, + &refund_addr, + &[], + &relay_instructions, + ); + + let quote_ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(quoter_registration_pda, false), // quoter_a's registration + AccountMeta::new_readonly(QUOTER_PROGRAM_ID, false), + AccountMeta::new_readonly(quoter_config_pubkey, false), + AccountMeta::new_readonly(quoter_chain_info_pda, false), + AccountMeta::new_readonly(quoter_quote_body_pda, false), + ], + data: quote_ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, quote_ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "QuoteExecution should fail with mismatched quoter address" + ); +} + +#[tokio::test] +async fn test_quote_execution_chain_disabled() { + let pt = setup_program_test_with_quoter(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Register quoter + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + "ER_PROGRAM_ID, + &sender.pubkey(), + u64::MAX, + ); + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Setup quoter accounts but with chain DISABLED + let quoter_config_pubkey = get_quoter_dummy_config_pubkey(); + let (quoter_chain_info_pda, _) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let (quoter_quote_body_pda, _) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let updater = get_quoter_updater_keypair(); + + // Update chain info with enabled = FALSE + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let (_, chain_info_bump) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let mut chain_info_data = Vec::new(); + chain_info_data.push(QUOTER_IX_UPDATE_CHAIN_INFO); // 1-byte discriminator + chain_info_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); + chain_info_data.push(0); // enabled = FALSE (chain disabled) + chain_info_data.push(9); // gas_price_decimals + chain_info_data.push(18); // native_decimals + chain_info_data.push(chain_info_bump); + chain_info_data.extend_from_slice(&[0u8; 2]); // padding + + let chain_info_ix = Instruction { + program_id: QUOTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), // updater + AccountMeta::new(quoter_chain_info_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: chain_info_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, chain_info_ix], + Some(&payer.pubkey()), + &[&payer, &updater], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Update quote (needed even for disabled chain test) + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let (_, quote_body_bump) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let mut quote_data = Vec::new(); + quote_data.push(QUOTER_IX_UPDATE_QUOTE); // 1-byte discriminator + quote_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); + quote_data.push(quote_body_bump); + quote_data.extend_from_slice(&[0u8; 5]); // padding + quote_data.extend_from_slice(&2000_0000000000u64.to_le_bytes()); + quote_data.extend_from_slice(&200_0000000000u64.to_le_bytes()); + quote_data.extend_from_slice(&50_000000000u64.to_le_bytes()); + quote_data.extend_from_slice(&1000000u64.to_le_bytes()); + + let quote_ix = Instruction { + program_id: QUOTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), // updater + AccountMeta::new(quoter_quote_body_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: quote_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, quote_ix], + Some(&payer.pubkey()), + &[&payer, &updater], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Now call QuoteExecution - should fail because chain is disabled + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let dst_addr = [0x01u8; 32]; + let refund_addr = [0x02u8; 32]; + let relay_instructions = build_relay_instructions_gas(200000, 0); + + let quote_ix_data = build_quote_execution_data( + "er.eth_address, + DST_CHAIN_ID, + &dst_addr, + &refund_addr, + &[], + &relay_instructions, + ); + + let quote_ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(quoter_registration_pda, false), + AccountMeta::new_readonly(QUOTER_PROGRAM_ID, false), + AccountMeta::new_readonly(quoter_config_pubkey, false), + AccountMeta::new_readonly(quoter_chain_info_pda, false), + AccountMeta::new_readonly(quoter_quote_body_pda, false), + ], + data: quote_ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(300_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, quote_ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "QuoteExecution should fail when chain is disabled" + ); +} + +#[tokio::test] +async fn test_request_execution() { + let pt = setup_program_test_full(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Register quoter + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + "ER_PROGRAM_ID, + &sender.pubkey(), + u64::MAX, + ); + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Setup quoter accounts + let quoter_config_pubkey = get_quoter_dummy_config_pubkey(); + let (quoter_chain_info_pda, _) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let (quoter_quote_body_pda, _) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let updater = get_quoter_updater_keypair(); + + // Payee is the hardcoded PAYEE_ADDRESS in the quoter program + // This must match what the quoter returns via CPI (updater.pubkey() when built + // with QUOTER_PAYEE_PUBKEY=QUOTER_UPDATER_PUBKEY) + let payee_pubkey = updater.pubkey(); + + // Update chain info + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let (_, chain_info_bump) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let mut chain_info_data = Vec::new(); + chain_info_data.push(QUOTER_IX_UPDATE_CHAIN_INFO); // 1-byte discriminator + chain_info_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); + chain_info_data.push(1); // enabled + chain_info_data.push(12); // gas_price_decimals (matching EVM tests: 0x12 = 18, but the hex update shows 12) + chain_info_data.push(18); // native_decimals (ETH = 18) + chain_info_data.push(chain_info_bump); + chain_info_data.extend_from_slice(&[0u8; 2]); // padding + + let chain_info_ix = Instruction { + program_id: QUOTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), // updater + AccountMeta::new(quoter_chain_info_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: chain_info_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, chain_info_ix], + Some(&payer.pubkey()), + &[&payer, &updater], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Update quote + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let (_, quote_body_bump) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let mut quote_data = Vec::new(); + quote_data.push(QUOTER_IX_UPDATE_QUOTE); // 1-byte discriminator + quote_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); + quote_data.push(quote_body_bump); + quote_data.extend_from_slice(&[0u8; 5]); // padding + quote_data.extend_from_slice(&35751300000000u64.to_le_bytes()); // dst_price (matching EVM tests) + quote_data.extend_from_slice(&35751300000000u64.to_le_bytes()); // src_price (same as dst for 1:1 ratio) + quote_data.extend_from_slice(&100000000u64.to_le_bytes()); // dst_gas_price (0.1 gwei, matching EVM) + quote_data.extend_from_slice(&27971u64.to_le_bytes()); // base_fee (matching EVM tests) + + let quote_ix = Instruction { + program_id: QUOTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), // updater + AccountMeta::new(quoter_quote_body_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: quote_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, quote_ix], + Some(&payer.pubkey()), + &[&payer, &updater], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Now call RequestExecution + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let dst_addr = [0x01u8; 32]; + let refund_addr = payer.pubkey().to_bytes(); + let relay_instructions = build_relay_instructions_gas(250000, 0); // 250k gas, matching EVM tests + + // Use a large enough amount to cover the quote - EVM test uses 27797100000000 wei + // For our test with 1:1 price ratio, let's use a generous amount + let amount: u64 = 100_000_000_000; // 100 SOL (way more than needed) + + let request_ix_data = build_request_execution_data( + "er.eth_address, + amount, + DST_CHAIN_ID, + &dst_addr, + &refund_addr, + &[], + &relay_instructions, + ); + + // The event_cpi account is required but not used - use a placeholder + let event_cpi = Pubkey::new_unique(); + + let request_ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), // payer + AccountMeta::new_readonly(config_pubkey, false), // config + AccountMeta::new_readonly(quoter_registration_pda, false), // quoter_registration + AccountMeta::new_readonly(QUOTER_PROGRAM_ID, false), // quoter_program + AccountMeta::new_readonly(EXECUTOR_PROGRAM_ID, false), // executor_program + AccountMeta::new(payee_pubkey, false), // payee + AccountMeta::new(payer.pubkey(), false), // refund_addr + AccountMeta::new_readonly(system_program::ID, false), // system_program + AccountMeta::new_readonly(quoter_config_pubkey, false), // quoter_config + AccountMeta::new_readonly(quoter_chain_info_pda, false), // quoter_chain_info + AccountMeta::new_readonly(quoter_quote_body_pda, false), // quoter_quote_body + AccountMeta::new_readonly(event_cpi, false), // event_cpi + ], + data: request_ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, request_ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!(result.is_ok(), "RequestExecution failed: {:?}", result); + + // Verify payee received payment + let payee_account = banks_client.get_account(payee_pubkey).await.unwrap(); + assert!( + payee_account.is_some(), + "Payee account should exist after payment" + ); + assert!( + payee_account.unwrap().lamports > 0, + "Payee should have received payment" + ); +} + +#[tokio::test] +async fn test_request_execution_underpaid() { + let pt = setup_program_test_full(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Register quoter + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + "ER_PROGRAM_ID, + &sender.pubkey(), + u64::MAX, + ); + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Setup quoter accounts + let quoter_config_pubkey = get_quoter_dummy_config_pubkey(); + let (quoter_chain_info_pda, _) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let (quoter_quote_body_pda, _) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let updater = get_quoter_updater_keypair(); + + let payee = Keypair::new(); + let payee_address_bytes: [u8; 32] = payee.pubkey().to_bytes(); + + // Update chain info + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let (_, chain_info_bump) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let mut chain_info_data = Vec::new(); + chain_info_data.push(QUOTER_IX_UPDATE_CHAIN_INFO); // 1-byte discriminator + chain_info_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); + chain_info_data.push(1); + chain_info_data.push(9); + chain_info_data.push(18); + chain_info_data.push(chain_info_bump); + chain_info_data.extend_from_slice(&[0u8; 2]); + + let chain_info_ix = Instruction { + program_id: QUOTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), // updater + AccountMeta::new(quoter_chain_info_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: chain_info_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, chain_info_ix], + Some(&payer.pubkey()), + &[&payer, &updater], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Update quote with a high base_fee to ensure the quote is high + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let (_, quote_body_bump) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let mut quote_data = Vec::new(); + quote_data.push(QUOTER_IX_UPDATE_QUOTE); // 1-byte discriminator + quote_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); + quote_data.push(quote_body_bump); + quote_data.extend_from_slice(&[0u8; 5]); + quote_data.extend_from_slice(&2000_0000000000u64.to_le_bytes()); + quote_data.extend_from_slice(&200_0000000000u64.to_le_bytes()); + quote_data.extend_from_slice(&50_000000000u64.to_le_bytes()); + quote_data.extend_from_slice(&1_000_000_000u64.to_le_bytes()); // high base_fee = 1 SOL + + let quote_ix = Instruction { + program_id: QUOTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), // updater + AccountMeta::new(quoter_quote_body_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: quote_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, quote_ix], + Some(&payer.pubkey()), + &[&payer, &updater], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Call RequestExecution with insufficient payment + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let dst_addr = [0x01u8; 32]; + let refund_addr = payer.pubkey().to_bytes(); + let relay_instructions = build_relay_instructions_gas(200000, 0); + + // Use a very small amount that will definitely be less than the quote + let amount: u64 = 1000; // Only 1000 lamports + + let request_ix_data = build_request_execution_data( + "er.eth_address, + amount, + DST_CHAIN_ID, + &dst_addr, + &refund_addr, + &[], + &relay_instructions, + ); + + let event_cpi = Pubkey::new_unique(); + + let request_ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(quoter_registration_pda, false), + AccountMeta::new_readonly(QUOTER_PROGRAM_ID, false), + AccountMeta::new_readonly(EXECUTOR_PROGRAM_ID, false), + AccountMeta::new(payee.pubkey(), false), + AccountMeta::new(payer.pubkey(), false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(quoter_config_pubkey, false), + AccountMeta::new_readonly(quoter_chain_info_pda, false), + AccountMeta::new_readonly(quoter_quote_body_pda, false), + AccountMeta::new_readonly(event_cpi, false), + ], + data: request_ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, request_ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "RequestExecution should fail when underpaid" + ); +} + +// ============================================================================ +// Boundary Condition Tests +// ============================================================================ + +// Note: Signer verification tests are omitted because the Solana runtime +// handles signature verification before the program is invoked. Testing +// missing signatures would only test the SDK/runtime, not our program logic. + +// --- UpdateQuoterContract Boundary Tests --- + +#[tokio::test] +async fn test_update_quoter_contract_empty_data() { + let pt = setup_program_test(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Try UpdateQuoterContract with empty data + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, _) = derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: vec![1], // Only discriminator + }; + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + let result = banks_client.process_transaction(tx).await; + assert!(result.is_err(), "Should fail with empty governance data"); +} + +// Note: test_update_quoter_contract_invalid_prefix is covered by +// test_update_quoter_contract_invalid_governance_prefix above. +// Note: test_update_quoter_contract_expiry_exactly_now is covered by +// test_update_quoter_contract_expired_fails above. + +// --- QuoteExecution Boundary Tests --- + +#[tokio::test] +async fn test_quote_execution_empty_data() { + let pt = setup_program_test_full(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, _) = derive_quoter_registration_pda("er.eth_address); + let quoter_config_pubkey = get_quoter_dummy_config_pubkey(); + let (quoter_chain_info_pda, _) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let (quoter_quote_body_pda, _) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // QuoteExecution with empty data + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(quoter_registration_pda, false), + AccountMeta::new_readonly(QUOTER_PROGRAM_ID, false), + AccountMeta::new_readonly(quoter_config_pubkey, false), + AccountMeta::new_readonly(quoter_chain_info_pda, false), + AccountMeta::new_readonly(quoter_quote_body_pda, false), + ], + data: vec![2], // Only discriminator + }; + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "Should fail with empty QuoteExecution data" + ); +} + +#[tokio::test] +async fn test_quote_execution_partial_data() { + let pt = setup_program_test_full(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, _) = derive_quoter_registration_pda("er.eth_address); + let quoter_config_pubkey = get_quoter_dummy_config_pubkey(); + let (quoter_chain_info_pda, _) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let (quoter_quote_body_pda, _) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // QuoteExecution with only 50 bytes (needs 94 minimum) + let mut ix_data = vec![2u8]; // QuoteExecution discriminator + ix_data.extend_from_slice(&[0u8; 50]); // Not enough data + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new_readonly(quoter_registration_pda, false), + AccountMeta::new_readonly(QUOTER_PROGRAM_ID, false), + AccountMeta::new_readonly(quoter_config_pubkey, false), + AccountMeta::new_readonly(quoter_chain_info_pda, false), + AccountMeta::new_readonly(quoter_quote_body_pda, false), + ], + data: ix_data, + }; + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "Should fail with partial QuoteExecution data" + ); +} + +// --- RequestExecution Boundary Tests --- + +#[tokio::test] +async fn test_request_execution_empty_data() { + let pt = setup_program_test_full(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, _) = derive_quoter_registration_pda("er.eth_address); + let payee = Keypair::new(); + let quoter_config_pubkey = get_quoter_dummy_config_pubkey(); + let (quoter_chain_info_pda, _) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let (quoter_quote_body_pda, _) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let event_cpi = Pubkey::new_unique(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // RequestExecution with empty data + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(quoter_registration_pda, false), + AccountMeta::new_readonly(QUOTER_PROGRAM_ID, false), + AccountMeta::new_readonly(EXECUTOR_PROGRAM_ID, false), + AccountMeta::new(payee.pubkey(), false), + AccountMeta::new(payer.pubkey(), false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(quoter_config_pubkey, false), + AccountMeta::new_readonly(quoter_chain_info_pda, false), + AccountMeta::new_readonly(quoter_quote_body_pda, false), + AccountMeta::new_readonly(event_cpi, false), + ], + data: vec![3], // Only discriminator + }; + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "Should fail with empty RequestExecution data" + ); +} + +#[tokio::test] +async fn test_request_execution_amount_zero() { + let pt = setup_program_test_full(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + // Register quoter + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, quoter_bump) = + derive_quoter_registration_pda("er.eth_address); + let sender = Keypair::new(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let ix_data = build_signed_update_quoter_contract_data( + SOLANA_CHAIN_ID, + "er, + "ER_PROGRAM_ID, + &sender.pubkey(), + u64::MAX, + ); + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(sender.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new(quoter_registration_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: ix_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, ix], + Some(&payer.pubkey()), + &[&payer, &sender], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Setup quoter accounts + let quoter_config_pubkey = get_quoter_dummy_config_pubkey(); + let (quoter_chain_info_pda, _) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let (quoter_quote_body_pda, _) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let payee = Keypair::new(); + let payee_address_bytes: [u8; 32] = payee.pubkey().to_bytes(); + let updater = get_quoter_updater_keypair(); + + // Update chain info + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let (_, chain_info_bump) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let mut chain_info_data = Vec::new(); + chain_info_data.push(QUOTER_IX_UPDATE_CHAIN_INFO); // 1-byte discriminator + chain_info_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); + chain_info_data.push(1); + chain_info_data.push(12); + chain_info_data.push(18); + chain_info_data.push(chain_info_bump); + chain_info_data.extend_from_slice(&[0u8; 2]); + + let chain_info_ix = Instruction { + program_id: QUOTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), // updater + AccountMeta::new(quoter_chain_info_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: chain_info_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, chain_info_ix], + Some(&payer.pubkey()), + &[&payer, &updater], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // Update quote with non-zero base_fee so quote > 0 + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let (_, quote_body_bump) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let mut quote_data = Vec::new(); + quote_data.push(QUOTER_IX_UPDATE_QUOTE); // 1-byte discriminator + quote_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); + quote_data.push(quote_body_bump); + quote_data.extend_from_slice(&[0u8; 5]); + quote_data.extend_from_slice(&35751300000000u64.to_le_bytes()); + quote_data.extend_from_slice(&35751300000000u64.to_le_bytes()); + quote_data.extend_from_slice(&100000000u64.to_le_bytes()); + quote_data.extend_from_slice(&27971u64.to_le_bytes()); + + let quote_ix = Instruction { + program_id: QUOTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), // updater + AccountMeta::new(quoter_quote_body_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + data: quote_data, + }; + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(200_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, quote_ix], + Some(&payer.pubkey()), + &[&payer, &updater], + recent_blockhash, + ); + banks_client.process_transaction(tx).await.unwrap(); + + // RequestExecution with amount = 0 + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + let dst_addr = [0x01u8; 32]; + let refund_addr = payer.pubkey().to_bytes(); + let relay_instructions = build_relay_instructions_gas(250000, 0); + + let request_ix_data = build_request_execution_data( + "er.eth_address, + 0, // Zero amount! + DST_CHAIN_ID, + &dst_addr, + &refund_addr, + &[], + &relay_instructions, + ); + + let event_cpi = Pubkey::new_unique(); + + let request_ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(quoter_registration_pda, false), + AccountMeta::new_readonly(QUOTER_PROGRAM_ID, false), + AccountMeta::new_readonly(EXECUTOR_PROGRAM_ID, false), + AccountMeta::new(payee.pubkey(), false), + AccountMeta::new(payer.pubkey(), false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(quoter_config_pubkey, false), + AccountMeta::new_readonly(quoter_chain_info_pda, false), + AccountMeta::new_readonly(quoter_quote_body_pda, false), + AccountMeta::new_readonly(event_cpi, false), + ], + data: request_ix_data, + }; + + let compute_ix = ComputeBudgetInstruction::set_compute_unit_limit(400_000); + let tx = Transaction::new_signed_with_payer( + &[compute_ix, request_ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + + let result = banks_client.process_transaction(tx).await; + // Should fail because quote requires payment but amount is 0 + assert!( + result.is_err(), + "Should fail with zero amount when quote requires payment" + ); +} + +#[tokio::test] +async fn test_request_execution_max_request_bytes_len() { + let pt = setup_program_test_full(); + let (mut banks_client, payer, recent_blockhash) = pt.start().await; + + let config_pubkey = get_dummy_config_pubkey(); + + let quoter = QuoterIdentity::new(); + let (quoter_registration_pda, _) = derive_quoter_registration_pda("er.eth_address); + let payee = Keypair::new(); + let quoter_config_pubkey = get_quoter_dummy_config_pubkey(); + let (quoter_chain_info_pda, _) = derive_quoter_chain_info_pda(DST_CHAIN_ID); + let (quoter_quote_body_pda, _) = derive_quoter_quote_body_pda(DST_CHAIN_ID); + let event_cpi = Pubkey::new_unique(); + + let recent_blockhash = banks_client + .get_new_latest_blockhash(&recent_blockhash) + .await + .unwrap(); + + // Build request with request_bytes_len = u32::MAX but no actual data + // This should trigger a bounds check failure + let mut ix_data = vec![3u8]; // RequestExecution discriminator + ix_data.extend_from_slice(&[0u8; 20]); // quoter_address + ix_data.extend_from_slice(&100u64.to_le_bytes()); // amount + ix_data.extend_from_slice(&DST_CHAIN_ID.to_le_bytes()); // dst_chain + ix_data.extend_from_slice(&[0u8; 32]); // dst_addr + ix_data.extend_from_slice(&[0u8; 32]); // refund_addr + ix_data.extend_from_slice(&u32::MAX.to_le_bytes()); // request_bytes_len = MAX + // No actual request_bytes - this should fail bounds check + + let ix = Instruction { + program_id: ROUTER_PROGRAM_ID, + accounts: vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(quoter_registration_pda, false), + AccountMeta::new_readonly(QUOTER_PROGRAM_ID, false), + AccountMeta::new_readonly(EXECUTOR_PROGRAM_ID, false), + AccountMeta::new(payee.pubkey(), false), + AccountMeta::new(payer.pubkey(), false), + AccountMeta::new_readonly(system_program::ID, false), + AccountMeta::new_readonly(quoter_config_pubkey, false), + AccountMeta::new_readonly(quoter_chain_info_pda, false), + AccountMeta::new_readonly(quoter_quote_body_pda, false), + AccountMeta::new_readonly(event_cpi, false), + ], + data: ix_data, + }; + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&payer.pubkey()), + &[&payer], + recent_blockhash, + ); + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_err(), + "Should fail with request_bytes_len overflow" + ); +} diff --git a/svm/pinocchio/tests/executor-quoter-tests/Cargo.toml b/svm/pinocchio/tests/executor-quoter-tests/Cargo.toml new file mode 100644 index 0000000..798fbc7 --- /dev/null +++ b/svm/pinocchio/tests/executor-quoter-tests/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "executor-quoter-tests" +version = "0.1.0" +description = "Integration tests and benchmarks for executor-quoter" +edition = "2021" + +[lib] +name = "executor_quoter_tests" + +[features] +default = [] + +[dev-dependencies] +solana-program-test = "1.18" +solana-sdk = "1.18" +mollusk-svm = "0.0.9-solana-1.18" +mollusk-svm-bencher = "0.0.9-solana-1.18" +executor-requests = { path = "../../../modules/executor-requests" } + +[[bench]] +name = "compute_units" +harness = false diff --git a/svm/pinocchio/tests/executor-quoter-tests/benches/compute_units.rs b/svm/pinocchio/tests/executor-quoter-tests/benches/compute_units.rs new file mode 100644 index 0000000..31aab68 --- /dev/null +++ b/svm/pinocchio/tests/executor-quoter-tests/benches/compute_units.rs @@ -0,0 +1,373 @@ +//! Compute unit benchmarks for executor-quoter using mollusk-svm. +//! +//! Run with: cargo bench +//! Output: target/benches/executor_quoter_compute_units.md + +use mollusk_svm::program::keyed_account_for_system_program; +use mollusk_svm::Mollusk; +use mollusk_svm_bencher::MolluskComputeUnitBencher; +use solana_sdk::{ + account::AccountSharedData, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + rent::Rent, + signature::Signer, + system_program, +}; + +// Program ID - must match the deployed program +const PROGRAM_ID: Pubkey = Pubkey::new_from_array([ + 0x58, 0xce, 0x85, 0x6b, 0x53, 0xca, 0x8b, 0x7d, 0xc9, 0xa3, 0x84, 0x42, 0x1c, 0x5c, 0xaf, 0x30, + 0x63, 0xcf, 0x30, 0x96, 0x2b, 0x4c, 0xf6, 0x0d, 0xad, 0x51, 0x9d, 0x3d, 0xcd, 0xf3, 0x86, 0x58, +]); + +// Account discriminators (must match state.rs) +const QUOTE_BODY_DISCRIMINATOR: u8 = 1; +const CHAIN_INFO_DISCRIMINATOR: u8 = 2; + +// PDA seeds +const QUOTE_SEED: &[u8] = b"quote"; +const CHAIN_INFO_SEED: &[u8] = b"chain_info"; + +// Account sizes (must match state.rs) +const QUOTE_BODY_SIZE: usize = 40; // 1 + 1 + 2 + 4 + 8 + 8 + 8 + 8 +const CHAIN_INFO_SIZE: usize = 8; // 1 + 1 + 2 + 1 + 1 + 1 + 1 + +// Instruction discriminators (initialize was removed) +const IX_UPDATE_CHAIN_INFO: u8 = 0; +const IX_UPDATE_QUOTE: u8 = 1; +const IX_REQUEST_QUOTE: u8 = 2; +const IX_REQUEST_EXECUTION_QUOTE: u8 = 3; + +// Test chain ID (Ethereum mainnet in Wormhole) +const DST_CHAIN_ID: u16 = 2; + +/// Get the updater address from keypair file. +/// Reads from QUOTER_UPDATER_KEYPAIR_PATH env var (path to JSON keypair file). +fn get_updater_address() -> Pubkey { + let keypair_path = std::env::var("QUOTER_UPDATER_KEYPAIR_PATH").expect( + "QUOTER_UPDATER_KEYPAIR_PATH env var must be set to path of updater keypair JSON file", + ); + let keypair = solana_sdk::signature::read_keypair_file(&keypair_path) + .expect("Failed to read updater keypair from file"); + keypair.pubkey() +} + +fn derive_chain_info_pda(chain_id: u16) -> (Pubkey, u8) { + Pubkey::find_program_address(&[CHAIN_INFO_SEED, &chain_id.to_le_bytes()], &PROGRAM_ID) +} + +fn derive_quote_body_pda(chain_id: u16) -> (Pubkey, u8) { + Pubkey::find_program_address(&[QUOTE_SEED, &chain_id.to_le_bytes()], &PROGRAM_ID) +} + +/// Create a funded payer account +fn create_payer_account() -> AccountSharedData { + AccountSharedData::new(1_000_000_000, 0, &system_program::ID) +} + +/// Create a signer account (updater) +fn create_signer_account() -> AccountSharedData { + AccountSharedData::new(0, 0, &system_program::ID) +} + +/// Create a placeholder config account (config is not used, just required in account list) +fn create_config_account() -> AccountSharedData { + AccountSharedData::new(0, 0, &system_program::ID) +} + +/// Create an initialized ChainInfo account +/// Layout: discriminator, bump, chain_id (u16), enabled, gas_price_decimals, native_decimals, _padding +fn create_chain_info_account(chain_id: u16, bump: u8) -> AccountSharedData { + let rent = Rent::default(); + let lamports = rent.minimum_balance(CHAIN_INFO_SIZE); + let mut data = vec![0u8; CHAIN_INFO_SIZE]; + + data[0] = CHAIN_INFO_DISCRIMINATOR; + data[1] = bump; + data[2..4].copy_from_slice(&chain_id.to_le_bytes()); + data[4] = 1; // enabled + data[5] = 15; // gas_price_decimals + data[6] = 18; // native_decimals (ETH) + // data[7] = padding + + let mut account = AccountSharedData::new(lamports, CHAIN_INFO_SIZE, &PROGRAM_ID); + account.set_data_from_slice(&data); + account +} + +/// Create an initialized QuoteBody account +/// Layout: discriminator, bump, chain_id (u16), _padding (4), dst_price, src_price, dst_gas_price, base_fee +fn create_quote_body_account(chain_id: u16, bump: u8) -> AccountSharedData { + let rent = Rent::default(); + let lamports = rent.minimum_balance(QUOTE_BODY_SIZE); + let mut data = vec![0u8; QUOTE_BODY_SIZE]; + + data[0] = QUOTE_BODY_DISCRIMINATOR; + data[1] = bump; + data[2..4].copy_from_slice(&chain_id.to_le_bytes()); + // _padding [4..8] + // dst_price (u64 at offset 8) - $16 ETH (test value) in 10^10 + data[8..16].copy_from_slice(&160_000_000u64.to_le_bytes()); + // src_price (u64 at offset 16) - $265 SOL in 10^10 + data[16..24].copy_from_slice(&2_650_000_000u64.to_le_bytes()); + // dst_gas_price (u64 at offset 24) - old test value + data[24..32].copy_from_slice(&399_146u64.to_le_bytes()); + // base_fee (u64 at offset 32) - old test value + data[32..40].copy_from_slice(&100u64.to_le_bytes()); + + let mut account = AccountSharedData::new(lamports, QUOTE_BODY_SIZE, &PROGRAM_ID); + account.set_data_from_slice(&data); + account +} + +/// Build UpdateChainInfo instruction data +/// Layout: ix_discriminator (1 byte), chain_id (u16), enabled, gas_price_decimals, native_decimals, _padding +fn build_update_chain_info_data(chain_id: u16) -> Vec { + let mut data = vec![IX_UPDATE_CHAIN_INFO]; // 1-byte discriminator + data.extend_from_slice(&chain_id.to_le_bytes()); + data.push(1); // enabled + data.push(18); // gas_price_decimals + data.push(18); // native_decimals + data.push(0); // _padding + data +} + +/// Build UpdateQuote instruction data +/// Layout: ix_discriminator (1 byte), chain_id (u16), _padding (6), dst_price, src_price, dst_gas_price, base_fee +fn build_update_quote_data(chain_id: u16) -> Vec { + let mut data = vec![IX_UPDATE_QUOTE]; // 1-byte discriminator + data.extend_from_slice(&chain_id.to_le_bytes()); + data.extend_from_slice(&[0u8; 6]); // padding + data.extend_from_slice(&20_000_000_000_000u64.to_le_bytes()); // dst_price + data.extend_from_slice(&1_500_000_000_000u64.to_le_bytes()); // src_price + data.extend_from_slice(&30_000_000_000u64.to_le_bytes()); // dst_gas_price + data.extend_from_slice(&1_000_000u64.to_le_bytes()); // base_fee + data +} + +/// Build RequestQuote instruction data +fn build_request_quote_data(chain_id: u16, gas_limit: u128, msg_value: u128) -> Vec { + let mut data = vec![IX_REQUEST_QUOTE, 0, 0, 0, 0, 0, 0, 0]; // 8-byte discriminator + // dst_chain (2 bytes) + data.extend_from_slice(&chain_id.to_le_bytes()); + // dst_addr (32 bytes) + data.extend_from_slice(&[0xab; 32]); + // refund_addr (32 bytes) + data.extend_from_slice(&[0xcd; 32]); + // request_bytes_len (4 bytes) + request_bytes + data.extend_from_slice(&0u32.to_le_bytes()); + // relay_instructions_len (4 bytes) + let relay_len = 33u32; // 1 byte type + 16 bytes gas + 16 bytes value + data.extend_from_slice(&relay_len.to_le_bytes()); + // relay_instructions: type 1 (Gas) + data.push(1); // IX_TYPE_GAS + data.extend_from_slice(&gas_limit.to_be_bytes()); + data.extend_from_slice(&msg_value.to_be_bytes()); + data +} + +/// Build RequestExecutionQuote instruction data +fn build_request_execution_quote_data(chain_id: u16, gas_limit: u128, msg_value: u128) -> Vec { + let mut data = vec![IX_REQUEST_EXECUTION_QUOTE, 0, 0, 0, 0, 0, 0, 0]; // 8-byte discriminator + // dst_chain (2 bytes) + data.extend_from_slice(&chain_id.to_le_bytes()); + // dst_addr (32 bytes) + data.extend_from_slice(&[0xab; 32]); + // refund_addr (32 bytes) + data.extend_from_slice(&[0xcd; 32]); + // request_bytes_len (4 bytes) + request_bytes + data.extend_from_slice(&0u32.to_le_bytes()); + // relay_instructions_len (4 bytes) + let relay_len = 33u32; + data.extend_from_slice(&relay_len.to_le_bytes()); + // relay_instructions: type 1 (Gas) + data.push(1); + data.extend_from_slice(&gas_limit.to_be_bytes()); + data.extend_from_slice(&msg_value.to_be_bytes()); + data +} + +fn main() { + // Initialize Mollusk with the program + let mollusk = Mollusk::new(&PROGRAM_ID, "executor_quoter"); + + // Get the system program keyed account for CPI + let system_program_account = keyed_account_for_system_program(); + + // Set up common accounts + let payer = Pubkey::new_unique(); + let updater = get_updater_address(); + let config_pda = Pubkey::new_unique(); // placeholder, not used + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(DST_CHAIN_ID); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(DST_CHAIN_ID); + + // Benchmark: UpdateChainInfo (create new) + let update_chain_info_ix = Instruction::new_with_bytes( + PROGRAM_ID, + &build_update_chain_info_data(DST_CHAIN_ID), + vec![ + AccountMeta::new(payer, true), + AccountMeta::new_readonly(updater, true), + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new(chain_info_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + ); + let update_chain_info_accounts = vec![ + (payer, create_payer_account()), + (updater, create_signer_account()), + (config_pda, create_config_account()), + ( + chain_info_pda, + AccountSharedData::new(0, 0, &system_program::ID), + ), + system_program_account.clone(), + ]; + + // Benchmark: UpdateChainInfo (update existing) + let update_chain_info_existing_accounts = vec![ + (payer, create_payer_account()), + (updater, create_signer_account()), + (config_pda, create_config_account()), + ( + chain_info_pda, + create_chain_info_account(DST_CHAIN_ID, chain_info_bump), + ), + system_program_account.clone(), + ]; + + // Benchmark: UpdateQuote (create new) + let update_quote_ix = Instruction::new_with_bytes( + PROGRAM_ID, + &build_update_quote_data(DST_CHAIN_ID), + vec![ + AccountMeta::new(payer, true), + AccountMeta::new_readonly(updater, true), + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new(quote_body_pda, false), + AccountMeta::new_readonly(system_program::ID, false), + ], + ); + let update_quote_accounts = vec![ + (payer, create_payer_account()), + (updater, create_signer_account()), + (config_pda, create_config_account()), + ( + quote_body_pda, + AccountSharedData::new(0, 0, &system_program::ID), + ), + system_program_account.clone(), + ]; + + // Benchmark: UpdateQuote (update existing) + let update_quote_existing_accounts = vec![ + (payer, create_payer_account()), + (updater, create_signer_account()), + (config_pda, create_config_account()), + ( + quote_body_pda, + create_quote_body_account(DST_CHAIN_ID, quote_body_bump), + ), + system_program_account.clone(), + ]; + + // Benchmark: RequestQuote (250k gas, no value) - matches old test + let request_quote_ix = Instruction::new_with_bytes( + PROGRAM_ID, + &build_request_quote_data(DST_CHAIN_ID, 250_000, 0), + vec![ + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + let request_quote_accounts = vec![ + (config_pda, create_config_account()), + ( + chain_info_pda, + create_chain_info_account(DST_CHAIN_ID, chain_info_bump), + ), + ( + quote_body_pda, + create_quote_body_account(DST_CHAIN_ID, quote_body_bump), + ), + ]; + + // Benchmark: RequestQuote (500k gas, with value) + let request_quote_large_ix = Instruction::new_with_bytes( + PROGRAM_ID, + &build_request_quote_data(DST_CHAIN_ID, 500_000, 1_000_000_000_000_000_000), // 1 ETH + vec![ + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + // Benchmark: RequestExecutionQuote (250k gas, no value) - matches old test + let event_cpi = Pubkey::new_unique(); + let request_exec_quote_ix = Instruction::new_with_bytes( + PROGRAM_ID, + &build_request_execution_quote_data(DST_CHAIN_ID, 250_000, 0), + vec![ + AccountMeta::new_readonly(config_pda, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + AccountMeta::new_readonly(event_cpi, false), + ], + ); + let request_exec_quote_accounts = vec![ + (config_pda, create_config_account()), + ( + chain_info_pda, + create_chain_info_account(DST_CHAIN_ID, chain_info_bump), + ), + ( + quote_body_pda, + create_quote_body_account(DST_CHAIN_ID, quote_body_bump), + ), + (event_cpi, AccountSharedData::new(0, 0, &system_program::ID)), + ]; + + // Run benchmarks + MolluskComputeUnitBencher::new(mollusk) + .bench(( + "update_chain_info_create", + &update_chain_info_ix, + &update_chain_info_accounts, + )) + .bench(( + "update_chain_info_update", + &update_chain_info_ix, + &update_chain_info_existing_accounts, + )) + .bench(( + "update_quote_create", + &update_quote_ix, + &update_quote_accounts, + )) + .bench(( + "update_quote_update", + &update_quote_ix, + &update_quote_existing_accounts, + )) + .bench(( + "request_quote_250k_gas", + &request_quote_ix, + &request_quote_accounts, + )) + .bench(( + "request_quote_500k_gas_1eth", + &request_quote_large_ix, + &request_quote_accounts, + )) + .bench(( + "request_execution_quote", + &request_exec_quote_ix, + &request_exec_quote_accounts, + )) + .must_pass(true) + .out_dir("target/benches") + .execute(); +} diff --git a/svm/pinocchio/tests/executor-quoter-tests/src/lib.rs b/svm/pinocchio/tests/executor-quoter-tests/src/lib.rs new file mode 100644 index 0000000..494acaa --- /dev/null +++ b/svm/pinocchio/tests/executor-quoter-tests/src/lib.rs @@ -0,0 +1 @@ +// Placeholder lib for test crate diff --git a/svm/pinocchio/tests/executor-quoter-tests/tests/integration.rs b/svm/pinocchio/tests/executor-quoter-tests/tests/integration.rs new file mode 100644 index 0000000..2958da3 --- /dev/null +++ b/svm/pinocchio/tests/executor-quoter-tests/tests/integration.rs @@ -0,0 +1,2507 @@ +//! Integration tests for executor-quoter using solana-program-test with BPF. +//! +//! Since this is a Pinocchio program (not using solana_program), we must +//! test using the compiled BPF binary rather than native execution. + +use solana_program_test::{tokio, ProgramTest}; +use solana_sdk::{ + account::Account, + compute_budget::ComputeBudgetInstruction, + instruction::{AccountMeta, Instruction}, + pubkey::Pubkey, + signature::{Keypair, Signer}, + system_program, + transaction::Transaction, +}; + +/// Program ID matching the deployed address from Anchor.toml +/// 6yfXVhNgRKRk7YHFT8nTkVpFn5zXktbJddPUWK7jFAGX +const PROGRAM_ID: Pubkey = Pubkey::new_from_array([ + 0x58, 0xce, 0x85, 0x6b, 0x53, 0xca, 0x8b, 0x7d, 0xc9, 0xa3, 0x84, 0x42, 0x1c, 0x5c, 0xaf, 0x30, + 0x63, 0xcf, 0x30, 0x96, 0x2b, 0x4c, 0xf6, 0x0d, 0xad, 0x51, 0x9d, 0x3d, 0xcd, 0xf3, 0x86, 0x58, +]); + +/// Account discriminators +const QUOTE_BODY_DISCRIMINATOR: u8 = 1; +const CHAIN_INFO_DISCRIMINATOR: u8 = 2; + +/// PDA seeds +const QUOTE_SEED: &[u8] = b"quote"; +const CHAIN_INFO_SEED: &[u8] = b"chain_info"; + +/// Account sizes +const CHAIN_INFO_SIZE: usize = 8; +const QUOTE_BODY_SIZE: usize = 40; + +/// Instruction discriminators +/// Admin instructions (0, 1): 1-byte discriminator for minimal tx size +/// CPI instructions (2, 3): 8-byte discriminator for Anchor compatibility +const IX_UPDATE_CHAIN_INFO: u8 = 0; +const IX_UPDATE_QUOTE: u8 = 1; +const IX_REQUEST_QUOTE: [u8; 8] = [2, 0, 0, 0, 0, 0, 0, 0]; +const IX_REQUEST_EXECUTION_QUOTE: [u8; 8] = [3, 0, 0, 0, 0, 0, 0, 0]; + +/// Get the authorized updater keypair. +/// Reads from QUOTER_UPDATER_KEYPAIR_PATH env var (path to JSON keypair file). +/// The program must be built with QUOTER_UPDATER_PUBKEY set to this keypair's pubkey. +fn get_updater_keypair() -> Keypair { + let keypair_path = std::env::var("QUOTER_UPDATER_KEYPAIR_PATH").expect( + "QUOTER_UPDATER_KEYPAIR_PATH env var must be set to path of updater keypair JSON file", + ); + solana_sdk::signature::read_keypair_file(&keypair_path) + .expect("Failed to read updater keypair from file") +} + +/// Get the payee address (32 bytes) from the updater keypair. +/// The program must be built with QUOTER_PAYEE_PUBKEY set to this value. +fn get_payee_address() -> [u8; 32] { + get_updater_keypair().pubkey().to_bytes() +} + +/// Get a dummy config pubkey for the _config account parameter. +/// This account is unused in get_quote instructions but required for the interface. +fn get_dummy_config_pubkey() -> Pubkey { + Pubkey::new_from_array([0u8; 32]) +} + +/// Helper to derive chain_info PDA +fn derive_chain_info_pda(chain_id: u16) -> (Pubkey, u8) { + Pubkey::find_program_address(&[CHAIN_INFO_SEED, &chain_id.to_le_bytes()], &PROGRAM_ID) +} + +/// Helper to derive quote_body PDA +fn derive_quote_body_pda(chain_id: u16) -> (Pubkey, u8) { + Pubkey::find_program_address(&[QUOTE_SEED, &chain_id.to_le_bytes()], &PROGRAM_ID) +} + +/// Build UpdateChainInfo instruction data +fn build_update_chain_info_data( + chain_id: u16, + enabled: bool, + gas_price_decimals: u8, + native_decimals: u8, +) -> Vec { + let mut data = Vec::with_capacity(1 + 6); + data.push(IX_UPDATE_CHAIN_INFO); // 1-byte discriminator + data.extend_from_slice(&chain_id.to_le_bytes()); + data.push(if enabled { 1 } else { 0 }); + data.push(gas_price_decimals); + data.push(native_decimals); + data.push(0); // padding + data +} + +/// Build UpdateQuote instruction data +fn build_update_quote_data( + chain_id: u16, + dst_price: u64, + src_price: u64, + dst_gas_price: u64, + base_fee: u64, +) -> Vec { + let mut data = Vec::with_capacity(1 + 40); + data.push(IX_UPDATE_QUOTE); // 1-byte discriminator + data.extend_from_slice(&chain_id.to_le_bytes()); + data.extend_from_slice(&[0u8; 6]); // padding + data.extend_from_slice(&dst_price.to_le_bytes()); + data.extend_from_slice(&src_price.to_le_bytes()); + data.extend_from_slice(&dst_gas_price.to_le_bytes()); + data.extend_from_slice(&base_fee.to_le_bytes()); + data +} + +/// Build RequestQuote instruction data +fn build_request_quote_data( + dst_chain: u16, + dst_addr: &[u8; 32], + refund_addr: &[u8; 32], + request_bytes: &[u8], + relay_instructions: &[u8], +) -> Vec { + let mut data = Vec::new(); + data.extend_from_slice(&IX_REQUEST_QUOTE); // 8-byte discriminator + data.extend_from_slice(&dst_chain.to_le_bytes()); + data.extend_from_slice(dst_addr); + data.extend_from_slice(refund_addr); + data.extend_from_slice(&(request_bytes.len() as u32).to_le_bytes()); + data.extend_from_slice(request_bytes); + data.extend_from_slice(&(relay_instructions.len() as u32).to_le_bytes()); + data.extend_from_slice(relay_instructions); + data +} + +/// Build RequestExecutionQuote instruction data +fn build_request_execution_quote_data( + dst_chain: u16, + dst_addr: &[u8; 32], + refund_addr: &[u8; 32], + request_bytes: &[u8], + relay_instructions: &[u8], +) -> Vec { + let mut data = Vec::new(); + data.extend_from_slice(&IX_REQUEST_EXECUTION_QUOTE); // 8-byte discriminator + data.extend_from_slice(&dst_chain.to_le_bytes()); + data.extend_from_slice(dst_addr); + data.extend_from_slice(refund_addr); + data.extend_from_slice(&(request_bytes.len() as u32).to_le_bytes()); + data.extend_from_slice(request_bytes); + data.extend_from_slice(&(relay_instructions.len() as u32).to_le_bytes()); + data.extend_from_slice(relay_instructions); + data +} + +/// Build relay instructions with gas limit and msg value +fn build_relay_instructions_gas(gas_limit: u128, msg_value: u128) -> Vec { + let mut data = Vec::with_capacity(33); + data.push(1); // IX_TYPE_GAS + data.extend_from_slice(&gas_limit.to_be_bytes()); + data.extend_from_slice(&msg_value.to_be_bytes()); + data +} + +/// Build drop-off relay instruction (Type 2) +fn build_relay_instructions_dropoff(msg_value: u128, recipient: &[u8; 32]) -> Vec { + let mut data = Vec::with_capacity(49); + data.push(2); // IX_TYPE_DROP_OFF + data.extend_from_slice(&msg_value.to_be_bytes()); + data.extend_from_slice(recipient); + data +} + +/// Build combined gas + dropoff relay instructions +fn build_relay_instructions_gas_and_dropoff( + gas_limit: u128, + gas_msg_value: u128, + dropoff_value: u128, + recipient: &[u8; 32], +) -> Vec { + let mut data = Vec::with_capacity(33 + 49); + // Gas instruction (Type 1) + data.push(1); + data.extend_from_slice(&gas_limit.to_be_bytes()); + data.extend_from_slice(&gas_msg_value.to_be_bytes()); + // DropOff instruction (Type 2) + data.push(2); + data.extend_from_slice(&dropoff_value.to_be_bytes()); + data.extend_from_slice(recipient); + data +} + +/// Build relay instruction with invalid type +fn build_relay_instructions_invalid_type() -> Vec { + let mut data = Vec::with_capacity(33); + data.push(0xFF); // Invalid type + data.extend_from_slice(&100u128.to_be_bytes()); + data.extend_from_slice(&0u128.to_be_bytes()); + data +} + +/// Build two dropoff instructions (invalid - only one allowed) +fn build_relay_instructions_two_dropoffs(recipient: &[u8; 32]) -> Vec { + let mut data = Vec::with_capacity(98); + // First dropoff + data.push(2); + data.extend_from_slice(&100u128.to_be_bytes()); + data.extend_from_slice(recipient); + // Second dropoff (this is invalid) + data.push(2); + data.extend_from_slice(&200u128.to_be_bytes()); + data.extend_from_slice(recipient); + data +} + +/// Build truncated relay instruction (missing bytes) +fn build_relay_instructions_truncated() -> Vec { + let mut data = Vec::with_capacity(10); + data.push(1); // Gas type + data.extend_from_slice(&[0u8; 8]); // Only 8 bytes instead of 32 + data +} + +/// Create a ChainInfo account with initialized data +/// Layout: discriminator, bump, chain_id (u16), enabled, gas_price_decimals, native_decimals, padding +fn create_chain_info_account_data( + bump: u8, + chain_id: u16, + enabled: bool, + gas_price_decimals: u8, + native_decimals: u8, +) -> Vec { + let mut data = vec![0u8; CHAIN_INFO_SIZE]; + data[0] = CHAIN_INFO_DISCRIMINATOR; + data[1] = bump; + data[2..4].copy_from_slice(&chain_id.to_le_bytes()); + data[4] = if enabled { 1 } else { 0 }; + data[5] = gas_price_decimals; + data[6] = native_decimals; + data[7] = 0; // padding + data +} + +/// Create a QuoteBody account with initialized data +/// Layout: discriminator, bump, chain_id (u16), padding (4), dst_price, src_price, dst_gas_price, base_fee +fn create_quote_body_account_data( + bump: u8, + chain_id: u16, + dst_price: u64, + src_price: u64, + dst_gas_price: u64, + base_fee: u64, +) -> Vec { + let mut data = vec![0u8; QUOTE_BODY_SIZE]; + data[0] = QUOTE_BODY_DISCRIMINATOR; + data[1] = bump; + data[2..4].copy_from_slice(&chain_id.to_le_bytes()); + // padding at 4..8 + data[8..16].copy_from_slice(&dst_price.to_le_bytes()); + data[16..24].copy_from_slice(&src_price.to_le_bytes()); + data[24..32].copy_from_slice(&dst_gas_price.to_le_bytes()); + data[32..40].copy_from_slice(&base_fee.to_le_bytes()); + data +} + +/// Create a ProgramTest loading the BPF program binary +fn create_program_test() -> ProgramTest { + let mut pt = ProgramTest::default(); + // Load the BPF program directly from the target/deploy directory + pt.add_program("executor_quoter", PROGRAM_ID, None); + // Force BPF execution for pinocchio programs + pt.prefer_bpf(true); + pt +} + +#[tokio::test] +async fn test_update_chain_info() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; // Ethereum + let (chain_info_pda, _chain_info_bump) = derive_chain_info_pda(chain_id); + + // Add payer and updater with funds + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + pt.add_account( + updater.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let instruction_data = build_update_chain_info_data( + chain_id, true, // enabled + 9, // gas_price_decimals (Gwei) + 18, // native_decimals (ETH) + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), + AccountMeta::new(chain_info_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &updater], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!(result.is_ok(), "UpdateChainInfo failed: {:?}", result.err()); + + // Verify chain_info account + let chain_info_account = banks_client + .get_account(chain_info_pda) + .await + .expect("Failed to get account") + .expect("ChainInfo account not found"); + + assert_eq!(chain_info_account.data.len(), CHAIN_INFO_SIZE); + assert_eq!(chain_info_account.data[0], CHAIN_INFO_DISCRIMINATOR); + // ChainInfo layout: discriminator (0), bump (1), chain_id (2-3), enabled (4) + assert_eq!(chain_info_account.data[4], 1); // enabled + + println!("UpdateChainInfo test passed!"); +} + +#[tokio::test] +async fn test_update_quote() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (quote_body_pda, quote_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + + // Add payer and updater + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + pt.add_account( + updater.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Add pre-existing config account + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let instruction_data = build_update_quote_data( + chain_id, + 2000_0000000000, // dst_price: $2000 in 10^10 + 200_0000000000, // src_price: $200 in 10^10 + 50_000000000, // dst_gas_price: 50 Gwei + 1000000, // base_fee: 0.001 SOL + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), + AccountMeta::new(quote_body_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &updater], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!(result.is_ok(), "UpdateQuote failed: {:?}", result.err()); + + // Verify quote_body account + let quote_body_account = banks_client + .get_account(quote_body_pda) + .await + .expect("Failed to get account") + .expect("QuoteBody account not found"); + + assert_eq!(quote_body_account.data.len(), QUOTE_BODY_SIZE); + assert_eq!(quote_body_account.data[0], QUOTE_BODY_DISCRIMINATOR); + + println!("UpdateQuote test passed!"); +} + +#[tokio::test] +async fn test_request_quote() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + // Add payer + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Add pre-existing accounts + + let chain_info_data = create_chain_info_account_data( + chain_info_bump, + chain_id, + true, // enabled + 9, // gas_price_decimals + 18, // native_decimals + ); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, // dst_price + 200_0000000000, // src_price + 50_000000000, // dst_gas_price (50 Gwei) + 1000000, // base_fee + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let dst_addr = [0x01u8; 32]; + let refund_addr = [0x02u8; 32]; + let relay_instructions = build_relay_instructions_gas(200000, 0); // 200k gas, 0 msg value + + let instruction_data = + build_request_quote_data(chain_id, &dst_addr, &refund_addr, &[], &relay_instructions); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!(result.is_ok(), "RequestQuote failed: {:?}", result.err()); + + println!("RequestQuote test passed!"); +} + +#[tokio::test] +async fn test_request_execution_quote() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + // Add payer + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Add pre-existing accounts + + let chain_info_data = create_chain_info_account_data( + chain_info_bump, + chain_id, + true, // enabled + 9, // gas_price_decimals + 18, // native_decimals + ); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, // dst_price + 200_0000000000, // src_price + 50_000000000, // dst_gas_price + 1000000, // base_fee + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let dst_addr = [0x01u8; 32]; + let refund_addr = [0x02u8; 32]; + let relay_instructions = build_relay_instructions_gas(200000, 0); + + let instruction_data = build_request_execution_quote_data( + chain_id, + &dst_addr, + &refund_addr, + &[], + &relay_instructions, + ); + + // Use system program as a dummy event_cpi account (required but unused) + let event_cpi = system_program::ID; + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + AccountMeta::new_readonly(event_cpi, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_ok(), + "RequestExecutionQuote failed: {:?}", + result.err() + ); + + println!("RequestExecutionQuote test passed!"); +} + +#[tokio::test] +async fn test_invalid_updater() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let authorized_updater = Keypair::new(); + let unauthorized_updater = Keypair::new(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let _payee_address = get_payee_address(); + + // Add payer and unauthorized updater + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + pt.add_account( + unauthorized_updater.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let instruction_data = build_update_chain_info_data(chain_id, true, 9, 18); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(unauthorized_updater.pubkey(), true), // Using unauthorized + AccountMeta::new(chain_info_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &unauthorized_updater], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_err(), + "Should have failed with InvalidUpdater error" + ); + + println!("InvalidUpdater test passed!"); +} + +#[tokio::test] +async fn test_chain_disabled() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + // Add payer + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Create chain_info with enabled = false + let chain_info_data = create_chain_info_account_data( + chain_info_bump, + chain_id, + false, // DISABLED + 9, + 18, + ); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let dst_addr = [0x01u8; 32]; + let refund_addr = [0x02u8; 32]; + let relay_instructions = build_relay_instructions_gas(200000, 0); + + let instruction_data = + build_request_quote_data(chain_id, &dst_addr, &refund_addr, &[], &relay_instructions); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!(result.is_err(), "Should have failed with ChainDisabled"); + + println!("ChainDisabled test passed!"); +} + +#[tokio::test] +async fn test_full_flow() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let quoter = Pubkey::new_unique(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + // Add payer and updater with funds + pt.add_account( + payer.pubkey(), + Account { + lamports: 10_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + pt.add_account( + updater.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + // Step 1: UpdateChainInfo + let update_chain_data = build_update_chain_info_data(chain_id, true, 9, 18); + let update_chain_ix = Instruction::new_with_bytes( + PROGRAM_ID, + &update_chain_data, + vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), + AccountMeta::new(chain_info_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + // Get fresh blockhash for each transaction + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("get blockhash"); + // Add compute budget instruction to allow more CUs + let compute_budget_ix = ComputeBudgetInstruction::set_compute_unit_limit(1_400_000); + let mut tx = + Transaction::new_with_payer(&[compute_budget_ix, update_chain_ix], Some(&payer.pubkey())); + tx.sign(&[&payer, &updater], recent_blockhash); + banks_client + .process_transaction(tx) + .await + .expect("UpdateChainInfo failed"); + println!("Step 2: UpdateChainInfo - PASSED"); + + // Step 3: UpdateQuote + let update_quote_data = build_update_quote_data( + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + let update_quote_ix = Instruction::new_with_bytes( + PROGRAM_ID, + &update_quote_data, + vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), + AccountMeta::new(quote_body_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("get blockhash"); + let mut tx = Transaction::new_with_payer(&[update_quote_ix], Some(&payer.pubkey())); + tx.sign(&[&payer, &updater], recent_blockhash); + banks_client + .process_transaction(tx) + .await + .expect("UpdateQuote failed"); + println!("Step 3: UpdateQuote - PASSED"); + + // Step 4: RequestQuote + let relay_instructions = build_relay_instructions_gas(200000, 0); + let request_quote_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + let request_quote_ix = Instruction::new_with_bytes( + PROGRAM_ID, + &request_quote_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("get blockhash"); + let mut tx = Transaction::new_with_payer(&[request_quote_ix], Some(&payer.pubkey())); + tx.sign(&[&payer], recent_blockhash); + banks_client + .process_transaction(tx) + .await + .expect("RequestQuote failed"); + println!("Step 4: RequestQuote - PASSED"); + + println!("\nFull flow completed successfully!"); +} + +// ============================================================================ +// ERROR PATH TESTS +// ============================================================================ + +#[tokio::test] +async fn test_invalid_updater_quote() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let authorized_updater = Keypair::new(); + let unauthorized_updater = Keypair::new(); + let chain_id: u16 = 2; + let (quote_body_pda, quote_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + pt.add_account( + unauthorized_updater.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let instruction_data = build_update_quote_data( + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(unauthorized_updater.pubkey(), true), + AccountMeta::new(quote_body_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer, &unauthorized_updater], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!(result.is_err(), "Should have failed with InvalidUpdater"); + + println!("InvalidUpdater (UpdateQuote) test passed!"); +} + +#[tokio::test] +async fn test_chain_disabled_execution_quote() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data( + chain_info_bump, + chain_id, + false, // DISABLED + 9, + 18, + ); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let relay_instructions = build_relay_instructions_gas(200000, 0); + let instruction_data = build_request_execution_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + // Use system program as a dummy event_cpi account (required but unused) + let event_cpi = system_program::ID; + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + AccountMeta::new_readonly(event_cpi, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!(result.is_err(), "Should have failed with ChainDisabled"); + + println!("ChainDisabled (RequestExecutionQuote) test passed!"); +} + +#[tokio::test] +async fn test_unsupported_instruction() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data(chain_info_bump, chain_id, true, 9, 18); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let relay_instructions = build_relay_instructions_invalid_type(); + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_err(), + "Should have failed with UnsupportedInstruction" + ); + + println!("UnsupportedInstruction test passed!"); +} + +#[tokio::test] +async fn test_more_than_one_dropoff() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data(chain_info_bump, chain_id, true, 9, 18); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let relay_instructions = build_relay_instructions_two_dropoffs(&[0x03u8; 32]); + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_err(), + "Should have failed with MoreThanOneDropOff" + ); + + println!("MoreThanOneDropOff test passed!"); +} + +#[tokio::test] +async fn test_invalid_relay_instructions() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data(chain_info_bump, chain_id, true, 9, 18); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let relay_instructions = build_relay_instructions_truncated(); + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_err(), + "Should have failed with InvalidRelayInstructions" + ); + + println!("InvalidRelayInstructions test passed!"); +} + +#[tokio::test] +async fn test_not_enough_accounts() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let relay_instructions = build_relay_instructions_gas(200000, 0); + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + // Only provide config account, missing chain_info and quote_body + let instruction = Instruction::new_with_bytes(PROGRAM_ID, &instruction_data, vec![]); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_err(), + "Should have failed with NotEnoughAccountKeys" + ); + + println!("NotEnoughAccountKeys test passed!"); +} + +// ============================================================================ +// EDGE CASE / BOUNDARY TESTS +// ============================================================================ + +#[tokio::test] +async fn test_zero_gas_limit() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data(chain_info_bump, chain_id, true, 9, 18); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, // base_fee = 1000000 (10^6 at QUOTE_DECIMALS=10 = 0.0001) + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + // Zero gas limit, zero msg value - should return base_fee only + let relay_instructions = build_relay_instructions_gas(0, 0); + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_ok(), + "Zero gas limit should succeed: {:?}", + result.err() + ); + + println!("Zero gas limit test passed!"); +} + +#[tokio::test] +async fn test_zero_src_price() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data(chain_info_bump, chain_id, true, 9, 18); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + // src_price = 0 (division by zero scenario) + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, // dst_price + 0, // src_price = 0! + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let relay_instructions = build_relay_instructions_gas(200000, 0); + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_err(), + "Should have failed with MathOverflow (division by zero)" + ); + + println!("Zero src_price test passed!"); +} + +#[tokio::test] +async fn test_multiple_gas_instructions() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data(chain_info_bump, chain_id, true, 9, 18); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + // Build two gas instructions - they should sum + let mut relay_instructions = build_relay_instructions_gas(100000, 1000000000000000000); // 100k gas, 1 ETH + relay_instructions.extend(build_relay_instructions_gas(50000, 500000000000000000)); // 50k gas, 0.5 ETH + + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_ok(), + "Multiple gas instructions should succeed: {:?}", + result.err() + ); + + println!("Multiple gas instructions test passed!"); +} + +#[tokio::test] +async fn test_gas_plus_dropoff() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data(chain_info_bump, chain_id, true, 9, 18); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + // Gas instruction + DropOff instruction + let relay_instructions = build_relay_instructions_gas_and_dropoff( + 200000, // gas_limit + 0, // gas msg_value + 1000000000000000000, // dropoff value (1 ETH) + &[0x03u8; 32], // recipient + ); + + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_ok(), + "Gas + DropOff should succeed: {:?}", + result.err() + ); + + println!("Gas + DropOff test passed!"); +} + +#[tokio::test] +async fn test_empty_relay_instructions() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data(chain_info_bump, chain_id, true, 9, 18); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + // Empty relay instructions - should just return base_fee + let relay_instructions: Vec = vec![]; + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_ok(), + "Empty relay instructions should succeed: {:?}", + result.err() + ); + + println!("Empty relay instructions test passed!"); +} + +#[tokio::test] +async fn test_arithmetic_overflow() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data( + chain_info_bump, + chain_id, + true, + 0, // gas_price_decimals = 0 (makes values larger) + 0, // native_decimals = 0 (makes values larger) + ); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + // Extreme prices to cause overflow + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + u64::MAX, // dst_price + 1, // tiny src_price + u64::MAX, // dst_gas_price + u64::MAX, // base_fee + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + // Very large values that should cause overflow + let relay_instructions = build_relay_instructions_gas(u128::MAX / 2, u128::MAX / 2); + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!(result.is_err(), "Should have failed with MathOverflow"); + + println!("Arithmetic overflow test passed!"); +} + +// ============================================================================ +// DECIMAL NORMALIZATION TESTS +// ============================================================================ + +#[tokio::test] +async fn test_decimals_18_to_9() { + // Test with dst_native_decimals=18 (ETH), which tests the division path + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data( + chain_info_bump, + chain_id, + true, + 9, // gas_price_decimals (Gwei) + 18, // native_decimals (ETH) + ); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let relay_instructions = build_relay_instructions_gas(200000, 1000000000000000000); // 200k gas, 1 ETH + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_ok(), + "Decimals 18->9 should succeed: {:?}", + result.err() + ); + + println!("Decimals 18->9 test passed!"); +} + +#[tokio::test] +async fn test_decimals_6_to_9() { + // Test with dst_native_decimals=6 (USDC chain), which tests the multiplication path + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data( + chain_info_bump, + chain_id, + true, + 6, // gas_price_decimals + 6, // native_decimals (6 decimals like USDC) + ); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 1_0000000000, // dst_price: $1 at 10^10 + 200_0000000000, // src_price: $200 at 10^10 + 1_000000, // dst_gas_price: 1 at 6 decimals + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let relay_instructions = build_relay_instructions_gas(200000, 1_000000); // 200k gas, 1 unit at 6 decimals + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_ok(), + "Decimals 6->9 should succeed: {:?}", + result.err() + ); + + println!("Decimals 6->9 test passed!"); +} + +#[tokio::test] +async fn test_decimals_9_to_9() { + // Test with dst_native_decimals=9 (same as SVM), identity path + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let chain_info_data = create_chain_info_account_data( + chain_info_bump, + chain_id, + true, + 9, // gas_price_decimals (same as SOL) + 9, // native_decimals (same as SOL) + ); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 200_0000000000, // dst_price (same as src) + 200_0000000000, // src_price + 1_000000000, // dst_gas_price at 9 decimals + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + let relay_instructions = build_relay_instructions_gas(200000, 1_000000000); // 200k gas, 1 SOL + let instruction_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + + let instruction = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + + let mut transaction = Transaction::new_with_payer(&[instruction], Some(&payer.pubkey())); + transaction.sign(&[&payer], recent_blockhash); + + let result = banks_client.process_transaction(transaction).await; + assert!( + result.is_ok(), + "Decimals 9->9 should succeed: {:?}", + result.err() + ); + + println!("Decimals 9->9 test passed!"); +} + +// ============================================================================ +// ACCOUNT STATE VERIFICATION TESTS +// ============================================================================ + +#[tokio::test] +async fn test_update_overwrites_quote() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (quote_body_pda, quote_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 10_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + pt.add_account( + updater.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + // First update + let instruction_data1 = build_update_quote_data( + chain_id, + 1000_0000000000, // dst_price + 100_0000000000, // src_price + 25_000000000, // dst_gas_price + 500000, // base_fee + ); + + let instruction1 = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data1, + vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), + AccountMeta::new(quote_body_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + let mut tx1 = Transaction::new_with_payer(&[instruction1], Some(&payer.pubkey())); + tx1.sign(&[&payer, &updater], recent_blockhash); + banks_client + .process_transaction(tx1) + .await + .expect("First update failed"); + + // Second update with different values + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("get blockhash"); + let instruction_data2 = build_update_quote_data( + chain_id, + 2000_0000000000, // different dst_price + 200_0000000000, // different src_price + 50_000000000, // different dst_gas_price + 1000000, // different base_fee + ); + + let instruction2 = Instruction::new_with_bytes( + PROGRAM_ID, + &instruction_data2, + vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), + AccountMeta::new(quote_body_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + let mut tx2 = Transaction::new_with_payer(&[instruction2], Some(&payer.pubkey())); + tx2.sign(&[&payer, &updater], recent_blockhash); + banks_client + .process_transaction(tx2) + .await + .expect("Second update failed"); + + // Verify the new values + let quote_body_account = banks_client + .get_account(quote_body_pda) + .await + .expect("Failed to get account") + .expect("QuoteBody account not found"); + + // Check dst_price at offset 8 (after discriminator/padding/chain_id/bump/reserved) + let dst_price = u64::from_le_bytes(quote_body_account.data[8..16].try_into().unwrap()); + assert_eq!( + dst_price, 2000_0000000000, + "dst_price should be updated to new value" + ); + + println!("Update overwrites quote test passed!"); +} + +#[tokio::test] +async fn test_chain_toggle() { + let mut pt = create_program_test(); + + let payer = Keypair::new(); + let updater = get_updater_keypair(); + let chain_id: u16 = 2; + let (chain_info_pda, chain_info_bump) = derive_chain_info_pda(chain_id); + let (quote_body_pda, quote_body_bump) = derive_quote_body_pda(chain_id); + let _payee_address = get_payee_address(); + let config_pubkey = get_dummy_config_pubkey(); + + pt.add_account( + payer.pubkey(), + Account { + lamports: 10_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + pt.add_account( + updater.pubkey(), + Account { + lamports: 1_000_000_000, + data: vec![], + owner: system_program::id(), + executable: false, + rent_epoch: 0, + }, + ); + + // Start with chain enabled + let chain_info_data = create_chain_info_account_data( + chain_info_bump, + chain_id, + true, // enabled + 9, + 18, + ); + pt.add_account( + chain_info_pda, + Account { + lamports: 1_000_000_000, + data: chain_info_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let quote_body_data = create_quote_body_account_data( + quote_body_bump, + chain_id, + 2000_0000000000, + 200_0000000000, + 50_000000000, + 1000000, + ); + pt.add_account( + quote_body_pda, + Account { + lamports: 1_000_000_000, + data: quote_body_data, + owner: PROGRAM_ID, + executable: false, + rent_epoch: 0, + }, + ); + + let (mut banks_client, _, recent_blockhash) = pt.start().await; + + // Disable the chain + let disable_data = build_update_chain_info_data(chain_id, false, 9, 18); + let disable_ix = Instruction::new_with_bytes( + PROGRAM_ID, + &disable_data, + vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), + AccountMeta::new(chain_info_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + let mut tx = Transaction::new_with_payer(&[disable_ix], Some(&payer.pubkey())); + tx.sign(&[&payer, &updater], recent_blockhash); + banks_client + .process_transaction(tx) + .await + .expect("Disable chain failed"); + + // Verify chain is disabled + let chain_info_account = banks_client + .get_account(chain_info_pda) + .await + .expect("Failed to get account") + .expect("ChainInfo account not found"); + // ChainInfo layout: discriminator (0), bump (1), chain_id (2-3), enabled (4) + assert_eq!(chain_info_account.data[4], 0, "Chain should be disabled"); + + // Try to request quote - should fail + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("get blockhash"); + let relay_instructions = build_relay_instructions_gas(200000, 0); + let quote_data = build_request_quote_data( + chain_id, + &[0x01u8; 32], + &[0x02u8; 32], + &[], + &relay_instructions, + ); + let quote_ix = Instruction::new_with_bytes( + PROGRAM_ID, + "e_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + let mut tx = Transaction::new_with_payer(&[quote_ix], Some(&payer.pubkey())); + tx.sign(&[&payer], recent_blockhash); + let result = banks_client.process_transaction(tx).await; + assert!(result.is_err(), "Quote should fail when chain disabled"); + + // Re-enable the chain + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("get blockhash"); + let enable_data = build_update_chain_info_data(chain_id, true, 9, 18); + let enable_ix = Instruction::new_with_bytes( + PROGRAM_ID, + &enable_data, + vec![ + AccountMeta::new(payer.pubkey(), true), + AccountMeta::new_readonly(updater.pubkey(), true), + AccountMeta::new(chain_info_pda, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + let mut tx = Transaction::new_with_payer(&[enable_ix], Some(&payer.pubkey())); + tx.sign(&[&payer, &updater], recent_blockhash); + banks_client + .process_transaction(tx) + .await + .expect("Re-enable chain failed"); + + // Verify chain is enabled again + let chain_info_account = banks_client + .get_account(chain_info_pda) + .await + .expect("Failed to get account") + .expect("ChainInfo account not found"); + // ChainInfo layout: discriminator (0), bump (1), chain_id (2-3), enabled (4) + assert_eq!(chain_info_account.data[4], 1, "Chain should be re-enabled"); + + // Quote should work now + let recent_blockhash = banks_client + .get_latest_blockhash() + .await + .expect("get blockhash"); + // Force account cache refresh - see solana-program-test behavior with BPF programs + let _acc = banks_client + .get_account(chain_info_pda) + .await + .expect("get") + .expect("exists"); + let quote_ix = Instruction::new_with_bytes( + PROGRAM_ID, + "e_data, + vec![ + AccountMeta::new_readonly(config_pubkey, false), + AccountMeta::new_readonly(chain_info_pda, false), + AccountMeta::new_readonly(quote_body_pda, false), + ], + ); + let mut tx = Transaction::new_with_payer(&[quote_ix], Some(&payer.pubkey())); + tx.sign(&[&payer], recent_blockhash); + let result = banks_client.process_transaction(tx).await; + assert!( + result.is_ok(), + "Quote should succeed when chain re-enabled: {:?}", + result.err() + ); + + println!("Chain toggle test passed!"); +} + +// Note: test_account_data_layout removed - Config account no longer exists +// Values are now hardcoded in the program diff --git a/svm/rust-toolchain.toml b/svm/rust-toolchain.toml index 7715ffe..822287c 100644 --- a/svm/rust-toolchain.toml +++ b/svm/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.75.0" +channel = "1.85.0" components = [ "clippy", "rustc", "rustfmt" ] diff --git a/svm/tests/executor-quoters.test.ts b/svm/tests/executor-quoters.test.ts new file mode 100644 index 0000000..7f700c0 --- /dev/null +++ b/svm/tests/executor-quoters.test.ts @@ -0,0 +1,949 @@ +/** + * Integration tests for executor-quoter and executor-quoter-router. + * + * Usage: bun test ./tests/executor-quoters.test.ts + */ + +import { beforeAll, describe, expect, test } from "bun:test"; +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + Transaction, + TransactionInstruction, + TransactionMessage, + VersionedTransaction, + sendAndConfirmTransaction, +} from "@solana/web3.js"; +import { signAsync, getPublicKey, utils } from "@noble/secp256k1"; +import { keccak256 } from "js-sha3"; +import * as fs from "fs"; + +// Program IDs (deployed to devnet) +const QUOTER_PROGRAM_ID = new PublicKey( + "957QU51h6VLbnbAmAPgtXz1kbFE1QhchDmNfgugW9xCc", +); +const ROUTER_PROGRAM_ID = new PublicKey( + "5pkyS8pnbbMforEqAR91gkgPeBs5XKhWpiuuuLdw6hbk", +); +const EXECUTOR_PROGRAM_ID = new PublicKey( + "execXUrAsMnqMmTHj5m7N1YQgsDz3cwGLYCYyuDRciV", +); + +// Instruction discriminators for executor-quoter +// Instructions 0-1 use 1-byte discriminator, 2-3 use 8-byte discriminator +const IX_QUOTER_UPDATE_CHAIN_INFO = 0; +const IX_QUOTER_UPDATE_QUOTE = 1; +const IX_QUOTER_REQUEST_QUOTE = 2; // Uses 8-byte discriminator + +// Instruction discriminators for executor-quoter-router +const IX_ROUTER_UPDATE_QUOTER_CONTRACT = 0; +const IX_ROUTER_QUOTE_EXECUTION = 1; + +// Chain IDs +const CHAIN_ID_SOLANA = 1; +const CHAIN_ID_ETHEREUM = 2; + +// Testnet chain IDs +const CHAIN_ID_ETH_SEPOLIA = 10002; +const CHAIN_ID_ARBITRUM_SEPOLIA = 10003; +const CHAIN_ID_BASE_SEPOLIA = 10004; +const CHAIN_ID_OPTIMISM_SEPOLIA = 10005; + +// Quote calculation constants (must match Rust) +const QUOTE_DECIMALS = 10n; +const SVM_DECIMAL_RESOLUTION = 9n; +const EVM_DECIMAL_RESOLUTION = 18n; + +// Test quote parameters for mainnet Ethereum +const TEST_DST_PRICE = 3000n * 10n ** QUOTE_DECIMALS; // ETH $3000 +const TEST_SRC_PRICE = 200n * 10n ** QUOTE_DECIMALS; // SOL $200 +const TEST_DST_GAS_PRICE = 20n; // 20 gwei (decimals=9) +const TEST_BASE_FEE = 1_000_000n; // 0.001 SOL in lamports +const TEST_GAS_PRICE_DECIMALS = 9; // gwei decimals +const TEST_NATIVE_DECIMALS = 18; // ETH decimals + +// Testnet chain configurations with realistic gas prices +// All testnets use ETH as native token (18 decimals) +// Gas prices vary by L2 characteristics +interface ChainConfig { + chainId: number; + name: string; + dstPrice: bigint; // Native token price in QUOTE_DECIMALS + gasPriceDecimals: number; + nativeDecimals: number; + dstGasPrice: bigint; // Gas price in gasPriceDecimals + baseFee: bigint; // Base fee in lamports +} + +const TESTNET_CHAINS: ChainConfig[] = [ + { + // Ethereum Sepolia - standard L1 gas prices + // All EVM chains store gas price in wei (gasPriceDecimals=18) + // Reference: w7-executor/src/env/testnet.ts + chainId: CHAIN_ID_ETH_SEPOLIA, + name: "Ethereum Sepolia", + dstPrice: 3000n * 10n ** QUOTE_DECIMALS, // ETH ~$3000 + gasPriceDecimals: 18, // gas price stored in wei + nativeDecimals: 18, + dstGasPrice: 25_000_000_000n, // 25 gwei in wei + baseFee: 1_000_000n, // 0.001 SOL + }, + { + // Arbitrum Sepolia - L2 with lower gas prices + // Arbitrum min gas price floor is 0.01 gwei + chainId: CHAIN_ID_ARBITRUM_SEPOLIA, + name: "Arbitrum Sepolia", + dstPrice: 3000n * 10n ** QUOTE_DECIMALS, // ETH ~$3000 + gasPriceDecimals: 18, // gas price stored in wei + nativeDecimals: 18, + dstGasPrice: 100_000_000n, // 0.1 gwei in wei + baseFee: 500_000n, // 0.0005 SOL (lower for L2) + }, + { + // Base Sepolia - L2 with very low gas prices + // Base typically has gas prices around 0.001-0.01 gwei + chainId: CHAIN_ID_BASE_SEPOLIA, + name: "Base Sepolia", + dstPrice: 3000n * 10n ** QUOTE_DECIMALS, // ETH ~$3000 + gasPriceDecimals: 18, // gas price stored in wei + nativeDecimals: 18, + dstGasPrice: 10_000_000n, // 0.01 gwei in wei + baseFee: 500_000n, // 0.0005 SOL + }, + { + // Optimism Sepolia - L2 with low gas prices + // OP typically has gas prices around 0.001-0.05 gwei + chainId: CHAIN_ID_OPTIMISM_SEPOLIA, + name: "Optimism Sepolia", + dstPrice: 3000n * 10n ** QUOTE_DECIMALS, // ETH ~$3000 + gasPriceDecimals: 18, // gas price stored in wei + nativeDecimals: 18, + dstGasPrice: 50_000_000n, // 0.05 gwei in wei + baseFee: 500_000n, // 0.0005 SOL + }, +]; + +// Test request parameters +const TEST_GAS_LIMIT = 100_000n; +const TEST_MSG_VALUE = 10n ** 18n; // 1 ETH in wei + +/** + * Calculate expected quote using the same algorithm as the Rust implementation. + * Returns the expected payment in lamports. + */ +function calculateExpectedQuote( + baseFee: bigint, + srcPrice: bigint, + dstPrice: bigint, + dstGasPrice: bigint, + gasPriceDecimals: number, + nativeDecimals: number, + gasLimit: bigint, + msgValue: bigint, +): bigint { + const pow10 = (exp: bigint) => 10n ** exp; + + // Normalize helper + const normalize = (amount: bigint, from: bigint, to: bigint): bigint => { + if (from > to) return amount / pow10(from - to); + if (from < to) return amount * pow10(to - from); + return amount; + }; + + // mul_decimals: (a * b) / 10^decimals + const mulDecimals = (a: bigint, b: bigint, decimals: bigint): bigint => { + return (a * b) / pow10(decimals); + }; + + // div_decimals: (a * 10^decimals) / b + const divDecimals = (a: bigint, b: bigint, decimals: bigint): bigint => { + return (a * pow10(decimals)) / b; + }; + + // 1. Base fee conversion + const srcChainValueForBaseFee = normalize( + baseFee, + QUOTE_DECIMALS, + EVM_DECIMAL_RESOLUTION, + ); + + // 2. Price ratio + const nSrcPrice = normalize(srcPrice, QUOTE_DECIMALS, EVM_DECIMAL_RESOLUTION); + const nDstPrice = normalize(dstPrice, QUOTE_DECIMALS, EVM_DECIMAL_RESOLUTION); + const scaledConversion = divDecimals( + nDstPrice, + nSrcPrice, + EVM_DECIMAL_RESOLUTION, + ); + + // 3. Gas limit cost + const gasCost = gasLimit * dstGasPrice; + const nGasLimitCost = normalize( + gasCost, + BigInt(gasPriceDecimals), + EVM_DECIMAL_RESOLUTION, + ); + const srcChainValueForGasLimit = mulDecimals( + nGasLimitCost, + scaledConversion, + EVM_DECIMAL_RESOLUTION, + ); + + // 4. Message value conversion + const nMsgValue = normalize( + msgValue, + BigInt(nativeDecimals), + EVM_DECIMAL_RESOLUTION, + ); + const srcChainValueForMsgValue = mulDecimals( + nMsgValue, + scaledConversion, + EVM_DECIMAL_RESOLUTION, + ); + + // 5. Sum (in EVM decimals) + const totalEvm = + srcChainValueForBaseFee + + srcChainValueForGasLimit + + srcChainValueForMsgValue; + + // 6. Convert to SVM decimals (lamports) + return normalize(totalEvm, EVM_DECIMAL_RESOLUTION, SVM_DECIMAL_RESOLUTION); +} + +// Helpers + +function keccak256Hash(data: Uint8Array): Uint8Array { + const hex = keccak256(data); + const result = new Uint8Array(32); + for (let i = 0; i < 32; i++) { + result[i] = parseInt(hex.substring(i * 2, i * 2 + 2), 16); + } + return result; +} + +function loadWallet(): Keypair { + const path = process.env.WALLET_PATH; + if (!path) { + throw new Error("WALLET_PATH environment variable is required"); + } + const secretKey = JSON.parse(fs.readFileSync(path, "utf-8")); + return Keypair.fromSecretKey(Uint8Array.from(secretKey)); +} + +// PDA derivation + +function deriveQuoterConfigPda(): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [Buffer.from("config")], + QUOTER_PROGRAM_ID, + ); +} + +function deriveQuoterChainInfoPda(chainId: number): [PublicKey, number] { + const buf = Buffer.alloc(2); + buf.writeUInt16LE(chainId); + return PublicKey.findProgramAddressSync( + [Buffer.from("chain_info"), buf], + QUOTER_PROGRAM_ID, + ); +} + +function deriveQuoterQuoteBodyPda(chainId: number): [PublicKey, number] { + const buf = Buffer.alloc(2); + buf.writeUInt16LE(chainId); + return PublicKey.findProgramAddressSync( + [Buffer.from("quote"), buf], + QUOTER_PROGRAM_ID, + ); +} + +function deriveRouterConfigPda(): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [Buffer.from("config")], + ROUTER_PROGRAM_ID, + ); +} + +function deriveQuoterRegistrationPda( + quoterAddress: Uint8Array, +): [PublicKey, number] { + return PublicKey.findProgramAddressSync( + [Buffer.from("quoter_registration"), quoterAddress], + ROUTER_PROGRAM_ID, + ); +} + +// Secp256k1 identity for governance signing + +class QuoterIdentity { + privateKey: Uint8Array; + publicKey: Uint8Array; + ethAddress: Uint8Array; + + constructor() { + this.privateKey = utils.randomSecretKey(); + this.publicKey = getPublicKey(this.privateKey, false); + const pubkeyHash = keccak256Hash(this.publicKey.slice(1)); + this.ethAddress = pubkeyHash.slice(12); + } + + async sign( + message: Uint8Array, + ): Promise<{ r: Uint8Array; s: Uint8Array; v: number }> { + const msgHash = keccak256Hash(message); + const sig = (await signAsync(msgHash, this.privateKey, { + lowS: true, + extraEntropy: false, + prehash: false, + format: "recovered", + })) as Uint8Array; + return { + r: sig.slice(1, 33), + s: sig.slice(33, 65), + v: sig[0] + 27, + }; + } +} + +// Instruction builders + +/** + * Build UpdateChainInfo instruction data. + * Layout: discriminator (1) + chain_id (2) + enabled (1) + gas_price_decimals (1) + native_decimals (1) + padding (1) + */ +function buildUpdateChainInfoData( + chainId: number, + enabled: number, + gasPriceDecimals: number, + nativeDecimals: number, +): Buffer { + const data = Buffer.alloc(7); + let o = 0; + data.writeUInt8(IX_QUOTER_UPDATE_CHAIN_INFO, o++); + data.writeUInt16LE(chainId, o); + o += 2; + data.writeUInt8(enabled, o++); + data.writeUInt8(gasPriceDecimals, o++); + data.writeUInt8(nativeDecimals, o++); + data.writeUInt8(0, o); // padding + return data; +} + +/** + * Build UpdateQuote instruction data. + * Layout: discriminator (1) + chain_id (2) + padding (6) + dst_price (8) + src_price (8) + dst_gas_price (8) + base_fee (8) + */ +function buildUpdateQuoteData( + chainId: number, + dstPrice: bigint, + srcPrice: bigint, + dstGasPrice: bigint, + baseFee: bigint, +): Buffer { + const data = Buffer.alloc(41); + let o = 0; + data.writeUInt8(IX_QUOTER_UPDATE_QUOTE, o++); + data.writeUInt16LE(chainId, o); + o += 2; + o += 6; // padding (6 bytes to align to 8-byte boundary for u64s) + data.writeBigUInt64LE(dstPrice, o); + o += 8; + data.writeBigUInt64LE(srcPrice, o); + o += 8; + data.writeBigUInt64LE(dstGasPrice, o); + o += 8; + data.writeBigUInt64LE(baseFee, o); + return data; +} + +/** + * Build UpdateQuoterContract instruction data. + * Layout: discriminator (1) + governance_message (163) + * + * Governance message (163 bytes): + * - prefix "EG01" (4) + * - chain_id (2, BE) + * - quoter_address (20) + * - universal_contract_address (32) + * - universal_sender_address (32) + * - expiry_time (8, BE) + * - signature_r (32) + * - signature_s (32) + * - signature_v (1) + */ +async function buildUpdateQuoterContractData( + quoter: QuoterIdentity, + implementationProgramId: PublicKey, + sender: PublicKey, + chainId: number, + expiryTime: bigint, +): Promise { + // Build the message body (98 bytes - everything before signature) + const body = Buffer.alloc(98); + let o = 0; + Buffer.from("EG01").copy(body, o); + o += 4; + body.writeUInt16BE(chainId, o); + o += 2; + Buffer.from(quoter.ethAddress).copy(body, o); + o += 20; + implementationProgramId.toBuffer().copy(body, o); + o += 32; + sender.toBuffer().copy(body, o); + o += 32; + body.writeBigUInt64BE(expiryTime, o); + + // Sign the body + const { r, s, v } = await quoter.sign(body); + + // Build full instruction data: discriminator + governance message + const data = Buffer.alloc(164); + o = 0; + data.writeUInt8(IX_ROUTER_UPDATE_QUOTER_CONTRACT, o++); + body.copy(data, o); + o += 98; + Buffer.from(r).copy(data, o); + o += 32; + Buffer.from(s).copy(data, o); + o += 32; + data.writeUInt8(v, o); + return data; +} + +/** + * Build RequestQuote instruction data. + * Uses 8-byte discriminator for Anchor compatibility. + * Layout: discriminator (8) + dst_chain (2) + dst_addr (32) + refund_addr (32) + + * request_bytes_len (4) + request_bytes + relay_instructions_len (4) + relay_instructions + */ +function buildRequestQuoteData( + dstChain: number, + dstAddr: Uint8Array, + refundAddr: Uint8Array, + requestBytes: Uint8Array, + relayInstructions: Buffer, +): Buffer { + const data = Buffer.alloc( + 8 + 2 + 32 + 32 + 4 + requestBytes.length + 4 + relayInstructions.length, + ); + let o = 0; + // 8-byte discriminator: instruction ID in first byte, rest zeros + data.writeUInt8(IX_QUOTER_REQUEST_QUOTE, o++); + o += 7; // padding for 8-byte discriminator + data.writeUInt16LE(dstChain, o); + o += 2; + Buffer.from(dstAddr).copy(data, o); + o += 32; + Buffer.from(refundAddr).copy(data, o); + o += 32; + data.writeUInt32LE(requestBytes.length, o); + o += 4; + Buffer.from(requestBytes).copy(data, o); + o += requestBytes.length; + data.writeUInt32LE(relayInstructions.length, o); + o += 4; + relayInstructions.copy(data, o); + return data; +} + +/** + * Build QuoteExecution instruction data for the router. + * Layout: discriminator (1) + quoter_address (20) + quoter_cpi_data + * + * Quoter CPI data (passed to quoter): + * - discriminator (8) - must be [2, 0, 0, 0, 0, 0, 0, 0] + * - dst_chain (2) + * - dst_addr (32) + * - refund_addr (32) + * - request_bytes_len (4) + request_bytes + * - relay_instructions_len (4) + relay_instructions + */ +function buildQuoteExecutionData( + quoterAddress: Uint8Array, + dstChain: number, + dstAddr: Uint8Array, + refundAddr: Uint8Array, + requestBytes: Uint8Array, + relayInstructions: Uint8Array, +): Buffer { + // CPI data size: 8 + 2 + 32 + 32 + 4 + requestBytes.length + 4 + relayInstructions.length + const cpiDataLen = + 8 + 2 + 32 + 32 + 4 + requestBytes.length + 4 + relayInstructions.length; + const data = Buffer.alloc(1 + 20 + cpiDataLen); + let o = 0; + + // Router discriminator + data.writeUInt8(IX_ROUTER_QUOTE_EXECUTION, o++); + + // Quoter address for registration lookup + Buffer.from(quoterAddress).copy(data, o); + o += 20; + + // Quoter CPI data - 8-byte discriminator [2, 0, 0, 0, 0, 0, 0, 0] for RequestQuote + data.writeUInt8(2, o++); // instruction ID + o += 7; // padding + + // Rest of quoter request data + data.writeUInt16LE(dstChain, o); + o += 2; + Buffer.from(dstAddr).copy(data, o); + o += 32; + Buffer.from(refundAddr).copy(data, o); + o += 32; + data.writeUInt32LE(requestBytes.length, o); + o += 4; + Buffer.from(requestBytes).copy(data, o); + o += requestBytes.length; + data.writeUInt32LE(relayInstructions.length, o); + o += 4; + Buffer.from(relayInstructions).copy(data, o); + return data; +} + +function buildGasRelayInstruction(gasLimit: bigint, msgValue: bigint): Buffer { + const data = Buffer.alloc(33); + data.writeUInt8(1, 0); + const gasLimitBuf = Buffer.alloc(16); + gasLimitBuf.writeBigUInt64BE(gasLimit >> 64n, 0); + gasLimitBuf.writeBigUInt64BE(gasLimit & 0xffffffffffffffffn, 8); + gasLimitBuf.copy(data, 1); + const msgValueBuf = Buffer.alloc(16); + msgValueBuf.writeBigUInt64BE(msgValue >> 64n, 0); + msgValueBuf.writeBigUInt64BE(msgValue & 0xffffffffffffffffn, 8); + msgValueBuf.copy(data, 17); + return data; +} + +// Simulation helper + +async function simulateInstruction( + connection: Connection, + wallet: Keypair, + ix: TransactionInstruction, +): Promise<{ computeUnits: number; returnData: Buffer }> { + const { blockhash } = await connection.getLatestBlockhash(); + const msg = new TransactionMessage({ + payerKey: wallet.publicKey, + recentBlockhash: blockhash, + instructions: [ix], + }).compileToV0Message(); + const tx = new VersionedTransaction(msg); + + const sim = await connection.simulateTransaction(tx, { sigVerify: false }); + + if (sim.value.err) { + throw new Error( + `Simulation failed: ${JSON.stringify(sim.value.err)}\nLogs: ${sim.value.logs?.join("\n")}`, + ); + } + if (!sim.value.returnData) { + throw new Error("No return data"); + } + + return { + computeUnits: sim.value.unitsConsumed ?? 0, + returnData: Buffer.from(sim.value.returnData.data[0], "base64"), + }; +} + +// Test context +let connection: Connection; +let wallet: Keypair; +let quoterConfigPda: PublicKey; +let quoterChainInfoPda: PublicKey; +let quoterQuoteBodyPda: PublicKey; +let routerConfigPda: PublicKey; +let quoterIdentity: QuoterIdentity; +let quoterRegistrationPda: PublicKey; + +// Calculate expected quote value +const EXPECTED_QUOTE = calculateExpectedQuote( + TEST_BASE_FEE, + TEST_SRC_PRICE, + TEST_DST_PRICE, + TEST_DST_GAS_PRICE, + TEST_GAS_PRICE_DECIMALS, + TEST_NATIVE_DECIMALS, + TEST_GAS_LIMIT, + TEST_MSG_VALUE, +); + +describe("executor-quoter", () => { + beforeAll(async () => { + connection = new Connection("https://api.devnet.solana.com", "confirmed"); + wallet = loadWallet(); + + [quoterConfigPda] = deriveQuoterConfigPda(); + [quoterChainInfoPda] = deriveQuoterChainInfoPda(CHAIN_ID_ETHEREUM); + [quoterQuoteBodyPda] = deriveQuoterQuoteBodyPda(CHAIN_ID_ETHEREUM); + }); + + test("updates chain info for Ethereum", async () => { + // Accounts: [payer, updater, chain_info, system_program] + const ix = new TransactionInstruction({ + programId: QUOTER_PROGRAM_ID, + keys: [ + { pubkey: wallet.publicKey, isSigner: true, isWritable: true }, + { pubkey: wallet.publicKey, isSigner: true, isWritable: false }, + { pubkey: quoterChainInfoPda, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + data: buildUpdateChainInfoData( + CHAIN_ID_ETHEREUM, + 1, + TEST_GAS_PRICE_DECIMALS, + TEST_NATIVE_DECIMALS, + ), + }); + + const tx = new Transaction().add(ix); + await sendAndConfirmTransaction(connection, tx, [wallet]); + + expect(await connection.getAccountInfo(quoterChainInfoPda)).not.toBeNull(); + }); + + test("updates quote for Ethereum", async () => { + // Accounts: [payer, updater, quote_body, system_program] + const ix = new TransactionInstruction({ + programId: QUOTER_PROGRAM_ID, + keys: [ + { pubkey: wallet.publicKey, isSigner: true, isWritable: true }, + { pubkey: wallet.publicKey, isSigner: true, isWritable: false }, + { pubkey: quoterQuoteBodyPda, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + data: buildUpdateQuoteData( + CHAIN_ID_ETHEREUM, + TEST_DST_PRICE, + TEST_SRC_PRICE, + TEST_DST_GAS_PRICE, + TEST_BASE_FEE, + ), + }); + + const tx = new Transaction().add(ix); + await sendAndConfirmTransaction(connection, tx, [wallet]); + + expect(await connection.getAccountInfo(quoterQuoteBodyPda)).not.toBeNull(); + }); + + test("returns correct quote via RequestQuote", async () => { + const dstAddr = new Uint8Array(32).fill(0xab); + const refundAddr = new Uint8Array(32); + wallet.publicKey.toBuffer().copy(Buffer.from(refundAddr)); + + // Accounts: [_config, chain_info, quote_body] + const ix = new TransactionInstruction({ + programId: QUOTER_PROGRAM_ID, + keys: [ + { pubkey: quoterConfigPda, isSigner: false, isWritable: false }, + { pubkey: quoterChainInfoPda, isSigner: false, isWritable: false }, + { pubkey: quoterQuoteBodyPda, isSigner: false, isWritable: false }, + ], + data: buildRequestQuoteData( + CHAIN_ID_ETHEREUM, + dstAddr, + refundAddr, + new Uint8Array(0), + buildGasRelayInstruction(TEST_GAS_LIMIT, TEST_MSG_VALUE), + ), + }); + + const { returnData } = await simulateInstruction(connection, wallet, ix); + + expect(returnData.length).toBe(8); + const payment = returnData.readBigUInt64BE(0); + expect(payment).toBe(EXPECTED_QUOTE); + }); + + test("msg_value increases the quote", async () => { + const dstAddr = new Uint8Array(32).fill(0xab); + const refundAddr = new Uint8Array(32); + wallet.publicKey.toBuffer().copy(Buffer.from(refundAddr)); + + // Quote without msg_value + const ixNoValue = new TransactionInstruction({ + programId: QUOTER_PROGRAM_ID, + keys: [ + { pubkey: quoterConfigPda, isSigner: false, isWritable: false }, + { pubkey: quoterChainInfoPda, isSigner: false, isWritable: false }, + { pubkey: quoterQuoteBodyPda, isSigner: false, isWritable: false }, + ], + data: buildRequestQuoteData( + CHAIN_ID_ETHEREUM, + dstAddr, + refundAddr, + new Uint8Array(0), + buildGasRelayInstruction(TEST_GAS_LIMIT, 0n), + ), + }); + + // Quote with msg_value + const ixWithValue = new TransactionInstruction({ + programId: QUOTER_PROGRAM_ID, + keys: [ + { pubkey: quoterConfigPda, isSigner: false, isWritable: false }, + { pubkey: quoterChainInfoPda, isSigner: false, isWritable: false }, + { pubkey: quoterQuoteBodyPda, isSigner: false, isWritable: false }, + ], + data: buildRequestQuoteData( + CHAIN_ID_ETHEREUM, + dstAddr, + refundAddr, + new Uint8Array(0), + buildGasRelayInstruction(TEST_GAS_LIMIT, TEST_MSG_VALUE), + ), + }); + + const { returnData: noValueData } = await simulateInstruction( + connection, + wallet, + ixNoValue, + ); + const { returnData: withValueData } = await simulateInstruction( + connection, + wallet, + ixWithValue, + ); + + const quoteNoValue = noValueData.readBigUInt64BE(0); + const quoteWithValue = withValueData.readBigUInt64BE(0); + + // Calculate expected values + const expectedNoValue = calculateExpectedQuote( + TEST_BASE_FEE, + TEST_SRC_PRICE, + TEST_DST_PRICE, + TEST_DST_GAS_PRICE, + TEST_GAS_PRICE_DECIMALS, + TEST_NATIVE_DECIMALS, + TEST_GAS_LIMIT, + 0n, + ); + const expectedWithValue = calculateExpectedQuote( + TEST_BASE_FEE, + TEST_SRC_PRICE, + TEST_DST_PRICE, + TEST_DST_GAS_PRICE, + TEST_GAS_PRICE_DECIMALS, + TEST_NATIVE_DECIMALS, + TEST_GAS_LIMIT, + TEST_MSG_VALUE, + ); + + expect(quoteNoValue).toBe(expectedNoValue); + expect(quoteWithValue).toBe(expectedWithValue); + expect(quoteWithValue).toBeGreaterThan(quoteNoValue); + + // The difference should be 1 ETH * (ETH_price / SOL_price) in lamports + // = 1 ETH * 15 = 15 SOL = 15_000_000_000 lamports + const expectedDiff = 15_000_000_000n; + expect(quoteWithValue - quoteNoValue).toBe(expectedDiff); + }); +}); + +describe("executor-quoter testnet chains", () => { + beforeAll(async () => { + connection = new Connection("https://api.devnet.solana.com", "confirmed"); + wallet = loadWallet(); + [quoterConfigPda] = deriveQuoterConfigPda(); + }); + + for (const chain of TESTNET_CHAINS) { + test(`updates chain info for ${chain.name} (${chain.chainId})`, async () => { + const [chainInfoPda] = deriveQuoterChainInfoPda(chain.chainId); + + const ix = new TransactionInstruction({ + programId: QUOTER_PROGRAM_ID, + keys: [ + { pubkey: wallet.publicKey, isSigner: true, isWritable: true }, + { pubkey: wallet.publicKey, isSigner: true, isWritable: false }, + { pubkey: chainInfoPda, isSigner: false, isWritable: true }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ], + data: buildUpdateChainInfoData( + chain.chainId, + 1, // enabled + chain.gasPriceDecimals, + chain.nativeDecimals, + ), + }); + + const tx = new Transaction().add(ix); + await sendAndConfirmTransaction(connection, tx, [wallet]); + + const accountInfo = await connection.getAccountInfo(chainInfoPda); + expect(accountInfo).not.toBeNull(); + console.log( + ` Chain info PDA for ${chain.name}: ${chainInfoPda.toBase58()}`, + ); + }); + + test(`updates quote for ${chain.name} (${chain.chainId})`, async () => { + const [quoteBodyPda] = deriveQuoterQuoteBodyPda(chain.chainId); + + const ix = new TransactionInstruction({ + programId: QUOTER_PROGRAM_ID, + keys: [ + { pubkey: wallet.publicKey, isSigner: true, isWritable: true }, + { pubkey: wallet.publicKey, isSigner: true, isWritable: false }, + { pubkey: quoteBodyPda, isSigner: false, isWritable: true }, + { + pubkey: SystemProgram.programId, + isSigner: false, + isWritable: false, + }, + ], + data: buildUpdateQuoteData( + chain.chainId, + chain.dstPrice, + TEST_SRC_PRICE, + chain.dstGasPrice, + chain.baseFee, + ), + }); + + const tx = new Transaction().add(ix); + await sendAndConfirmTransaction(connection, tx, [wallet]); + + const accountInfo = await connection.getAccountInfo(quoteBodyPda); + expect(accountInfo).not.toBeNull(); + console.log( + ` Quote body PDA for ${chain.name}: ${quoteBodyPda.toBase58()}`, + ); + }); + + test(`returns correct quote for ${chain.name} (${chain.chainId})`, async () => { + const [chainInfoPda] = deriveQuoterChainInfoPda(chain.chainId); + const [quoteBodyPda] = deriveQuoterQuoteBodyPda(chain.chainId); + + const dstAddr = new Uint8Array(32).fill(0xab); + const refundAddr = new Uint8Array(32); + wallet.publicKey.toBuffer().copy(Buffer.from(refundAddr)); + + const ix = new TransactionInstruction({ + programId: QUOTER_PROGRAM_ID, + keys: [ + { pubkey: quoterConfigPda, isSigner: false, isWritable: false }, + { pubkey: chainInfoPda, isSigner: false, isWritable: false }, + { pubkey: quoteBodyPda, isSigner: false, isWritable: false }, + ], + data: buildRequestQuoteData( + chain.chainId, + dstAddr, + refundAddr, + new Uint8Array(0), + buildGasRelayInstruction(TEST_GAS_LIMIT, TEST_MSG_VALUE), + ), + }); + + const { returnData } = await simulateInstruction(connection, wallet, ix); + + expect(returnData.length).toBe(8); + const payment = returnData.readBigUInt64BE(0); + + // Calculate expected quote + const expectedQuote = calculateExpectedQuote( + chain.baseFee, + TEST_SRC_PRICE, + chain.dstPrice, + chain.dstGasPrice, + chain.gasPriceDecimals, + chain.nativeDecimals, + TEST_GAS_LIMIT, + TEST_MSG_VALUE, + ); + + expect(payment).toBe(expectedQuote); + console.log( + ` Quote for ${chain.name}: ${payment} lamports (${Number(payment) / 1e9} SOL)`, + ); + }); + } +}); + +describe("executor-quoter-router", () => { + beforeAll(async () => { + connection = new Connection("https://api.devnet.solana.com", "confirmed"); + wallet = loadWallet(); + + [quoterConfigPda] = deriveQuoterConfigPda(); + [quoterChainInfoPda] = deriveQuoterChainInfoPda(CHAIN_ID_ETHEREUM); + [quoterQuoteBodyPda] = deriveQuoterQuoteBodyPda(CHAIN_ID_ETHEREUM); + [routerConfigPda] = deriveRouterConfigPda(); + + quoterIdentity = new QuoterIdentity(); + [quoterRegistrationPda] = deriveQuoterRegistrationPda( + quoterIdentity.ethAddress, + ); + }); + + test("registers quoter via governance", async () => { + const expiryTime = BigInt(Math.floor(Date.now() / 1000) + 3600); + + // Accounts: [payer, sender, _config, quoter_registration, system_program] + const ix = new TransactionInstruction({ + programId: ROUTER_PROGRAM_ID, + keys: [ + { pubkey: wallet.publicKey, isSigner: true, isWritable: true }, + { pubkey: wallet.publicKey, isSigner: true, isWritable: false }, + { pubkey: routerConfigPda, isSigner: false, isWritable: false }, + { pubkey: quoterRegistrationPda, isSigner: false, isWritable: true }, + { pubkey: SystemProgram.programId, isSigner: false, isWritable: false }, + ], + data: await buildUpdateQuoterContractData( + quoterIdentity, + QUOTER_PROGRAM_ID, + wallet.publicKey, + CHAIN_ID_SOLANA, + expiryTime, + ), + }); + + const tx = new Transaction().add(ix); + await sendAndConfirmTransaction(connection, tx, [wallet]); + + expect( + await connection.getAccountInfo(quoterRegistrationPda), + ).not.toBeNull(); + }); + + test("returns correct quote via QuoteExecution CPI", async () => { + const dstAddr = new Uint8Array(32).fill(0xab); + const refundAddr = new Uint8Array(32); + wallet.publicKey.toBuffer().copy(Buffer.from(refundAddr)); + + // Accounts: [quoter_registration, quoter_program, quoter_config, quoter_chain_info, quoter_quote_body] + const ix = new TransactionInstruction({ + programId: ROUTER_PROGRAM_ID, + keys: [ + { pubkey: quoterRegistrationPda, isSigner: false, isWritable: false }, + { pubkey: QUOTER_PROGRAM_ID, isSigner: false, isWritable: false }, + { pubkey: quoterConfigPda, isSigner: false, isWritable: false }, + { pubkey: quoterChainInfoPda, isSigner: false, isWritable: false }, + { pubkey: quoterQuoteBodyPda, isSigner: false, isWritable: false }, + ], + data: buildQuoteExecutionData( + quoterIdentity.ethAddress, + CHAIN_ID_ETHEREUM, + dstAddr, + refundAddr, + new Uint8Array(0), + buildGasRelayInstruction(TEST_GAS_LIMIT, TEST_MSG_VALUE), + ), + }); + + const { returnData } = await simulateInstruction(connection, wallet, ix); + + expect(returnData.length).toBe(8); + const payment = returnData.readBigUInt64BE(0); + expect(payment).toBe(EXPECTED_QUOTE); + }); +}); diff --git a/svm/tsconfig.json b/svm/tsconfig.json index cd5d2e3..f557baf 100644 --- a/svm/tsconfig.json +++ b/svm/tsconfig.json @@ -1,10 +1,10 @@ { "compilerOptions": { - "types": ["mocha", "chai"], + "types": ["mocha", "chai", "node"], "typeRoots": ["./node_modules/@types"], - "lib": ["es2015"], + "lib": ["es2020"], "module": "commonjs", - "target": "es6", + "target": "es2020", "esModuleInterop": true } } diff --git a/svm/yarn.lock b/svm/yarn.lock index 14410f5..b8de155 100644 --- a/svm/yarn.lock +++ b/svm/yarn.lock @@ -55,6 +55,16 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.5.0.tgz#abadc5ca20332db2b1b2aa3e496e9af1213570b0" integrity sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA== +"@noble/hashes@^2.0.1": + version "2.0.1" + resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-2.0.1.tgz#fc1a928061d1232b0a52bb754393c37a5216c89e" + integrity sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw== + +"@noble/secp256k1@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-3.0.0.tgz#29711361db8f37b1b7e0b8d80c933013fc887475" + integrity sha512-NJBaR352KyIvj3t6sgT/+7xrNyF9Xk9QlLSIqUGVUYlsnDTAUqY8LOmwpcgEx4AMJXRITQ5XEVHD+mMaPfr3mg== + "@solana/buffer-layout@^4.0.1": version "4.0.1" resolved "https://registry.yarnpkg.com/@solana/buffer-layout/-/buffer-layout-4.0.1.tgz#b996235eaec15b1e0b5092a8ed6028df77fa6c15" @@ -150,6 +160,13 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-12.20.55.tgz#c329cbd434c42164f846b909bd6f85b5537f6240" integrity sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ== +"@types/node@^24.10.1": + version "24.10.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-24.10.1.tgz#91e92182c93db8bd6224fca031e2370cef9a8f01" + integrity sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ== + dependencies: + undici-types "~7.16.0" + "@types/uuid@^8.3.4": version "8.3.4" resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.4.tgz#bd86a43617df0594787d38b735f55c805becf1bc" @@ -689,6 +706,11 @@ jayson@^4.1.1: uuid "^8.3.2" ws "^7.5.10" +js-sha3@^0.9.3: + version "0.9.3" + resolved "https://registry.yarnpkg.com/js-sha3/-/js-sha3-0.9.3.tgz#f0209432b23a66a0f6c7af592c26802291a75c2a" + integrity sha512-BcJPCQeLg6WjEx3FE591wVAevlli8lxsxm9/FzV4HXkV49TmBH38Yvrpce6fjbADGMKFrBMGTqrVz3qPIZ88Gg== + js-yaml@4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" @@ -1092,6 +1114,11 @@ undici-types@~6.19.2: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +undici-types@~7.16.0: + version "7.16.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-7.16.0.tgz#ffccdff36aea4884cbfce9a750a0580224f58a46" + integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== + utf-8-validate@^5.0.2: version "5.0.10" resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2"