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"